Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ commit-date-format: 'yyyy-MM-dd'
ignore:
sha: []
commits-before: yyyy-MM-ddTHH:mm:ss
merge-messages: {}
```

And the description of the available options are:
Expand Down Expand Up @@ -179,6 +180,24 @@ Date and time in the format `yyyy-MM-ddTHH:mm:ss` (eg `commits-before:
2015-10-23T12:23:15`) to setup an exclusion range. Effectively any commit before
`commits-before` will be ignored.

### merge-messages
Custom merge message formats to enable identification of merge messages that do not
follow the built-in conventions. Entries should be added as key-value pairs where
the value is a regular expression.
e.g.

```
merge-messages:
tfs: ^Merged (?:PR (?<PullRequestNumber>\d+)): Merge (?<SourceBranch>.+) to (?<TargetBranch>.+)
```

The regular expression should contain the following capture groups:
+ SourceBranch - Identifies the source branch of the merge
+ TargetBranch - Identifies the target of the merge
+ PullRequestNumber - Captures the pull-request number

Custom merge message formats are evalauted _before_ any built in formats.

## Branch configuration
Then we have branch specific configuration, which looks something like this:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ branches:
ignore:
sha: []
commit-date-format: yyyy-MM-dd
merge-messages: {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ next-version: 2.0.0
branches: {}
ignore:
sha: []
merge-messages: {}
105 changes: 103 additions & 2 deletions src/GitVersionCore.Tests/MergeMessageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using NUnit.Framework;
using Shouldly;
using System;
using System.Collections.Generic;

namespace GitVersionCore.Tests
{
Expand Down Expand Up @@ -228,15 +229,15 @@ public void ParsesTfsEnglishUSMessage(
var sut = new MergeMessage(message, _config);

// Assert
sut.MatchDefinition.ShouldBe("TfsMergeMessageEnglishUS");
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 =
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 },
Expand Down Expand Up @@ -291,5 +292,105 @@ public void ParsesInvalidMergeMessage(
sut.PullRequestNumber.ShouldBe(expectedPullRequestNumber);
sut.Version.ShouldBe(expectedVersion);
}

[Test]
public void MatchesSingleCustomMessage()
{
// Arrange
var message = "My custom message";
var definition = "Mycustom";
_config.MergeMessageFormats = new Dictionary<string, string>
{
[definition] = message
};

// Act
var sut = new MergeMessage(message, _config);

// Assert
sut.MatchDefinition.ShouldBe(definition);
sut.TargetBranch.ShouldBeNull();
sut.MergedBranch.ShouldBeEmpty();
sut.IsMergedPullRequest.ShouldBeFalse();
sut.PullRequestNumber.ShouldBeNull();
sut.Version.ShouldBeNull();
}

[Test]
public void MatchesMultipleCustomMessages()
{
// Arrange
var format = "My custom message";
var definition = "Mycustom";
_config.MergeMessageFormats = new Dictionary<string, string>
{
["Default2"] = "some example",
["Default3"] = "another example",
[definition] = format
};

// Act
var sut = new MergeMessage(format, _config);

// Assert
sut.MatchDefinition.ShouldBe(definition);
sut.TargetBranch.ShouldBeNull();
sut.MergedBranch.ShouldBeEmpty();
sut.IsMergedPullRequest.ShouldBeFalse();
sut.PullRequestNumber.ShouldBeNull();
sut.Version.ShouldBeNull();
}

[Test]
public void MatchesCaptureGroupsFromCustomMessages()
{
// Arrange
var format = @"^Merged PR #(?<PullRequestNumber>\d+) into (?<TargetBranch>[^\s]*) from (?:(?<SourceBranch>[^\s]*))";
var definition = "Mycustom";
_config.MergeMessageFormats = new Dictionary<string, string>
{
[definition] = format
};
var pr = 1234;
var target = "master";
var source = "feature/2.0/example";


// Act
var sut = new MergeMessage($"Merged PR #{pr} into {target} from {source}", _config);

// Assert
sut.MatchDefinition.ShouldBe(definition);
sut.TargetBranch.ShouldBe(target);
sut.MergedBranch.ShouldBe(source);
sut.IsMergedPullRequest.ShouldBeTrue();
sut.PullRequestNumber.ShouldBe(pr);
sut.Version.ShouldBe(new SemanticVersion(2, 0));
}

[Test]
public void ReturnsAfterFirstMatchingPattern()
{
// Arrange
var format = @"^Merge (branch|tag) '(?<SourceBranch>[^']*)'(?: into (?<TargetBranch>[^\s]*))*";
var definition = "Mycustom";
_config.MergeMessageFormats = new Dictionary<string, string>
{
[definition] = format,
["Default2"] = format,
["Default3"] = format
};

// Act
var sut = new MergeMessage("Merge branch 'this'", _config);

// Assert
sut.MatchDefinition.ShouldBe(definition);
sut.TargetBranch.ShouldBeNull();
sut.MergedBranch.ShouldBe("this");
sut.IsMergedPullRequest.ShouldBeFalse();
sut.PullRequestNumber.ShouldBeNull();
sut.Version.ShouldBeNull();
}
}
}
3 changes: 3 additions & 0 deletions src/GitVersionCore/Configuration/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,8 @@ T MergeObjects<T>(T target, T source)

[YamlMember(Alias = "commit-date-format")]
public string CommitDateFormat { get; set; }

[YamlMember(Alias = "merge-messages")]
public Dictionary<string, string> MergeMessageFormats { get; set; } = new Dictionary<string, string>();
}
}
96 changes: 52 additions & 44 deletions src/GitVersionCore/MergeMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,74 @@

namespace GitVersion
{
class MergeMessage
public class MergeMessage
{
private static readonly IList<MergeMessagePattern> Patterns = new List<MergeMessagePattern>
private static readonly IList<KeyValuePair<string, Regex>> DefaultPatterns = new List<KeyValuePair<string, Regex>>
{
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]*))*"),
new MergeMessagePattern("TfsMergeMessageEnglishUS", @"^Merge (?<SourceBranch>[^\s]*) to (?<TargetBranch>[^\s]*)"),
new MergeMessagePattern("TfsMergeMessageGermanDE",@"^Zusammengeführter PR ""(?<PullRequestNumber>\d+)""\: (?<SourceBranch>.*) mit (?<TargetBranch>.*) mergen")
Pattern("Default", @"^Merge (branch|tag) '(?<SourceBranch>[^']*)'(?: into (?<TargetBranch>[^\s]*))*"),
Pattern("SmartGit", @"^Finish (?<SourceBranch>[^\s]*)(?: into (?<TargetBranch>[^\s]*))*"),
Pattern("BitBucketPull", @"^Merge pull request #(?<PullRequestNumber>\d+) (from|in) (?<Source>.*) from (?<SourceBranch>[^\s]*) to (?<TargetBranch>[^\s]*)"),
Pattern("GitHubPull", @"^Merge pull request #(?<PullRequestNumber>\d+) (from|in) (?:(?<SourceBranch>[^\s]*))(?: into (?<TargetBranch>[^\s]*))*"),
Pattern("RemoteTracking", @"^Merge remote-tracking branch '(?<SourceBranch>[^\s]*)'(?: into (?<TargetBranch>[^\s]*))*"),
Pattern("TfsMergeMessageEnglishUS", @"^Merge (?<SourceBranch>[^\s]*) to (?<TargetBranch>[^\s]*)"),
Pattern("TfsMergeMessageGermanDE",@"^Zusammengeführter PR ""(?<PullRequestNumber>\d+)""\: (?<SourceBranch>.*) mit (?<TargetBranch>.*) mergen")
};

public MergeMessage(string mergeMessage, Config config)
{
if (mergeMessage == null)
throw new NullReferenceException();

foreach (var pattern in Patterns)
foreach(var entry in config.MergeMessageFormats)
{
var match = pattern.Format.Match(mergeMessage);
if (match.Success)
var pattern = Pattern(entry.Key, entry.Value);
if (ApplyPattern(mergeMessage, config.TagPrefix, pattern))
{
MatchDefinition = pattern.Name;
MergedBranch = match.Groups["SourceBranch"].Value;

if (match.Groups["TargetBranch"].Success)
{
TargetBranch = match.Groups["TargetBranch"].Value;
}

if (int.TryParse(match.Groups["PullRequestNumber"].Value, out var pullNumber))
{
PullRequestNumber = pullNumber;
}

Version = ParseVersion(MergedBranch, config.TagPrefix);
return;
}
}

break;
foreach (var pattern in DefaultPatterns)
{
if (ApplyPattern(mergeMessage, config.TagPrefix, pattern))
{
return;
}
}
}

public string MatchDefinition { get; }
public string TargetBranch { get; }
public string MergedBranch { get; } = "";
public string MatchDefinition { get; private set; }
public string TargetBranch { get; private set; }
public string MergedBranch { get; private set; } = "";
public bool IsMergedPullRequest => PullRequestNumber != null;
public int? PullRequestNumber { get; }
public SemanticVersion Version { get; }
public int? PullRequestNumber { get; private set; }
public SemanticVersion Version { get; private set; }

private bool ApplyPattern(string mergeMessage, string tagPrefix, KeyValuePair<string, Regex> pattern)
{
var match = pattern.Value.Match(mergeMessage);
if (match.Success)
{
MatchDefinition = pattern.Key;
MergedBranch = match.Groups["SourceBranch"].Value;

if (match.Groups["TargetBranch"].Success)
{
TargetBranch = match.Groups["TargetBranch"].Value;
}

if (int.TryParse(match.Groups["PullRequestNumber"].Value, out var pullNumber))
{
PullRequestNumber = pullNumber;
}

Version = ParseVersion(MergedBranch, tagPrefix);

return true;
}

return false;
}

private SemanticVersion ParseVersion(string branchName, string tagPrefix)
{
Expand All @@ -72,17 +90,7 @@ private SemanticVersion ParseVersion(string branchName, string tagPrefix)
return null;
}

private class MergeMessagePattern
{
public MergeMessagePattern(string name, string format)
{
Name = name;
Format = new Regex(format, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}

public string Name { get; }

public Regex Format { get; }
}
private static KeyValuePair<string, Regex> Pattern(string name, string format)
=> new KeyValuePair<string, Regex>(name, new Regex(format, RegexOptions.IgnoreCase | RegexOptions.Compiled));
}
}