diff --git a/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs b/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs index 203085f3d0..8e06d19e9a 100644 --- a/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs +++ b/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs @@ -6,6 +6,7 @@ using GitVersionCore.Tests; using LibGit2Sharp; using NUnit.Framework; +using System.Collections.Generic; public class MainlineDevelopmentMode { @@ -206,7 +207,7 @@ public void VerifyMergingMasterToFeatureDoesNotCauseBranchCommitsToIncrementVers fixture.BranchTo("feature/foo", "foo"); fixture.MakeACommit("first in foo"); - + fixture.Checkout("master"); fixture.MakeACommit("second in master"); @@ -303,6 +304,41 @@ public void VerifyMergingMasterIntoAFeatureBranchWorksWithMultipleBranches() fixture.AssertFullSemver(config, "1.0.2"); } } + + [Test] + public void MergingFeatureBranchThatIncrementsMinorNumberIncrementsMinorVersionOfMaster() + { + var currentConfig = new Config + { + VersioningMode = VersioningMode.Mainline, + Branches = new Dictionary + { + { + "feature", new BranchConfig + { + VersioningMode = VersioningMode.ContinuousDeployment, + Increment = IncrementStrategy.Minor + } + } + } + }; + + using (var fixture = new EmptyRepositoryFixture()) + { + fixture.MakeACommit("first in master"); + fixture.MakeATaggedCommit("1.0.0"); + fixture.AssertFullSemver(currentConfig, "1.0.0"); + + fixture.BranchTo("feature/foo", "foo"); + fixture.MakeACommit("first in foo"); + fixture.MakeACommit("second in foo"); + fixture.AssertFullSemver(currentConfig, "1.1.0-foo.2"); + + fixture.Checkout("master"); + fixture.MergeNoFF("feature/foo"); + fixture.AssertFullSemver(currentConfig, "1.1.0"); + } + } } static class CommitExtensions diff --git a/src/GitVersionCore.Tests/VersionCalculation/Strategies/MergeMessageBaseVersionStrategyTests.cs b/src/GitVersionCore.Tests/VersionCalculation/Strategies/MergeMessageBaseVersionStrategyTests.cs index 8bab66b5c1..6890b1080f 100644 --- a/src/GitVersionCore.Tests/VersionCalculation/Strategies/MergeMessageBaseVersionStrategyTests.cs +++ b/src/GitVersionCore.Tests/VersionCalculation/Strategies/MergeMessageBaseVersionStrategyTests.cs @@ -82,14 +82,12 @@ Another commit message Commit message including a IP-number https://10.50.1.1 A commit message")] [TestCase(@"Merge branch 'release/Sprint_2.0_Holdings_Computed_Balances'")] - [TestCase(@"Merge branch 'feature/fix-for-08.14-push'")] [TestCase(@"Merge branch 'develop' of http://10.0.6.3/gitblit/r/... into develop")] [TestCase(@"Merge branch 'master' of http://172.16.3.10:8082/r/asu_tk/p_sd")] [TestCase(@"Merge branch 'master' of http://212.248.89.56:8082/r/asu_tk/p_sd")] [TestCase(@"Merge branch 'DEMO' of http://10.10.10.121/gitlab/mtolland/orcid into DEMO")] public void MergeMessagesThatIsNotRelatedToGitVersion(string commitMessage) { - var parents = GetParents(true); AssertMergeMessage(commitMessage, null, parents); diff --git a/src/GitVersionCore/BranchConfigurationCalculator.cs b/src/GitVersionCore/BranchConfigurationCalculator.cs index 97154e88f1..c7ac864c1d 100644 --- a/src/GitVersionCore/BranchConfigurationCalculator.cs +++ b/src/GitVersionCore/BranchConfigurationCalculator.cs @@ -13,50 +13,21 @@ public class BranchConfigurationCalculator /// public static BranchConfig GetBranchConfiguration(GitVersionContext context, Branch targetBranch, IList excludedInheritBranches = null) { - var matchingBranches = LookupBranchConfiguration(context.FullConfiguration, targetBranch).ToArray(); - - BranchConfig branchConfiguration; - if (matchingBranches.Length > 0) - { - branchConfiguration = matchingBranches[0]; - - if (matchingBranches.Length > 1) - { - Logger.WriteWarning(string.Format( - "Multiple branch configurations match the current branch branchName of '{0}'. Using the first matching configuration, '{1}'. Matching configurations include: '{2}'", - targetBranch.FriendlyName, - branchConfiguration.Name, - string.Join("', '", matchingBranches.Select(b => b.Name)))); - } - } - else + var matchingBranches = context.FullConfiguration.GetConfigForBranch(targetBranch.FriendlyName); + + if (matchingBranches == null) { Logger.WriteInfo(string.Format( "No branch configuration found for branch {0}, falling back to default configuration", targetBranch.FriendlyName)); - branchConfiguration = new BranchConfig { Name = string.Empty }; - ConfigurationProvider.ApplyBranchDefaults(context.FullConfiguration, branchConfiguration, ""); - } - - return branchConfiguration.Increment == IncrementStrategy.Inherit ? - InheritBranchConfiguration(context, targetBranch, branchConfiguration, excludedInheritBranches) : - branchConfiguration; - } - - static IEnumerable LookupBranchConfiguration(Config config, Branch currentBranch) - { - if (config == null) - { - throw new ArgumentNullException(nameof(config)); - } - - if (currentBranch == null) - { - throw new ArgumentNullException(nameof(currentBranch)); + matchingBranches = new BranchConfig { Name = string.Empty }; + ConfigurationProvider.ApplyBranchDefaults(context.FullConfiguration, matchingBranches, ""); } - return config.Branches.Where(b => Regex.IsMatch(currentBranch.FriendlyName, "^" + b.Value.Regex, RegexOptions.IgnoreCase)).Select(kvp => kvp.Value); + return matchingBranches.Increment == IncrementStrategy.Inherit ? + InheritBranchConfiguration(context, targetBranch, matchingBranches, excludedInheritBranches) : + matchingBranches; } static BranchConfig InheritBranchConfiguration(GitVersionContext context, Branch targetBranch, BranchConfig branchConfiguration, IList excludedInheritBranches) @@ -77,11 +48,9 @@ static BranchConfig InheritBranchConfiguration(GitVersionContext context, Branch { excludedInheritBranches = repository.Branches.Where(b => { - var branchConfig = LookupBranchConfiguration(config, b).ToArray(); + var branchConfig = config.GetConfigForBranch(b.FriendlyName); - // NOTE: if length is 0 we couldn't find the configuration for the branch e.g. "origin/master" - // NOTE: if the length is greater than 1 we cannot decide which merge strategy to pick - return (branchConfig.Length != 1) || (branchConfig.Length == 1 && branchConfig[0].Increment == IncrementStrategy.Inherit); + return branchConfig != null && branchConfig.Increment == IncrementStrategy.Inherit; }).ToList(); } // Add new excluded branches. diff --git a/src/GitVersionCore/Configuration/Config.cs b/src/GitVersionCore/Configuration/Config.cs index 8cd94e083c..a5d6dcecf8 100644 --- a/src/GitVersionCore/Configuration/Config.cs +++ b/src/GitVersionCore/Configuration/Config.cs @@ -1,8 +1,10 @@ namespace GitVersion { + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; + using System.Text.RegularExpressions; using YamlDotNet.Serialization; public class Config @@ -89,6 +91,33 @@ public Dictionary Branches } } + public BranchConfig GetConfigForBranch(string branchName) + { + if (branchName == null) throw new ArgumentNullException(nameof(branchName)); + var matches = Branches + .Where(b => Regex.IsMatch(branchName, "^" + b.Value.Regex, RegexOptions.IgnoreCase)); + + try + { + return matches + .Select(kvp => kvp.Value) + .SingleOrDefault(); + } + catch (InvalidOperationException) + { + var matchingConfigs = string.Join("\n - ", matches.Select(m => m.Key)); + var picked = matches + .Select(kvp => kvp.Value) + .First(); + + Logger.WriteWarning( + $"Multiple branch configurations match the current branch branchName of '{branchName}'. " + + $"Using the first matching configuration, '{picked}'. Matching configurations include: '{matchingConfigs}'"); + + return picked; + } + } + T MergeObjects(T target, T source) { typeof(T).GetProperties() diff --git a/src/GitVersionCore/GitVersionCore.csproj b/src/GitVersionCore/GitVersionCore.csproj index 906bd1abef..fea4f65af9 100644 --- a/src/GitVersionCore/GitVersionCore.csproj +++ b/src/GitVersionCore/GitVersionCore.csproj @@ -128,10 +128,12 @@ + + diff --git a/src/GitVersionCore/MergeMessage.cs b/src/GitVersionCore/MergeMessage.cs new file mode 100644 index 0000000000..1dd5ecc3ac --- /dev/null +++ b/src/GitVersionCore/MergeMessage.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace GitVersion +{ + class MergeMessage + { + static Regex parseMergeMessage = new Regex( + @"^Merge (branch|tag) '(?[^']*)'", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + static Regex parseGitHubPullMergeMessage = new Regex( + @"^Merge pull request #(?\d*) (from|in) (?.*)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + static Regex smartGitMergeMessage = new Regex( + @"^Finish (?.*)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private string mergeMessage; + private Config config; + + public MergeMessage(string mergeMessage, Config config) + { + this.mergeMessage = mergeMessage; + this.config = config; + + var lastIndexOf = mergeMessage.LastIndexOf("into", StringComparison.OrdinalIgnoreCase); + if (lastIndexOf != -1) + { + // If we have into in the merge message the rest should be the target branch + TargetBranch = mergeMessage.Substring(lastIndexOf + 5); + } + + MergedBranch = ParseBranch(); + + // Remove remotes and branch prefixes like release/ feature/ hotfix/ etc + var toMatch = Regex.Replace(MergedBranch, @"^(\w+[-/])*", "", RegexOptions.IgnoreCase); + toMatch = Regex.Replace(toMatch, $"^{config.TagPrefix}", ""); + // We don't match if the version is likely an ip (i.e starts with http://) + var versionMatch = new Regex(@"^(? @@ -21,7 +19,7 @@ public override IEnumerable GetVersions(GitVersionContext context) .SelectMany(c => { SemanticVersion semanticVersion; - if (TryParse(c, context.Configuration, out semanticVersion)) + if (TryParse(c, context, out semanticVersion)) { var shouldIncrement = !context.Configuration.PreventIncrementForMergedBranchVersion; return new[] @@ -34,35 +32,21 @@ public override IEnumerable GetVersions(GitVersionContext context) return baseVersions; } - static bool TryParse(Commit mergeCommit, EffectiveConfiguration configuration, out SemanticVersion semanticVersion) + static bool TryParse(Commit mergeCommit, GitVersionContext context, out SemanticVersion semanticVersion) { - semanticVersion = Inner(mergeCommit, configuration); + semanticVersion = Inner(mergeCommit, context); return semanticVersion != null; } - static SemanticVersion Inner(Commit mergeCommit, EffectiveConfiguration configuration) + static SemanticVersion Inner(Commit mergeCommit, GitVersionContext context) { if (mergeCommit.Parents.Count() < 2) { return null; } - var commitMessage = mergeCommit.Message; - var lastIndexOf = commitMessage.LastIndexOf("into", StringComparison.OrdinalIgnoreCase); - if (lastIndexOf != -1) - commitMessage = commitMessage.Substring(0, lastIndexOf); - - //TODO: Make the version prefixes customizable - var possibleVersions = Regex.Matches(commitMessage, @"^.*?(([rR]elease|[hH]otfix|[aA]lpha)-|-v|/|/v|'|Finish )(?(?() - .Select(m => m.Groups["PossibleVersions"].Value); - - return possibleVersions - .Select(part => - { - SemanticVersion v; - return SemanticVersion.TryParse(part, configuration.GitTagPrefix, out v) ? v : null; - }).FirstOrDefault(v => v != null); + var mergeMessage = new MergeMessage(mergeCommit.Message, context.FullConfiguration); + return mergeMessage.Version; } } } \ No newline at end of file diff --git a/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs b/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs new file mode 100644 index 0000000000..4d27c77d79 --- /dev/null +++ b/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs @@ -0,0 +1,225 @@ +using GitVersion.VersionCalculation.BaseVersionCalculators; +using LibGit2Sharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace GitVersion.VersionCalculation +{ + class MainlineVersionCalculator + { + IMetaDataCalculator metaDataCalculator; + + public MainlineVersionCalculator(IMetaDataCalculator metaDataCalculator) + { + this.metaDataCalculator = metaDataCalculator; + } + + public SemanticVersion FindMainlineModeVersion(BaseVersion baseVersion, GitVersionContext context) + { + if (baseVersion.SemanticVersion.PreReleaseTag.HasTag()) + { + throw new NotSupportedException("Mainline development mode doesn't yet support pre-release tags on master"); + } + + using (Logger.IndentLog("Using mainline development mode to calculate current version")) + { + var mainlineVersion = baseVersion.SemanticVersion; + + // Forward merge / PR + // * feature/foo + // / | + // master * * + // + + var mainlineTip = GetMainlineTip(context); + var commitsNotOnMainline = context.Repository.Commits.QueryBy(new CommitFilter + { + IncludeReachableFrom = context.CurrentBranch, + ExcludeReachableFrom = mainlineTip, + SortBy = CommitSortStrategies.Reverse, + FirstParentOnly = true + }).Where(c => c.Sha != baseVersion.BaseVersionSource.Sha && c.Parents.Count() == 1).ToList(); + var commitLog = context.Repository.Commits.QueryBy(new CommitFilter + { + IncludeReachableFrom = context.CurrentBranch, + ExcludeReachableFrom = baseVersion.BaseVersionSource, + SortBy = CommitSortStrategies.Reverse, + FirstParentOnly = true + }) + .Where(c => c.Sha != baseVersion.BaseVersionSource.Sha) + .Except(commitsNotOnMainline) + .ToList(); + + var directCommits = new List(); + + // Scans commit log in reverse, aggregating merge commits + foreach (var commit in commitLog) + { + directCommits.Add(commit); + if (commit.Parents.Count() > 1) + { + mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion); + } + } + + if (context.CurrentBranch.FriendlyName != "master") + { + var mergedHead = context.CurrentCommit; + var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTip); + Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, findMergeBase)); + + var branchIncrement = FindMessageIncrement(context, null, mergedHead, findMergeBase, directCommits); + // This will increment for any direct commits on master + mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); + mainlineVersion.BuildMetaData = metaDataCalculator.Create(findMergeBase, context); + // Don't increment if the merge commit is a merge into mainline + // this ensures PR's and forward merges end up correct. + if (mergedHead.Parents.Count() == 1 || mergedHead.Parents.First() != mainlineTip) + { + Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement)); + mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement); + } + } + else + { + // If we are on master, make sure no commits get left behind + mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); + mainlineVersion.BuildMetaData = metaDataCalculator.Create(baseVersion.BaseVersionSource, context); + } + + return mainlineVersion; + } + } + + SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List directCommits, SemanticVersion mainlineVersion) + { + // Merge commit, process all merged commits as a batch + var mergeCommit = commit; + var mergedHead = GetMergedHead(mergeCommit); + var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(mergeCommit.Parents.First(), mergedHead); + var findMessageIncrement = FindMessageIncrement(context, mergeCommit, mergedHead, findMergeBase, directCommits); + + // If this collection is not empty there has been some direct commits against master + // Treat each commit as it's own 'release', we need to do this before we increment the branch + mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); + directCommits.Clear(); + + // Finally increment for the branch + mainlineVersion = mainlineVersion.IncrementVersion(findMessageIncrement); + Logger.WriteInfo(string.Format("Merge commit {0} incremented base versions {1}, now {2}", + mergeCommit.Sha, findMessageIncrement, mainlineVersion)); + return mainlineVersion; + } + + static Commit GetMainlineTip(GitVersionContext context) + { + var mainlineBranchConfigs = context.FullConfiguration.Branches.Where(b => b.Value.IsMainline == true).ToList(); + var seenMainlineTips = new List(); + var mainlineBranches = context.Repository.Branches + .Where(b => + { + return mainlineBranchConfigs.Any(c => Regex.IsMatch(b.FriendlyName, c.Key)); + }) + .Where(b => + { + if (seenMainlineTips.Contains(b.Tip.Sha)) + { + Logger.WriteInfo("Multiple possible mainlines pointing at the same commit, dropping " + b.FriendlyName); + return false; + } + seenMainlineTips.Add(b.Tip.Sha); + return true; + }) + .Select(b => new + { + MergeBase = context.Repository.ObjectDatabase.FindMergeBase(b.Tip, context.CurrentCommit), + Branch = b + }) + .Where(a => a.MergeBase != null) + .GroupBy(b => b.MergeBase.Sha, b => b.Branch) + .ToDictionary(b => b.Key, b => b.ToList()); + + var allMainlines = mainlineBranches.Values.SelectMany(branches => branches.Select(b => b.FriendlyName)); + Logger.WriteInfo("Found possible mainline branches: " + string.Join(", ", allMainlines)); + + // Find closest mainline branch + var firstMatchingCommit = context.CurrentBranch.Commits.First(c => mainlineBranches.ContainsKey(c.Sha)); + var possibleMainlineBranches = mainlineBranches[firstMatchingCommit.Sha]; + + if (possibleMainlineBranches.Count == 1) + { + var mainlineBranch = possibleMainlineBranches[0]; + Logger.WriteInfo("Mainline for current branch is " + mainlineBranch.FriendlyName); + return mainlineBranch.Tip; + } + + var chosenMainline = possibleMainlineBranches[0]; + Logger.WriteInfo(string.Format( + "Multiple mainlines ({0}) have the same merge base for the current branch, choosing {1} because we found that branch first...", + string.Join(", ", possibleMainlineBranches.Select(b => b.FriendlyName)), + chosenMainline.FriendlyName)); + return chosenMainline.Tip; + } + + private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List directCommits, SemanticVersion mainlineVersion) + { + foreach (var directCommit in directCommits) + { + var directCommitIncrement = IncrementStrategyFinder.GetIncrementForCommits(context, new[] + { + directCommit + }) ?? VersionField.Patch; + mainlineVersion = mainlineVersion.IncrementVersion(directCommitIncrement); + Logger.WriteInfo(string.Format("Direct commit on master {0} incremented base versions {1}, now {2}", + directCommit.Sha, directCommitIncrement, mainlineVersion)); + } + return mainlineVersion; + } + + private static VersionField FindMessageIncrement( + GitVersionContext context, Commit mergeCommit, Commit mergedHead, Commit findMergeBase, List commitLog) + { + var filter = new CommitFilter + { + IncludeReachableFrom = mergedHead, + ExcludeReachableFrom = findMergeBase + }; + var commits = mergeCommit == null ? + context.Repository.Commits.QueryBy(filter).ToList() : + new[] { mergeCommit }.Union(context.Repository.Commits.QueryBy(filter)).ToList(); + commitLog.RemoveAll(c => commits.Any(c1 => c1.Sha == c.Sha)); + return IncrementStrategyFinder.GetIncrementForCommits(context, commits) + ?? TryFindIncrementFromMergeMessage(mergeCommit, context); + } + + private static VersionField TryFindIncrementFromMergeMessage(Commit mergeCommit, GitVersionContext context) + { + if (mergeCommit != null) + { + var mergeMessage = new MergeMessage(mergeCommit.Message, context.FullConfiguration); + if (mergeMessage.MergedBranch != null) + { + var config = context.FullConfiguration.GetConfigForBranch(mergeMessage.MergedBranch); + if (config != null && config.Increment.HasValue && config.Increment != IncrementStrategy.Inherit) + { + return config.Increment.Value.ToVersionField(); + } + } + } + + // Fallback to patch + return VersionField.Patch; + } + + private Commit GetMergedHead(Commit mergeCommit) + { + var parents = mergeCommit.Parents.Skip(1).ToList(); + if (parents.Count > 1) + throw new NotSupportedException("Mainline development does not support more than one merge source in a single commit yet"); + return parents.Single(); + } + } +} diff --git a/src/GitVersionCore/VersionCalculation/NextVersionCalculator.cs b/src/GitVersionCore/VersionCalculation/NextVersionCalculator.cs index 771a7e4743..3cb2a5e145 100644 --- a/src/GitVersionCore/VersionCalculation/NextVersionCalculator.cs +++ b/src/GitVersionCore/VersionCalculation/NextVersionCalculator.cs @@ -1,11 +1,8 @@ namespace GitVersion.VersionCalculation { - using System; - using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using BaseVersionCalculators; - using LibGit2Sharp; public class NextVersionCalculator { @@ -45,7 +42,10 @@ public SemanticVersion FindVersion(GitVersionContext context) var baseVersion = baseVersionFinder.GetBaseVersion(context); SemanticVersion semver; if (context.Configuration.VersioningMode == VersioningMode.Mainline) - semver = FindMainlineModeVersion(baseVersion, context); + { + var mainlineMode = new MainlineVersionCalculator(metaDataCalculator); + semver = mainlineMode.FindMainlineModeVersion(baseVersion, context); + } else { semver = PerformIncrement(context, baseVersion); @@ -82,191 +82,6 @@ private static SemanticVersion PerformIncrement(GitVersionContext context, BaseV return semver; } - SemanticVersion FindMainlineModeVersion(BaseVersion baseVersion, GitVersionContext context) - { - if (baseVersion.SemanticVersion.PreReleaseTag.HasTag()) - { - throw new NotSupportedException("Mainline development mode doesn't yet support pre-release tags on master"); - } - - using (Logger.IndentLog("Using mainline development mode to calculate current version")) - { - var mainlineVersion = baseVersion.SemanticVersion; - - // Forward merge / PR - // * feature/foo - // / | - // master * * - // - - var mainlineTip = GetMainlineTip(context); - var commitsNotOnMainline = context.Repository.Commits.QueryBy(new CommitFilter - { - IncludeReachableFrom = context.CurrentBranch, - ExcludeReachableFrom = mainlineTip, - SortBy = CommitSortStrategies.Reverse, - FirstParentOnly = true - }).Where(c => c.Sha != baseVersion.BaseVersionSource.Sha && c.Parents.Count() == 1).ToList(); - var commitLog = context.Repository.Commits.QueryBy(new CommitFilter - { - IncludeReachableFrom = context.CurrentBranch, - ExcludeReachableFrom = baseVersion.BaseVersionSource, - SortBy = CommitSortStrategies.Reverse, - FirstParentOnly = true - }) - .Where(c => c.Sha != baseVersion.BaseVersionSource.Sha) - .Except(commitsNotOnMainline) - .ToList(); - - var directCommits = new List(); - - // Scans commit log in reverse, aggregating merge commits - foreach (var commit in commitLog) - { - directCommits.Add(commit); - if (commit.Parents.Count() > 1) - { - mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion); - } - } - - if (context.CurrentBranch.FriendlyName != "master") - { - var mergedHead = context.CurrentCommit; - var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTip); - Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, findMergeBase)); - - var branchIncrement = FindMessageIncrement(context, null, mergedHead, findMergeBase, directCommits); - // This will increment for any direct commits on master - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); - mainlineVersion.BuildMetaData = metaDataCalculator.Create(findMergeBase, context); - // Don't increment if the merge commit is a merge into mainline - // this ensures PR's and forward merges end up correct. - if (mergedHead.Parents.Count() == 1 || mergedHead.Parents.First() != mainlineTip) - { - Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement)); - mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement); - } - } - else - { - // If we are on master, make sure no commits get left behind - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); - mainlineVersion.BuildMetaData = metaDataCalculator.Create(baseVersion.BaseVersionSource, context); - } - - return mainlineVersion; - } - } - - SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List directCommits, SemanticVersion mainlineVersion) - { - // Merge commit, process all merged commits as a batch - var mergeCommit = commit; - var mergedHead = GetMergedHead(mergeCommit); - var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(mergeCommit.Parents.First(), mergedHead); - var findMessageIncrement = FindMessageIncrement(context, mergeCommit, mergedHead, findMergeBase, directCommits); - - // If this collection is not empty there has been some direct commits against master - // Treat each commit as it's own 'release', we need to do this before we increment the branch - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); - directCommits.Clear(); - - // Finally increment for the branch - mainlineVersion = mainlineVersion.IncrementVersion(findMessageIncrement); - Logger.WriteInfo(string.Format("Merge commit {0} incremented base versions {1}, now {2}", - mergeCommit.Sha, findMessageIncrement, mainlineVersion)); - return mainlineVersion; - } - - static Commit GetMainlineTip(GitVersionContext context) - { - var mainlineBranchConfigs = context.FullConfiguration.Branches.Where(b => b.Value.IsMainline == true).ToList(); - var seenMainlineTips = new List(); - var mainlineBranches = context.Repository.Branches - .Where(b => - { - return mainlineBranchConfigs.Any(c => Regex.IsMatch(b.FriendlyName, c.Key)); - }) - .Where(b => - { - if (seenMainlineTips.Contains(b.Tip.Sha)) - { - Logger.WriteInfo("Multiple possible mainlines pointing at the same commit, dropping " + b.FriendlyName); - return false; - } - seenMainlineTips.Add(b.Tip.Sha); - return true; - }) - .Select(b => new - { - MergeBase = context.Repository.ObjectDatabase.FindMergeBase(b.Tip, context.CurrentCommit), - Branch = b - }) - .Where(a => a.MergeBase != null) - .GroupBy(b => b.MergeBase.Sha, b => b.Branch) - .ToDictionary(b => b.Key, b => b.ToList()); - - var allMainlines = mainlineBranches.Values.SelectMany(branches => branches.Select(b => b.FriendlyName)); - Logger.WriteInfo("Found possible mainline branches: " + string.Join(", ", allMainlines)); - - // Find closest mainline branch - var firstMatchingCommit = context.CurrentBranch.Commits.First(c => mainlineBranches.ContainsKey(c.Sha)); - var possibleMainlineBranches = mainlineBranches[firstMatchingCommit.Sha]; - - if (possibleMainlineBranches.Count == 1) - { - var mainlineBranch = possibleMainlineBranches[0]; - Logger.WriteInfo("Mainline for current branch is " + mainlineBranch.FriendlyName); - return mainlineBranch.Tip; - } - - var chosenMainline = possibleMainlineBranches[0]; - Logger.WriteInfo(string.Format( - "Multiple mainlines ({0}) have the same merge base for the current branch, choosing {1} because we found that branch first...", - string.Join(", ", possibleMainlineBranches.Select(b => b.FriendlyName)), - chosenMainline.FriendlyName)); - return chosenMainline.Tip; - } - - private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List directCommits, SemanticVersion mainlineVersion) - { - foreach (var directCommit in directCommits) - { - var directCommitIncrement = IncrementStrategyFinder.GetIncrementForCommits(context, new[] - { - directCommit - }) ?? VersionField.Patch; - mainlineVersion = mainlineVersion.IncrementVersion(directCommitIncrement); - Logger.WriteInfo(string.Format("Direct commit on master {0} incremented base versions {1}, now {2}", - directCommit.Sha, directCommitIncrement, mainlineVersion)); - } - return mainlineVersion; - } - - private static VersionField FindMessageIncrement( - GitVersionContext context, Commit mergeCommit, Commit mergedHead, Commit findMergeBase, List commitLog) - { - var filter = new CommitFilter - { - IncludeReachableFrom = mergedHead, - ExcludeReachableFrom = findMergeBase - }; - var commits = mergeCommit == null ? - context.Repository.Commits.QueryBy(filter).ToList() : - new[] { mergeCommit }.Union(context.Repository.Commits.QueryBy(filter)).ToList(); - commitLog.RemoveAll(c => commits.Any(c1 => c1.Sha == c.Sha)); - return IncrementStrategyFinder.GetIncrementForCommits(context, commits) ?? VersionField.Patch; - } - - private Commit GetMergedHead(Commit mergeCommit) - { - var parents = mergeCommit.Parents.Skip(1).ToList(); - if (parents.Count > 1) - throw new NotSupportedException("Mainline development does not support more than one merge source in a single commit yet"); - return parents.Single(); - } - void UpdatePreReleaseTag(GitVersionContext context, SemanticVersion semanticVersion, string branchNameOverride) { var tagToUse = GetBranchSpecificTag(context.Configuration, context.CurrentBranch.FriendlyName, branchNameOverride);