diff --git a/src/GitVersionCore.Tests/MergeMessageTests.cs b/src/GitVersionCore.Tests/MergeMessageTests.cs index 42d9ca6966..4defaec033 100644 --- a/src/GitVersionCore.Tests/MergeMessageTests.cs +++ b/src/GitVersionCore.Tests/MergeMessageTests.cs @@ -54,15 +54,14 @@ public void EmptyTagPrefix(string prefix) private static readonly object[] MergeMessages = { - new object[] { "Merge branch 'feature/one'", "feature/one", null, null }, - new object[] { "Merge branch 'origin/feature/one'", "origin/feature/one", null, null }, - new object[] { "Merge tag 'v4.0.0' into master", "v4.0.0", "master", new SemanticVersion(4) }, - new object[] { "Merge tag 'V4.0.0' into master", "V4.0.0", "master", new SemanticVersion(4) }, - new object[] { "Merge branch 'feature/4.1/one'", "feature/4.1/one", null, new SemanticVersion(4, 1) }, - new object[] { "Merge branch 'origin/4.1/feature/one'", "origin/4.1/feature/one", null, new SemanticVersion(4, 1) }, - new object[] { "Merge tag 'v://10.10.10.10' into master", "v://10.10.10.10", "master", null } - - }; + new object[] { "Merge branch 'feature/one'", "feature/one", null, null }, + new object[] { "Merge branch 'origin/feature/one'", "origin/feature/one", null, null }, + new object[] { "Merge tag 'v4.0.0' into master", "v4.0.0", "master", new SemanticVersion(4) }, + new object[] { "Merge tag 'V4.0.0' into master", "V4.0.0", "master", new SemanticVersion(4) }, + new object[] { "Merge branch 'feature/4.1/one'", "feature/4.1/one", null, new SemanticVersion(4, 1) }, + new object[] { "Merge branch 'origin/4.1/feature/one'", "origin/4.1/feature/one", null, new SemanticVersion(4, 1) }, + new object[] { "Merge tag 'v://10.10.10.10' into master", "v://10.10.10.10", "master", null } + }; [TestCaseSource(nameof(MergeMessages))] public void ParsesMergeMessage( @@ -75,6 +74,7 @@ public void ParsesMergeMessage( var sut = new MergeMessage(message, _config); // Assert + sut.MatchDefinition.ShouldBe("Default"); sut.TargetBranch.ShouldBe(expectedTargetBranch); sut.MergedBranch.ShouldBe(expectedMergedBranch); sut.IsMergedPullRequest.ShouldBeFalse(); @@ -84,21 +84,14 @@ public void ParsesMergeMessage( private static readonly object[] GitHubPullPullMergeMessages = { - new object[] { "Merge pull request #1234 from feature/one", "feature/one", null, null, 1234 }, - new object[] { "Merge pull request #1234 in feature/one", "feature/one", null, null, 1234 }, - new object[] { "Merge pull request #1234 in v4.0.0", "v4.0.0", null, new SemanticVersion(4), 1234 }, - new object[] { "Merge pull request #1234 in V4.0.0", "V4.0.0", null, new SemanticVersion(4), 1234 }, - new object[] { "Merge pull request #1234 from origin/feature/one", "origin/feature/one", null, null, 1234 }, - new object[] { "Merge pull request #1234 in feature/4.1/one", "feature/4.1/one", null, new SemanticVersion(4,1), 1234 }, - new object[] { "Merge pull request #1234 in V://10.10.10.10", "V://10.10.10.10", null, null, 1234 }, - - - //TODO: Investigate successful github merge messages that may be invalid - // Should an empty PR number be valid? - new object[] { "Merge pull request # from feature/one", "feature/one", null, null, 0 }, - // The branch name appears to be incorrect - new object[] { "Merge pull request #1234 from feature/one into dev", "feature/one into dev", "dev", null, 1234 }, - }; + new object[] { "Merge pull request #1234 from feature/one", "feature/one", null, null, 1234 }, + new object[] { "Merge pull request #1234 in feature/one", "feature/one", null, null, 1234 }, + new object[] { "Merge pull request #1234 in v4.0.0", "v4.0.0", null, new SemanticVersion(4), 1234 }, + new object[] { "Merge pull request #1234 from origin/feature/one", "origin/feature/one", null, null, 1234 }, + new object[] { "Merge pull request #1234 in feature/4.1/one", "feature/4.1/one", null, new SemanticVersion(4,1), 1234 }, + new object[] { "Merge pull request #1234 in V://10.10.10.10", "V://10.10.10.10", null, null, 1234 }, + new object[] { "Merge pull request #1234 from feature/one into dev", "feature/one", "dev", null, 1234 } + }; [TestCaseSource(nameof(GitHubPullPullMergeMessages))] public void ParsesGitHubPullMergeMessage( @@ -112,6 +105,7 @@ public void ParsesGitHubPullMergeMessage( var sut = new MergeMessage(message, _config); // Assert + sut.MatchDefinition.ShouldBe("GitHubPull"); sut.TargetBranch.ShouldBe(expectedTargetBranch); sut.MergedBranch.ShouldBe(expectedMergedBranch); sut.IsMergedPullRequest.ShouldBeTrue(); @@ -121,23 +115,17 @@ public void ParsesGitHubPullMergeMessage( private static readonly object[] BitBucketPullMergeMessages = { - new object[] { "Merge pull request #1234 from feature/one from feature/two to dev", "feature/two", null, null, 1234 }, - new object[] { "Merge pull request #1234 in feature/one from feature/two to dev", "feature/two", null, null, 1234 }, - new object[] { "Merge pull request #1234 in v4.0.0 from v4.1.0 to dev", "v4.1.0", null, new SemanticVersion(4,1), 1234 }, - new object[] { "Merge pull request #1234 in V4.0.0 from V4.1.0 to dev", "V4.1.0", null, new SemanticVersion(4,1), 1234 }, - new object[] { "Merge pull request #1234 from origin/feature/one from origin/feature/4.2/two to dev", "origin/feature/4.2/two", null, new SemanticVersion(4,2), 1234 }, - new object[] { "Merge pull request #1234 in feature/4.1/one from feature/4.2/two to dev", "feature/4.2/two", null, new SemanticVersion(4,2), 1234 }, - new object[] { "Merge pull request #1234 in feature/4.1/one from feature/4.2/two to dev into master", "feature/4.2/two", "master", new SemanticVersion(4,2), 1234 }, - new object[] { "Merge pull request #1234 in V4.1.0 from V://10.10.10.10 to dev", "V://10.10.10.10", null, null, 1234 }, - //TODO: Investigate successful bitbucket merge messages that may be invalid - // Regex has double 'from/in from' section. Is that correct? - new object[] { "Merge pull request #1234 in feature/4.1/one from feature/4.2/two to dev", "feature/4.2/two", null, new SemanticVersion(4,2), 1234 }, - new object[] { "Merge pull request #1234 from feature/one from v4.0.0 to master", "v4.0.0", null, new SemanticVersion(4), 1234 }, - // target branch is not resolved from targetbranch group - new object[] { "Merge pull request #1234 from feature/one from feature/two to master" , "feature/two", null, null, 1234 }, - // Should an empty PR number be valid? - new object[] { "Merge pull request # in feature/one from feature/two to master" , "feature/two", null, null, 0 } - }; + new object[] { "Merge pull request #1234 from feature/one from feature/two to dev", "feature/two", "dev", null, 1234 }, + new object[] { "Merge pull request #1234 in feature/one from feature/two to dev", "feature/two", "dev", null, 1234 }, + new object[] { "Merge pull request #1234 in v4.0.0 from v4.1.0 to dev", "v4.1.0", "dev", new SemanticVersion(4,1), 1234 }, + new object[] { "Merge pull request #1234 from origin/feature/one from origin/feature/4.2/two to dev", "origin/feature/4.2/two", "dev", new SemanticVersion(4,2), 1234 }, + new object[] { "Merge pull request #1234 in feature/4.1/one from feature/4.2/two to dev", "feature/4.2/two", "dev", new SemanticVersion(4,2), 1234 }, + new object[] { "Merge pull request #1234 from feature/one from feature/two to master" , "feature/two", "master", null, 1234 }, + new object[] { "Merge pull request #1234 in V4.1.0 from V://10.10.10.10 to dev", "V://10.10.10.10", "dev", null, 1234 }, + //TODO: Investigate successful bitbucket merge messages that may be invalid + // Regex has double 'from/in from' section. Is that correct? + new object[] { "Merge pull request #1234 from feature/one from v4.0.0 to master", "v4.0.0", "master", new SemanticVersion(4), 1234 } + }; [TestCaseSource(nameof(BitBucketPullMergeMessages))] public void ParsesBitBucketPullMergeMessage( @@ -151,6 +139,7 @@ public void ParsesBitBucketPullMergeMessage( var sut = new MergeMessage(message, _config); // Assert + sut.MatchDefinition.ShouldBe("BitBucketPull"); sut.TargetBranch.ShouldBe(expectedTargetBranch); sut.MergedBranch.ShouldBe(expectedMergedBranch); sut.IsMergedPullRequest.ShouldBeTrue(); @@ -158,19 +147,17 @@ public void ParsesBitBucketPullMergeMessage( sut.Version.ShouldBe(expectedVersion); } + private static readonly object[] SmartGitMergeMessages = { - new object[] { "Finish feature/one", "feature/one", null, null }, - new object[] { "Finish origin/feature/one", "origin/feature/one", null, null }, - new object[] { "Finish v4.0.0", "v4.0.0", null, new SemanticVersion(4) }, - new object[] { "Finish feature/4.1/one", "feature/4.1/one", null, new SemanticVersion(4, 1) }, - new object[] { "Finish origin/4.1/feature/one", "origin/4.1/feature/one", null, new SemanticVersion(4, 1) }, - new object[] { "Finish V://10.10.10.10", "V://10.10.10.10", null, null }, - - //TODO: Investigate successful smart git merge messages that may be invalid - // The branch name appears to be incorrect - new object[] { "Finish V4.0.0 into master", "V4.0.0 into master", "master", new SemanticVersion(4) } - }; + new object[] { "Finish feature/one", "feature/one", null, null }, + new object[] { "Finish origin/feature/one", "origin/feature/one", null, null }, + new object[] { "Finish v4.0.0", "v4.0.0", null, new SemanticVersion(4) }, + new object[] { "Finish feature/4.1/one", "feature/4.1/one", null, new SemanticVersion(4, 1) }, + new object[] { "Finish origin/4.1/feature/one", "origin/4.1/feature/one", null, new SemanticVersion(4, 1) }, + new object[] { "Finish V://10.10.10.10", "V://10.10.10.10", null, null }, + new object[] { "Finish V4.0.0 into master", "V4.0.0", "master", new SemanticVersion(4) } + }; [TestCaseSource(nameof(SmartGitMergeMessages))] public void ParsesSmartGitMergeMessage( @@ -183,6 +170,7 @@ public void ParsesSmartGitMergeMessage( var sut = new MergeMessage(message, _config); // Assert + sut.MatchDefinition.ShouldBe("SmartGit"); sut.TargetBranch.ShouldBe(expectedTargetBranch); sut.MergedBranch.ShouldBe(expectedMergedBranch); sut.IsMergedPullRequest.ShouldBeFalse(); @@ -192,14 +180,14 @@ public void ParsesSmartGitMergeMessage( private static readonly object[] RemoteTrackingMergeMessages = { - new object[] { "Merge remote-tracking branch 'feature/one' into master", "feature/one", "master", null }, - new object[] { "Merge remote-tracking branch 'origin/feature/one' into dev", "origin/feature/one", "dev", null }, - new object[] { "Merge remote-tracking branch 'v4.0.0' into master", "v4.0.0", "master", new SemanticVersion(4) }, - new object[] { "Merge remote-tracking branch 'V4.0.0' into master", "V4.0.0", "master", new SemanticVersion(4) }, - new object[] { "Merge remote-tracking branch 'feature/4.1/one' into dev", "feature/4.1/one", "dev", new SemanticVersion(4, 1) }, - new object[] { "Merge remote-tracking branch 'origin/4.1/feature/one' into master", "origin/4.1/feature/one", "master", new SemanticVersion(4, 1) }, - new object[] { "Merge remote-tracking branch 'v://10.10.10.10' into master", "v://10.10.10.10", "master", null } - }; + new object[] { "Merge remote-tracking branch 'feature/one' into master", "feature/one", "master", null }, + new object[] { "Merge remote-tracking branch 'origin/feature/one' into dev", "origin/feature/one", "dev", null }, + new object[] { "Merge remote-tracking branch 'v4.0.0' into master", "v4.0.0", "master", new SemanticVersion(4) }, + new object[] { "Merge remote-tracking branch 'V4.0.0' into master", "V4.0.0", "master", new SemanticVersion(4) }, + new object[] { "Merge remote-tracking branch 'feature/4.1/one' into dev", "feature/4.1/one", "dev", new SemanticVersion(4, 1) }, + new object[] { "Merge remote-tracking branch 'origin/4.1/feature/one' into master", "origin/4.1/feature/one", "master", new SemanticVersion(4, 1) }, + new object[] { "Merge remote-tracking branch 'v://10.10.10.10' into master", "v://10.10.10.10", "master", null } + }; [TestCaseSource(nameof(RemoteTrackingMergeMessages))] public void ParsesRemoteTrackingMergeMessage( @@ -212,11 +200,96 @@ public void ParsesRemoteTrackingMergeMessage( var sut = new MergeMessage(message, _config); // Assert + sut.MatchDefinition.ShouldBe("RemoteTracking"); sut.TargetBranch.ShouldBe(expectedTargetBranch); sut.MergedBranch.ShouldBe(expectedMergedBranch); sut.IsMergedPullRequest.ShouldBeFalse(); sut.PullRequestNumber.ShouldBeNull(); sut.Version.ShouldBe(expectedVersion); } + + private static readonly object[] ParsesTfsEnglishUSMergeMessages = + { + new object[] { "Merge feature/one to master", "feature/one", "master", null }, + new object[] { "Merge v://10.10.10.10 to master", "v://10.10.10.10", "master", null }, + new object[] { "Merge feature/one to v://10.10.10.10", "feature/one", "v://10.10.10.10", null }, + new object[] { "Merge V4.0.0 to master", "V4.0.0", "master", new SemanticVersion(4) }, + new object[] { "Merge feature/4.1/one to master", "feature/4.1/one", "master", new SemanticVersion(4, 1) } + }; + + [TestCaseSource(nameof(ParsesTfsEnglishUSMergeMessages))] + public void ParsesTfsEnglishUSMessage( + string message, + string expectedMergedBranch, + string expectedTargetBranch, + SemanticVersion expectedVersion) + { + // Act + var sut = new MergeMessage(message, _config); + + // Assert + sut.MatchDefinition.ShouldBe("TfsMergeMessageEnglishUS"); + sut.TargetBranch.ShouldBe(expectedTargetBranch); + sut.MergedBranch.ShouldBe(expectedMergedBranch); + sut.IsMergedPullRequest.ShouldBeFalse(); + sut.PullRequestNumber.ShouldBeNull(); + sut.Version.ShouldBe(expectedVersion); + } + + private static readonly object[] ParsesTfsGermanDEMergeMessages = + { + new object[] { "Zusammengeführter PR \"1234\": feature/one mit master mergen", "feature/one", "master", null, 1234 }, + new object[] { "Zusammengeführter PR \"1234\": v://10.10.10.10 mit master mergen", "v://10.10.10.10", "master", null, 1234 }, + new object[] { "Zusammengeführter PR \"1234\": feature/one mit v://10.10.10.10 mergen", "feature/one", "v://10.10.10.10", null, 1234 }, + new object[] { "Zusammengeführter PR \"1234\": V4.0.0 mit master mergen", "V4.0.0", "master", new SemanticVersion(4), 1234 }, + new object[] { "Zusammengeführter PR \"1234\": feature/4.1/one mit master mergen", "feature/4.1/one", "master", new SemanticVersion(4, 1), 1234 } + }; + + [TestCaseSource(nameof(ParsesTfsGermanDEMergeMessages))] + public void ParseTfsGermanDEMessage( + string message, + string expectedMergedBranch, + string expectedTargetBranch, + SemanticVersion expectedVersion, + int? expectedPullRequestNumber) + { + // Act + var sut = new MergeMessage(message, _config); + + // Assert + sut.MatchDefinition.ShouldBe("TfsMergeMessageGermanDE"); + sut.TargetBranch.ShouldBe(expectedTargetBranch); + sut.MergedBranch.ShouldBe(expectedMergedBranch); + sut.IsMergedPullRequest.ShouldBeTrue(); + sut.PullRequestNumber.ShouldBe(expectedPullRequestNumber); + sut.Version.ShouldBe(expectedVersion); + } + + private static readonly object[] InvalidMergeMessages = + { + new object[] { "Merge pull request # from feature/one", "", null, null, null }, + new object[] { "Merge pull request # in feature/one from feature/two to master" , "", null, null, null }, + new object[] { "Zusammengeführter PR : feature/one mit master mergen", "", null, null, null } + }; + + [TestCaseSource(nameof(InvalidMergeMessages))] + public void ParsesInvalidMergeMessage( + string message, + string expectedMergedBranch, + string expectedTargetBranch, + SemanticVersion expectedVersion, + int? expectedPullRequestNumber) + { + // Act + var sut = new MergeMessage(message, _config); + + // Assert + sut.MatchDefinition.ShouldBeNull(); + sut.TargetBranch.ShouldBe(expectedTargetBranch); + sut.MergedBranch.ShouldBe(expectedMergedBranch); + sut.IsMergedPullRequest.ShouldBeFalse(); + sut.PullRequestNumber.ShouldBe(expectedPullRequestNumber); + sut.Version.ShouldBe(expectedVersion); + } } } diff --git a/src/GitVersionCore/MergeMessage.cs b/src/GitVersionCore/MergeMessage.cs index 9c3a03a88b..accad9b726 100644 --- a/src/GitVersionCore/MergeMessage.cs +++ b/src/GitVersionCore/MergeMessage.cs @@ -1,138 +1,88 @@ using System; +using System.Collections.Generic; 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 parseBitBucketPullMergeMessage = new Regex( - @"^Merge pull request #(?\d*) (from|in) (?.*) from (?.*) to (?.*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - static Regex smartGitMergeMessage = new Regex( - @"^Finish (?.*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - static Regex parseRemoteTrackingMergeMessage = new Regex( - @"^Merge remote-tracking branch '(?.*)'( into (?.*))?", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - static Regex parseTfsMergeMessageEnglishUS = new Regex( - @"^Merge (?.*) to (?.*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - // Zusammengeführter PR \"9\": release/5.0.1 mit master mergen - static Regex parseTfsMergeMessageGermanDE = new Regex( - @"^Zusammengeführter PR ""(?\d*)""\: (?.*) mit (?.*) mergen", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private string mergeMessage; + private static readonly IList Patterns = new List + { + new MergeMessagePattern("Default", @"^Merge (branch|tag) '(?[^']*)'(?: into (?[^\s]*))*"), + new MergeMessagePattern("SmartGit", @"^Finish (?[^\s]*)(?: into (?[^\s]*))*"), + new MergeMessagePattern("BitBucketPull", @"^Merge pull request #(?\d+) (from|in) (?.*) from (?[^\s]*) to (?[^\s]*)"), + new MergeMessagePattern("GitHubPull", @"^Merge pull request #(?\d+) (from|in) (?:(?[^\s]*))(?: into (?[^\s]*))*"), + new MergeMessagePattern("RemoteTracking", @"^Merge remote-tracking branch '(?[^\s]*)'(?: into (?[^\s]*))*"), + new MergeMessagePattern("TfsMergeMessageEnglishUS", @"^Merge (?[^\s]*) to (?[^\s]*)"), + new MergeMessagePattern("TfsMergeMessageGermanDE",@"^Zusammengeführter PR ""(?\d+)""\: (?.*) mit (?.*) mergen") + }; public MergeMessage(string mergeMessage, Config config) { - this.mergeMessage = mergeMessage; + if (mergeMessage == null) + throw new NullReferenceException(); - var lastIndexOf = mergeMessage.LastIndexOf("into", StringComparison.OrdinalIgnoreCase); - if (lastIndexOf != -1) + foreach (var pattern in Patterns) { - // If we have into in the merge message the rest should be the target branch - TargetBranch = mergeMessage.Substring(lastIndexOf + 5); - } + var match = pattern.Format.Match(mergeMessage); + if (match.Success) + { + MatchDefinition = pattern.Name; + MergedBranch = match.Groups["SourceBranch"].Value; - MergedBranch = ParseBranch(); + if (match.Groups["TargetBranch"].Success) + { + TargetBranch = match.Groups["TargetBranch"].Value; + } - // 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(@"^(? PullRequestNumber != null; + public int? PullRequestNumber { get; } + public SemanticVersion Version { get; } - match = parseRemoteTrackingMergeMessage.Match(mergeMessage); - if (match.Success) - { - var from = match.Groups["SourceBranch"].Value; - // TODO We could remove/separate the remote name at this point? - return from; - } - match = parseTfsMergeMessageEnglishUS.Match(mergeMessage); - if (match.Success) - { - IsMergedPullRequest = true; - var from = match.Groups["SourceBranch"].Value; - return from; - } + private SemanticVersion ParseVersion(string branchName, string tagPrefix) + { + // Remove remotes and branch prefixes like release/ feature/ hotfix/ etc + var toMatch = Regex.Replace(MergedBranch, @"^(\w+[-/])*", "", RegexOptions.IgnoreCase); + toMatch = Regex.Replace(toMatch, $"^{tagPrefix}", ""); + // We don't match if the version is likely an ip (i.e starts with http://) + var versionMatch = new Regex(@"^(?