Skip to content

Refactoring of merge message class #1639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 49 additions & 26 deletions src/GitVersionCore.Tests/MergeMessageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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();
Expand All @@ -91,13 +92,7 @@ public void ParsesMergeMessage(
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 into dev", "feature/one", "dev", null, 1234 }
};

[TestCaseSource(nameof(GitHubPullPullMergeMessages))]
Expand All @@ -112,31 +107,32 @@ 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();
sut.PullRequestNumber.ShouldBe(expectedPullRequestNumber);
sut.Version.ShouldBe(expectedVersion);
}

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 },
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 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 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 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 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 v4.0.0 to master", "v4.0.0", "master", new SemanticVersion(4), 1234 }



};

[TestCaseSource(nameof(BitBucketPullMergeMessages))]
Expand All @@ -151,13 +147,15 @@ 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();
sut.PullRequestNumber.ShouldBe(expectedPullRequestNumber);
sut.Version.ShouldBe(expectedVersion);
}


private static readonly object[] SmartGitMergeMessages =
{
new object[] { "Finish feature/one", "feature/one", null, null },
Expand All @@ -166,10 +164,7 @@ public void ParsesBitBucketPullMergeMessage(
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 V4.0.0 into master", "V4.0.0", "master", new SemanticVersion(4) }
};

[TestCaseSource(nameof(SmartGitMergeMessages))]
Expand All @@ -183,6 +178,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();
Expand Down Expand Up @@ -212,11 +208,38 @@ 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[] 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 }
};

[TestCaseSource(nameof(InvalidMergeMessages))]
public void ParsesInvalidBitBucketPullMergeMessage(
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);
}
}
}
158 changes: 53 additions & 105 deletions src/GitVersionCore/MergeMessage.cs
Original file line number Diff line number Diff line change
@@ -1,138 +1,86 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace GitVersion
{
class MergeMessage
{
static Regex parseMergeMessage = new Regex(
@"^Merge (branch|tag) '(?<Branch>[^']*)'",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static Regex parseGitHubPullMergeMessage = new Regex(
@"^Merge pull request #(?<PullRequestNumber>\d*) (from|in) (?<Source>.*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static Regex parseBitBucketPullMergeMessage = new Regex(
@"^Merge pull request #(?<PullRequestNumber>\d*) (from|in) (?<Source>.*) from (?<SourceBranch>.*) to (?<TargetBranch>.*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static Regex smartGitMergeMessage = new Regex(
@"^Finish (?<Branch>.*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static Regex parseRemoteTrackingMergeMessage = new Regex(
@"^Merge remote-tracking branch '(?<SourceBranch>.*)'( into (?<TargetBranch>.*))?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static Regex parseTfsMergeMessageEnglishUS = new Regex(
@"^Merge (?<SourceBranch>.*) to (?<TargetBranch>.*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Zusammengeführter PR \"9\": release/5.0.1 mit master mergen
static Regex parseTfsMergeMessageGermanDE = new Regex(
@"^Zusammengeführter PR ""(?<PullRequestNumber>\d*)""\: (?<SourceBranch>.*) mit (?<TargetBranch>.*) mergen",
RegexOptions.IgnoreCase | RegexOptions.Compiled);

private string mergeMessage;
private static readonly IList<MergeMessagePattern> Patterns = new List<MergeMessagePattern>
{
new MergeMessagePattern("Default", @"^Merge (branch|tag) '(?<SourceBranch>[^']*)'(?: into (?<TargetBranch>[^\s]*))*"),
new MergeMessagePattern("SmartGit", @"^Finish (?<SourceBranch>[^\s]*)(?: into (?<TargetBranch>[^\s]*))*"),
new MergeMessagePattern("BitBucketPull", @"^Merge pull request #(?<PullRequestNumber>\d+) (from|in) (?<Source>.*) from (?<SourceBranch>[^\s]*) to (?<TargetBranch>[^\s]*)"),
new MergeMessagePattern("GitHubPull", @"^Merge pull request #(?<PullRequestNumber>\d+) (from|in) (?:(?<SourceBranch>[^\s]*))(?: into (?<TargetBranch>[^\s]*))*"),
new MergeMessagePattern("RemoteTracking", @"^Merge remote-tracking branch '(?<SourceBranch>[^\s]*)'(?: into (?<TargetBranch>[^\s]*))*")
};

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(@"^(?<!://)\d+\.\d+(\.*\d+)*");
var version = versionMatch.Match(toMatch);
if (int.TryParse(match.Groups["PullRequestNumber"].Value, out var pullNumber))
{
PullRequestNumber = pullNumber;
}

if (version.Success)
{
SemanticVersion val;
if (SemanticVersion.TryParse(version.Value, config.TagPrefix, out val))
{
Version = val;
Version = ParseVersion(MergedBranch, config.TagPrefix);

break;
}
}
}

private string ParseBranch()
{
var match = parseMergeMessage.Match(mergeMessage);
if (match.Success)
{
return match.Groups["Branch"].Value;
}

match = smartGitMergeMessage.Match(mergeMessage);
if (match.Success)
{
return match.Groups["Branch"].Value;
}

match = parseBitBucketPullMergeMessage.Match(mergeMessage);
if (match.Success)
{
IsMergedPullRequest = true;
PullRequestNumber = GetPullRequestNumber(match);
return match.Groups["SourceBranch"].Value;
}

match = parseGitHubPullMergeMessage.Match(mergeMessage);
if (match.Success)
{
IsMergedPullRequest = true;
PullRequestNumber = GetPullRequestNumber(match);
var from = match.Groups["Source"].Value;
// TODO We could remove/separate the remote name at this point?
return from;
}
public string MatchDefinition { get; }
public string TargetBranch { get; }
public string MergedBranch { get; } = "";
public bool IsMergedPullRequest => 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(@"^(?<!://)\d+\.\d+(\.*\d+)*");
var version = versionMatch.Match(toMatch);

match = parseTfsMergeMessageGermanDE.Match(mergeMessage);
if (match.Success)
if (version.Success && SemanticVersion.TryParse(version.Value, tagPrefix, out var val))
{
IsMergedPullRequest = true;
var from = match.Groups["SourceBranch"].Value;
return from;
return val;
}

return "";
return null;
}

private int GetPullRequestNumber(Match match)
private class MergeMessagePattern
{
int pullNumber;
if (int.TryParse(match.Groups["PullRequestNumber"].Value, out pullNumber))
public MergeMessagePattern(string name, string format)
{
PullRequestNumber = pullNumber;
Name = name;
Format = new Regex(format, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
return pullNumber;
}

public string TargetBranch { get; }
public string MergedBranch { get; }
public bool IsMergedPullRequest { get; private set; }
public int? PullRequestNumber { get; private set; }
public SemanticVersion Version { get; }
public string Name { get; }

public Regex Format { get; }
}
}
}