chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
//            Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using LibGit2Sharp;
using System.Linq;
using System.Collections.Generic;
using System;
using System.IO;
using Tap.Shared;
 
namespace OpenTap.Package
{
    /// <summary>
    /// Calculates the version number of a commit in a git repository
    /// </summary>
    internal class GitVersionCalulator : IDisposable
    {
        private static readonly TraceSource log = Log.CreateSource("GitVersion");
        private const string configFileName = ".gitversion";
        private readonly LibGit2Sharp.Repository repo;
        private readonly string RepoDir;
 
        private class Config
        {
            public SemanticVersion Version { get => _version; set => _version = value; }
            private SemanticVersion _version = new SemanticVersion(0, 0, 1, null, null);
 
            /// <summary> version before it got parsed to a SemanticVersion. Possibly not valid.</summary>
            public string RawVersion { get; set; }
            
            /// <summary>
            /// Regex that runs against the FriendlyName of a branch to determine if it is a beta branch 
            /// (commits from this branch will get a "beta" prerelease identifier)
            /// </summary>
            public List<Regex> BetaBranchRegexes { get; private set; } = new List<Regex>
            {
                new Regex("^integration$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
                new Regex("^develop$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
                new Regex("^dev$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
                new Regex("^master$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
                new Regex("^main$", RegexOptions.Compiled | RegexOptions.IgnoreCase)
            };
            public List<string> BetaBranchPatterns { get; private set; } = new List<string>
            {
                "^integration$",
                "^master$",
                "^develop$",
                "^dev$",
                "^main$"
            };
 
            /// <summary>
            /// Regex that runs against the FriendlyName of a branch to determine if it is a release branch 
            /// (commits from this branch will not get any prerelease identifier)
            /// </summary>
            public Regex ReleaseBranchRegex { get; private set; } = new Regex("^release[0-9x]*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
            public Regex ReleaseTagRegex { get; private set; } = new Regex(@"v\d+\.\d+\.\d+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
            private int _maxBranchChars = 30;
            /// <summary>
            /// Cap the length of the branch name to this many chars. This can be useful e.g. if the version number is used in a file name, which could otherwise become too long.
            /// </summary>
            public int MaxBranchChars => _maxBranchChars;
 
            public string ConfigFilePath;
 
            private Config()
            {
 
            }
 
            private static Regex configLineRegex = new Regex(@"^(?!#)(?<key>.*?)\s*=\s*(?<value>.*)", RegexOptions.Compiled);
            public static Config ParseConfig(Stream str, string configFilePath = configFileName)
            {
                Config cfg = new Config();
                if (str == null)
                    return cfg;
                cfg.ConfigFilePath = configFilePath;
                bool isBetaBranchSet = false;
                using (var reader = new StreamReader(str, Encoding.UTF8))
                {
                    String line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        var m = configLineRegex.Match(line);
                        if (m.Success)
                        {
                            string val = m.Groups["value"].Value;
                            switch (m.Groups["key"].Value.ToLower())
                            {
                                case "beta branch":
                                    if (!isBetaBranchSet)
                                    {
                                        cfg.BetaBranchRegexes = new List<Regex>();
                                        cfg.BetaBranchPatterns = new List<string>();
                                    }
                                    isBetaBranchSet = true;
                                    cfg.BetaBranchRegexes.Add(new Regex(val, RegexOptions.IgnoreCase));
                                    cfg.BetaBranchPatterns.Add(val);
                                    break;
                                case "release branch":
                                    cfg.ReleaseBranchRegex = new Regex(val, RegexOptions.IgnoreCase);
                                    break;
                                case "release tag":
                                    cfg.ReleaseTagRegex = new Regex(val, RegexOptions.IgnoreCase);
                                    break;
                                case "max branch chars":
                                    int.TryParse(val, out cfg._maxBranchChars);
                                    break;
                                case "version":
                                    cfg.RawVersion = val;
                                    SemanticVersion.TryParse(val, out cfg._version);
                                    break;
                            }
                        }
                    }
                }
                return cfg;
            }
        }
        
        private const string GIT_HASH = "b7bad55";
        
        void ensureLibgit2Present()
        {
            string libgit2name;
 
            if (OperatingSystem.Current == OperatingSystem.Windows)
                libgit2name = $"git2-{GIT_HASH}.dll";
            else if (OperatingSystem.Current == OperatingSystem.Linux)
                libgit2name = $"libgit2-{GIT_HASH}.so";
            else if (OperatingSystem.Current == OperatingSystem.MacOS)
                libgit2name = $"libgit2-{GIT_HASH}.dylib";
            else
            {
                log.Error($"Unsupported platform.");
                return;
            }
            
            var requiredFile = Path.Combine(PathUtils.OpenTapDir, libgit2name);
            if (File.Exists(requiredFile))
                return;
 
            string sourceFile = Path.Combine(PathUtils.OpenTapDir, "Dependencies/LibGit2Sharp.0.27.0.0/", libgit2name);
            if (OperatingSystem.Current == OperatingSystem.Windows)
                sourceFile += $".{(Environment.Is64BitProcess ? CpuArchitecture.x64 : CpuArchitecture.x86)}";
            if (OperatingSystem.Current == OperatingSystem.MacOS)
                sourceFile += $".{MacOsArchitecture.Current.Architecture}";
            if (OperatingSystem.Current == OperatingSystem.Linux)
                sourceFile += $".{LinuxArchitecture.Current.Architecture}";
 
            try
            {
                File.Copy(sourceFile, requiredFile, true);
            }
            catch (Exception e)
            {
                if (OperatingSystem.Current == OperatingSystem.Windows)
                {
                    var opentapArch = Installation.Current.GetOpenTapPackage()?.Architecture;
                    var processArch = Environment.Is64BitProcess ? CpuArchitecture.x64 : CpuArchitecture.x86;
                    if (opentapArch != processArch)
                        throw new PlatformNotSupportedException($"Unable to find the correct 'libgit2-{GIT_HASH}' because the process architecture '{processArch}' does not match the installed OpenTAP architecture '{opentapArch}'", e);
                }
 
                throw new PlatformNotSupportedException($"Unable to copy 'libgit2-{GIT_HASH}': {e.Message}.", e);
            }
        }
 
        /// <summary>
        /// Instanciates a new <see cref="GitVersionCalulator"/> to work on a specified git repository.
        /// </summary>
        /// <param name="repositoryDir">Path pointing to a directory inside the git repository to use.</param>
        public GitVersionCalulator(string repositoryDir)
        {
            repositoryDir = Path.GetFullPath(repositoryDir);
            RepoDir = repositoryDir;
            while (!Directory.Exists(Path.Combine(repositoryDir, ".git")))
            {
                repositoryDir = Path.GetDirectoryName(repositoryDir);
                if (repositoryDir == null)
                    throw new ArgumentException("Directory is not a git repository.", "repositoryDir");
            }
            RepoDir = RepoDir.Substring(repositoryDir.Length);
 
            ensureLibgit2Present();
            repo = new LibGit2Sharp.Repository(repositoryDir);
        }
 
        public void Dispose()
        {
            if (repo != null)
                repo.Dispose();
        }
 
        /// <summary> Keeps iterating until a valid version is read.</summary>
        SemanticVersion getLatestReadableVersion(Commit c)
        {
            while (c != null)
            {
                var cfg = readConfig(c);
                if (cfg.Version != null) return cfg.Version;
                c = getLatestConfigVersionChange(c.Parents.FirstOrDefault());
            }
            // no version was found.
            return null;
        }
 
        static IEnumerable<TreeEntry> GetAllConfigFilesInTree(Tree t)
        {
            foreach (TreeEntry te in t)
            {
                if (te.Target is Tree subtree)
                {
                    foreach (var match in GetAllConfigFilesInTree(subtree))
                        yield return match;
                }
                else if (te.Name == configFileName)
                {
                    yield return te;
                }
            }
        }
 
        Config readConfig(Commit c)
        {
            var cfgFiles = GetAllConfigFilesInTree(c?.Tree).ToList();
 
            string repositoryDir = RepoDir.TrimStart('/','\\');
            TreeEntry cfg = null;
            while (cfg == null)
            {
                var dir = Path.Combine(repositoryDir ?? "", configFileName).Replace('\\','/');
                cfg = cfgFiles.FirstOrDefault(c => c.Path == dir);
                if (String.IsNullOrEmpty(repositoryDir))
                    break;
                repositoryDir = Path.GetDirectoryName(repositoryDir);
            }
            Blob configBlob = cfg?.Target as Blob;
            return Config.ParseConfig(configBlob?.GetContentStream(), cfg?.Path);
        }
 
        Config ParseConfig(Commit c)
        {
            var cfg = readConfig(c);
            if (cfg.Version == null)
            {
                log.Error("Unable to parse version specification {0}. It is not a valid semantic version.", cfg.RawVersion);
                var ver = getLatestReadableVersion(c.Parents.FirstOrDefault());
                if (ver != null)
                {
                    log.Warning("Using previous {0} as version instead.", ver);
                    cfg.Version = ver;
                }
            }
             
            return cfg;
        }
 
        private Commit getLatestConfigVersionChange(Commit c)
        {
            if (c.Parents.Any() == false)
                return c; // 'c' is the first commit in the repo. There was never any change.
            
            // find all changes in the file (for some reason that sometimes returns an empty list)
            //var fileLog = repo.Commits.QueryBy(configFileName, new CommitFilter() { IncludeReachableFrom = c, SortBy = CommitSortStrategies.Topological, FirstParentOnly = false });
            //... go on to iterate through filelog...
 
            // Instead, just walk all commits comparing the version in the .gitversion file to the one in the previous commit
            Config currentCfg = readConfig(c);
            while (true)
            {
                Commit parent = c.Parents.FirstOrDefault(); // first parent only, we are only interested in when the file changes on the beta branch
                if (parent == null)
                {
                    // we got to the very first commit in this repo without seeing any changes in the gitversion
                    // this might be because there is no .gitversion file, or just because the content of the file is the same as the default values.
                    // in both cases, we should treat this commit (the initial commit) as the LatestConfigVersionChange
                    return c;
                }
                Config parentCfg = readConfig(parent);                
                if (currentCfg.Version != null && (parentCfg.Version == null || currentCfg.Version.CompareTo(parentCfg.Version) > 0))
                {
                    // the version number was bumped
                    return c;
                }
                c = parent;
                currentCfg = parentCfg;
            }
        }
        
        /// <summary>
        /// Calculates the version number of the current HEAD of the git repository
        /// </summary>
        public SemanticVersion GetVersion()
        {
            if (!repo.Commits.Any())
                return new SemanticVersion(0, 0, 0, null, null);
            return GetVersion(repo.Head.Tip);
        }
 
        /// <summary>
        /// Calculates the version number of a specific commit in the git repository
        /// </summary>
        public SemanticVersion GetVersion(string sha)
        {
            Commit commit = repo.Lookup<Commit>(sha);
            if (commit == null)
                throw new ArgumentException($"The commit with reference {sha} does not exist in the repository.");
            return GetVersion(commit);
        }
 
        /// <summary>
        /// Calculates the version number of a specific commit in the git repository
        /// </summary>
        public SemanticVersion GetVersion(Commit targetCommit)
        {
            if (repo.Lookup<Commit>(targetCommit.Sha) == null)
                throw new ArgumentException($"The commit with hash {targetCommit} does not exist the in repository.");
            if(!GetAllConfigFilesInTree(targetCommit.Tree).Any())
            {
                log.Warning("Did not find any .gitversion file.");
            }
            Config cfg = ParseConfig(targetCommit);
            if (cfg.ConfigFilePath != configFileName)
                log.Debug("Using configuration from {0}", cfg.ConfigFilePath);
 
            Branch defaultBranch = getBetaBranch(cfg);
 
            string branchName = guessBranchName(cfg,targetCommit,defaultBranch);
 
            string preRelease = "alpha";
            if (branchName == defaultBranch.GetShortName())
                preRelease = "beta";
            if (cfg.ReleaseBranchRegex.IsMatch(branchName))
                preRelease = "rc";
            Tag releaseTag = getReleaseTag(cfg, targetCommit);
            if (releaseTag != null)
                preRelease = null;
 
            string metadata = targetCommit.Sha.Substring(0, 8);
            if (preRelease == "alpha")
            {
                if (branchName == "(no branch)")
                    branchName = "NONE"; // '(' and ' ' are not allowed in semver
                else
                    branchName = Regex.Replace(branchName, "[^a-zA-Z0-9-]", "-"); // replace any chars that is not valid semver with '-'
                if (branchName.Length > cfg.MaxBranchChars)
                    branchName = branchName.Remove(cfg.MaxBranchChars);
                metadata += "." + branchName;
            }
            if (!String.IsNullOrEmpty(preRelease))
            {
                // The version calculation is slightly different for RC versions
                // For an RC, we want to count merge commits as a single commit
                // For other branches, we want to count the literal number of commits
                // Historically, we have counted merge commits as single commits for all branches,
                // but this causes issues in scenarios where merge commits are fast-forwarded onto e.g. the main branch.
                // See here: https://github.com/opentap/opentap/pull/1384
                // And here: https://github.com/opentap/opentap/issues/1321#issuecomment-1895749385
                bool isRc = preRelease.StartsWith("rc", StringComparison.OrdinalIgnoreCase);
                Commit cfgCommit = getLatestConfigVersionChange(targetCommit);
                Commit commonAncestor = findFirstCommonAncestor(defaultBranch, targetCommit);
                int commitsFromDefaultBranch = countCommitsBetween(commonAncestor, targetCommit, firstParentOnly: isRc);
                log.Debug("Found {0} commits since branchout from beta branch in commit {1}.", commitsFromDefaultBranch, commonAncestor.Sha.Substring(0, 8));
                int commitsSinceVersionUpdate = countCommitsBetween(cfgCommit, targetCommit, firstParentOnly: isRc) + 1;
                log.Debug("Found {0} commits since last version bump in commit {1}.", commitsSinceVersionUpdate, cfgCommit.Sha.Substring(0, 8));
                int alphaVersion = Math.Min(commitsFromDefaultBranch, commitsSinceVersionUpdate);
                if (isRc == false)
                {
                    int betaVersion = countCommitsBetween(cfgCommit, commonAncestor, false) + 1;
                    if (betaVersion > 0)
                    {
                        preRelease += "." + betaVersion;
                    }
                }
                if (alphaVersion > 0)
                {
                    preRelease += "." + alphaVersion;
                }
            }
 
            if (cfg.Version == null) return new SemanticVersion(0, 0, 0, preRelease, metadata);
            return new SemanticVersion(cfg.Version.Major,cfg.Version.Minor,cfg.Version.Patch,preRelease,metadata);
        }
        
        private Tag getReleaseTag(Config cfg, Commit c)
        {
            foreach (Tag t in repo.Tags)
            {
                if (t.IsAnnotated &&
                    t.Target.Peel<Commit>() == c &&
                    cfg.ReleaseTagRegex.IsMatch(t.FriendlyName))
                {
                    return t;
                }
            }
            return null;
        }
 
        /// <summary>
        /// Find the first (youngest) commit that is reachable from two specified places
        /// </summary>
        private Commit findFirstCommonAncestor(Branch b1, Commit target)
        {
            // This fixes gitversion calculation in scenarios where the local revision of
            // a checked out branch is behind the origin branch. If the local revision is fully merged
            // in the remote tracking branch, we base our calculation on the remote branch instead.
            // Otherwise, if the local branch contains commits that are *not* merged in the remote, we base the calculation on that.
            // This should make the gitversion calculation work as expected after `git fetch --all`.
            if (b1.TrackedBranch != null)
            {
                // Check if any local commits are unreachable from the tracking branch
                var commitsMissingFromUpstream = (IQueryableCommitLog)repo.Commits.QueryBy(new CommitFilter() { IncludeReachableFrom = b1.Tip, ExcludeReachableFrom = b1.TrackedBranch.Tip});
                if (commitsMissingFromUpstream.Any())
                {
                    throw new Exception(
                        $"The local branch '{b1.GetShortName()}' contains commits missing from the tracked upstream branch.\n" +
                        $"This can cause unexpected mismatching version numbers. Please align '{b1.GetShortName()}' with its upstream.");
                }
                b1 = b1.TrackedBranch;
            }
 
            Commit b1Commit = b1.Tip;
            while (b1Commit != null)
            {
                if (b1Commit.Sha == target.Sha)
                    return target; // target is a directly on the b1 branch
                b1Commit = b1Commit.Parents.FirstOrDefault();
            }
            log.Debug($"Common ancestor of {b1.Tip} and {target} is not on the same branch.");
            HashSet<Commit> targetHistory = repo.Commits.QueryBy(new CommitFilter() { IncludeReachableFrom = target }).ToHashSet();
            Commit firstCommon = b1.Commits.FirstOrDefault(c => targetHistory.Contains(c)); // same as repo.ObjectDatabase.FindMergeBase(b1.Tip, target); but faster on average
            // if this branch is being used for several releases (merged to several times, one for each release)
            // we will need to check against older releases as well as target might already exist on the tip of 
            // this release branch (i.e. it could have been merged there "in the future").
            b1Commit = b1.Tip;
            while (firstCommon == target) // this can happen if target is later merged into b1
            {
                if (targetHistory.Contains(b1Commit))
                {
                    // We have reached past the begining of the release branch. There is no point in going further 
                    firstCommon = null;
                    break;
                }
                b1Commit = b1Commit.Parents.FirstOrDefault();
                var releaseCommits = (IQueryableCommitLog)repo.Commits.QueryBy(new CommitFilter() { SortBy = CommitSortStrategies.Topological, IncludeReachableFrom = b1Commit });
                firstCommon = releaseCommits.FirstOrDefault(c => targetHistory.Contains(c));
            }
            return firstCommon;
        }
 
        private int countCommitsBetween(object tag, object now, bool firstParentOnly = false)
        {
            var filter = new CommitFilter()
            {
                SortBy = CommitSortStrategies.Reverse | CommitSortStrategies.Time,
                ExcludeReachableFrom = tag,
                IncludeReachableFrom = now,
                FirstParentOnly = firstParentOnly
            };
            return repo.Commits.QueryBy(filter).Count();
        }
 
        private Branch getBetaBranch(Config cfg)
        {
            // Try to find the HEAD of the 
            foreach (var remote in repo.Network.Remotes)
            {
                string expectedDefaultRefName = $"refs/remotes/{remote.Name}/HEAD";
                var defaultRef = repo.Refs.FirstOrDefault(r => r.CanonicalName == expectedDefaultRefName) as SymbolicReference;
                if (defaultRef != null)
                {
                    // be careful to return the remote branch instead of any local one. On build runners the local branch might be behind, as they usually just checkout a sha not the actual branch
                    var branch = repo.Branches.FirstOrDefault(b => b.CanonicalName == defaultRef.TargetIdentifier);
                    if (branch != null)
                    {
                        log.Debug("Determined beta branch to be '{0}' by looking at the HEAD of the remote '{1}'.",
                            branch.GetShortName(), remote.Name);
                        return branch;
                    }
                }
            }
 
            // For each regex from the config, try to find a branch that matches.
            Branch defaultBranch = cfg.BetaBranchRegexes.Select(rx => repo.Branches.FirstOrDefault(b => rx.IsMatch(b.GetShortName()))).FirstOrDefault(b => b != null);
 
            if (defaultBranch == null)
            {
                StringBuilder error = new StringBuilder("Unable to determine the default branch. No branch matching ");
                error.Append(String.Join(", ", cfg.BetaBranchPatterns.SkipLastN(1).Select(p => $"'{p}'")));
                if (cfg.BetaBranchPatterns.Count > 1)
                    error.Append($" or ");
                error.Append($"'{cfg.BetaBranchPatterns.Last()}' could be found. Searched {repo.Branches.Count()} branches.");
                log.Error(error.ToString());
                log.Debug("Branches:");
                int c = 0;
                StringBuilder line = new StringBuilder();
                foreach (Branch item in repo.Branches.OrderBy(b => b.FriendlyName))
                {
                    string bName = item.FriendlyName;
                    if (bName.Length > 25)
                        bName = bName.Substring(0, 25 - 3) + "...";
                    line.AppendFormat("{0,-26}", bName);
                    c++;
                    if (c == 4)
                    {
                        log.Debug(line.ToString());
                        c = 0;
                        line.Clear();
                    }
                }
                if (line.Length > 0)
                    log.Debug(line.ToString());
                throw new NotSupportedException(error.ToString());
            }
            log.Debug("Determined beta branch to be '{0}' using regular expression match.", defaultBranch.GetShortName());
            return defaultBranch;
        }
        
        /// <summary>
        /// Try to find the name of the branch a commit was originally created on. Logs warnings if not sure.
        /// </summary>
        private string guessBranchName(Config cfg,Commit commit, Branch defaultBranch)
        {
            // is this the tip of the current branch
            if (repo.Head.Tip.Sha == commit.Sha && repo.Info.IsHeadDetached == false)
                return repo.Head.GetShortName();
 
            // is the commit directly on the default branch?
            var commitsOnDefault = repo.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = defaultBranch, FirstParentOnly = true });
            var shasOnDefault = commitsOnDefault.Select(c => c.Sha).ToHashSet();
            if (shasOnDefault.Contains(commit.Sha))
            {
                // is this also the tip of a release branch, then pick that instead
                var releaseBranch = repo.Branches.Where(r => cfg.ReleaseBranchRegex.IsMatch(r.GetShortName()) && r.Tip.Sha == commit.Sha).FirstOrDefault();
                if (releaseBranch != null)
                {
                    return releaseBranch.GetShortName();
                }
                return defaultBranch.GetShortName();
            }
 
            // is the commit the tip of any branches
            var tipMatches = repo.Branches.Where(r => r.Tip.Sha == commit.Sha).Where(r => !r.FriendlyName.EndsWith("HEAD"));
            if (tipMatches.Any())
            {
                if (tipMatches.Count() == 1)
                    return tipMatches.First().GetShortName();
                var releaseBranch = tipMatches.FirstOrDefault(b => cfg.ReleaseBranchRegex.IsMatch(b.FriendlyName));
                if (releaseBranch != null)
                    return releaseBranch.GetShortName();
                if (tipMatches.Contains(defaultBranch))
                    return defaultBranch.GetShortName();
                log.Warning("This commit is the tip of several branches, picking one. ({0})",String.Join(", ", tipMatches.Select(b => b.FriendlyName)));
                return tipMatches.First().GetShortName();
            }
 
            // is the commit on the default branch indirectly through a merge commit
            foreach (Commit onDefault in commitsOnDefault)
            {
                if (onDefault.Parents.Count() > 1)
                {
                    Commit commitOnBranch = onDefault.Parents.Last();
 
                    while (!shasOnDefault.Contains(commitOnBranch.Sha))
                    {
                        if (commitOnBranch.Sha == commit.Sha)
                        {
                            var m = Regex.Match(onDefault.MessageShort, "Merge branch '([^']*)'");
                            if (m.Success)
                            {
                                string branchName = m.Groups[m.Groups.Count - 1].Value;
                                if (branchName.StartsWith("origin/"))
                                    branchName = branchName.Substring(7);
                                return branchName;
                            }
                            else
                            {
                                log.Warning("Unable to determine old branch name. The branch has probably been deleted.");
                                return "DELETED";
                            }
                        }
                        commitOnBranch = commitOnBranch.Parents.First();
                    }
                }
            }
 
            // is the commit on a branch that has not yet been merged to the default branch?
            Stopwatch timer = Stopwatch.StartNew();
            List<Branch> candidates = new List<Branch>();
            foreach (var branch in repo.Branches)
            {
                Commit commitOnBranch = branch.Tip;
                while (!shasOnDefault.Contains(commitOnBranch.Sha))
                {
                    if (commitOnBranch.Sha == commit.Sha)
                    {
                        candidates.Add(branch);
                        break;
                    }
                    if (!commitOnBranch.Parents.Any())
                        break;
                    commitOnBranch = commitOnBranch.Parents.First();
                }
            }
            TimeSpan dur = timer.Elapsed;
            if (!candidates.Any())
            {
                log.Warning("Unable to determine branch name.");
                return "ERROR";
            }
            if (candidates.Count == 1)
                return candidates.First().GetShortName();
 
            if(candidates.Select(b => b.GetShortName()).Distinct().Count() == 1) // all candicates have the same name (e.g. on is a local branch and one is the remote of that same branch)
                return candidates.First().GetShortName();
 
            log.Warning("Several possible branch names found. Picking one.");
 
            // pick the first candidate that is has a merge commit as the child of commit
            try
            {
                for (int b = 0; b < candidates.Count; b++)
                {
                    var commitsOnB = repo.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = candidates[b], ExcludeReachableFrom = commit, FirstParentOnly = true });
                    if (commitsOnB.Last().Parents.Count() == 2 && commitsOnB.Last().Parents.First() == commit)
                        return candidates[b].GetShortName();
                }
            }
            catch
            {
 
            }
 
            // TODO: we need some better logic to pick the right candidate here
            //var selected = candidates.Select(b => (b, branchCountFromDefault(b, shasOnDefault))).ToList(); 
            return candidates.First().GetShortName();
        }      
    }
 
    internal static class libGit2Helpers
    {
        public static string GetShortName(this Branch b)
        {
            if (b.FriendlyName.StartsWith("origin/"))
                return b.FriendlyName.Substring(7);
            return b.FriendlyName;
        }
    }
}