diff --git a/docs/input/docs/reference/configuration.md b/docs/input/docs/reference/configuration.md index f0cc4a86f1..1aaea48b13 100644 --- a/docs/input/docs/reference/configuration.md +++ b/docs/input/docs/reference/configuration.md @@ -176,6 +176,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false @@ -210,7 +211,7 @@ update-build-number: true version-bump-reset-message: "=semver:" version-in-branch-pattern: "(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*" ``` -snippet source | anchor +snippet source | anchor The supported built-in configuration for the `GitHubFlow` workflow (`workflow: GitHubFlow/v1`) looks like: @@ -302,6 +303,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false @@ -336,7 +338,7 @@ update-build-number: true version-bump-reset-message: "=semver:" version-in-branch-pattern: "(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*" ``` -snippet source | anchor +snippet source | anchor The preview built-in configuration (experimental usage only) for the `TrunkBased` workflow (`workflow: TrunkBased/preview1`) looks like: @@ -417,6 +419,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false @@ -447,11 +450,19 @@ update-build-number: true version-bump-reset-message: "=semver:" version-in-branch-pattern: "(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*" ``` -snippet source | anchor +snippet source | anchor The details of the available options are as follows: +:::{.alert .alert-info} +**Regular expression matching** + +Unless stated otherwise, regular expressions in GitVersion configuration are +matched case-insensitively. To require case-sensitive matching, prefix the +expression with `(?-i)`, for example `(?-i)^experimental-`. +::: + ### assembly-file-versioning-format Specifies the format of `AssemblyFileVersion` and @@ -691,8 +702,9 @@ A commit having changes only in `/ProjectB/*` path would be ignored. A commit ha * `/ProductA/*` and `/ProductB/*` and `/LibraryC/*` ::: -Note: The `ignore.paths` configuration is case-sensitive. -This can lead to unexpected behavior on case-insensitive file systems, such as Windows. To ensure consistent matching regardless of case, you can prefix your regular expressions with the case-insensitive flag `(?i)`. For example, `(?i)^docs\/` will match both `docs/` and `Docs/`. +Note: The `ignore.paths` configuration is case-insensitive. To require +case-sensitive path matching, prefix the regular expression with `(?-i)`. For +example, `(?-i)^docs\/` matches `docs/` but not `Docs/`. ::: ::: {.alert .alert-warning} @@ -719,6 +731,24 @@ ignore: - 764e16321318f2fdb9cdeaa56d1156a1cba307d7 ``` +#### tags + +A sequence of regular expressions matching tag names that should not be used as +[version sources][version-sources]. Patterns are matched case-insensitively +against the friendly tag name without the `refs/tags/` prefix. Multiple patterns +use OR semantics, and `^` and `$` can be used to anchor a match. To require +case-sensitive matching, prefix a pattern with `(?-i)`. + +```yaml +ignore: + tags: + - ^experimental- + - ^v0\. +``` + +Ignoring a tag does not ignore the commit it points to. The commit remains part +of version calculation and can still be reached through other references. + ### increment The part of the SemVer to increment when GitVersion detects it needs to be diff --git a/docs/input/docs/reference/mdsource/configuration.source.md b/docs/input/docs/reference/mdsource/configuration.source.md index 658c7602d7..fb4e45bcbf 100644 --- a/docs/input/docs/reference/mdsource/configuration.source.md +++ b/docs/input/docs/reference/mdsource/configuration.source.md @@ -50,6 +50,14 @@ snippet: /docs/workflows/TrunkBased/preview1.yml The details of the available options are as follows: +:::{.alert .alert-info} +**Regular expression matching** + +Unless stated otherwise, regular expressions in GitVersion configuration are +matched case-insensitively. To require case-sensitive matching, prefix the +expression with `(?-i)`, for example `(?-i)^experimental-`. +::: + ### assembly-file-versioning-format Specifies the format of `AssemblyFileVersion` and @@ -289,8 +297,9 @@ A commit having changes only in `/ProjectB/*` path would be ignored. A commit ha * `/ProductA/*` and `/ProductB/*` and `/LibraryC/*` ::: -Note: The `ignore.paths` configuration is case-sensitive. -This can lead to unexpected behavior on case-insensitive file systems, such as Windows. To ensure consistent matching regardless of case, you can prefix your regular expressions with the case-insensitive flag `(?i)`. For example, `(?i)^docs\/` will match both `docs/` and `Docs/`. +Note: The `ignore.paths` configuration is case-insensitive. To require +case-sensitive path matching, prefix the regular expression with `(?-i)`. For +example, `(?-i)^docs\/` matches `docs/` but not `Docs/`. ::: ::: {.alert .alert-warning} @@ -317,6 +326,24 @@ ignore: - 764e16321318f2fdb9cdeaa56d1156a1cba307d7 ``` +#### tags + +A sequence of regular expressions matching tag names that should not be used as +[version sources][version-sources]. Patterns are matched case-insensitively +against the friendly tag name without the `refs/tags/` prefix. Multiple patterns +use OR semantics, and `^` and `$` can be used to anchor a match. To require +case-sensitive matching, prefix a pattern with `(?-i)`. + +```yaml +ignore: + tags: + - ^experimental- + - ^v0\. +``` + +Ignoring a tag does not ignore the commit it points to. The commit remains part +of version calculation and can still be reached through other references. + ### increment The part of the SemVer to increment when GitVersion detects it needs to be diff --git a/docs/input/docs/workflows/GitFlow/v1.yml b/docs/input/docs/workflows/GitFlow/v1.yml index 6d9dc6ae1a..abe425fc5e 100644 --- a/docs/input/docs/workflows/GitFlow/v1.yml +++ b/docs/input/docs/workflows/GitFlow/v1.yml @@ -133,6 +133,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/docs/input/docs/workflows/GitHubFlow/v1.yml b/docs/input/docs/workflows/GitHubFlow/v1.yml index b3d5d08601..2474e1c6f6 100644 --- a/docs/input/docs/workflows/GitHubFlow/v1.yml +++ b/docs/input/docs/workflows/GitHubFlow/v1.yml @@ -82,6 +82,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/docs/input/docs/workflows/TrunkBased/preview1.yml b/docs/input/docs/workflows/TrunkBased/preview1.yml index fe8ebe9e39..5f62fd5600 100644 --- a/docs/input/docs/workflows/TrunkBased/preview1.yml +++ b/docs/input/docs/workflows/TrunkBased/preview1.yml @@ -71,6 +71,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/schemas/7.0/GitVersion.configuration.json b/schemas/7.0/GitVersion.configuration.json index e1e4dbcd98..24c4890971 100644 --- a/schemas/7.0/GitVersion.configuration.json +++ b/schemas/7.0/GitVersion.configuration.json @@ -124,7 +124,7 @@ }, "regex": { "format": "regex", - "description": "The regular expression pattern to use to match this branch.", + "description": "The regular expression pattern to use to match this branch. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "type": [ "null", "string" @@ -195,12 +195,16 @@ ] }, "paths": { - "description": "A sequence of file paths to be excluded from the version calculations.", + "description": "A sequence of regular expressions matching file paths to be excluded from the version calculations. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "$ref": "#/$defs/hashSetOfString" }, "sha": { "description": "A sequence of SHAs to be excluded from the version calculations.", "$ref": "#/$defs/hashSetOfString" + }, + "tags": { + "description": "A sequence of regular expressions matching friendly tag names to be excluded as version sources. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", + "$ref": "#/$defs/hashSetOfString" } } }, @@ -235,7 +239,7 @@ }, "major-version-bump-message": { "format": "regex", - "description": "The regular expression to match commit messages with to perform a major version increment. Defaults to '[+=]semver:\\s?(breaking|major)'", + "description": "The regular expression to match commit messages with to perform a major version increment. Defaults to '[+=]semver:\\s?(breaking|major)'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "[+=]semver:\\s?(breaking|major)", "type": [ "null", @@ -243,7 +247,7 @@ ] }, "merge-message-formats": { - "description": "Custom merge message formats to enable identification of merge messages that do not follow the built-in conventions.", + "description": "Custom regular expression merge message formats to enable identification of merge messages that do not follow the built-in conventions. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "type": "object", "additionalProperties": { "type": "string" @@ -251,7 +255,7 @@ }, "minor-version-bump-message": { "format": "regex", - "description": "The regular expression to match commit messages with to perform a minor version increment. Defaults to '[+=]semver:\\s?(feature|minor)'", + "description": "The regular expression to match commit messages with to perform a minor version increment. Defaults to '[+=]semver:\\s?(feature|minor)'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "[+=]semver:\\s?(feature|minor)", "type": [ "null", @@ -276,7 +280,7 @@ }, "no-bump-message": { "format": "regex", - "description": "Used to tell GitVersion not to increment when in Mainline development mode. Defaults to '[+=]semver:\\s?(none|skip)'", + "description": "Used to tell GitVersion not to increment when in Mainline development mode. Defaults to '[+=]semver:\\s?(none|skip)'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "[+=]semver:\\s?(none|skip)", "type": [ "null", @@ -285,7 +289,7 @@ }, "patch-version-bump-message": { "format": "regex", - "description": "The regular expression to match commit messages with to perform a patch version increment. Defaults to '[+=]semver:\\s?(fix|patch)'", + "description": "The regular expression to match commit messages with to perform a patch version increment. Defaults to '[+=]semver:\\s?(fix|patch)'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "[+=]semver:\\s?(fix|patch)", "type": [ "null", @@ -305,7 +309,7 @@ }, "regex": { "format": "regex", - "description": "The regular expression pattern to use to match this branch.", + "description": "The regular expression pattern to use to match this branch. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "type": [ "null", "string" @@ -348,7 +352,7 @@ }, "tag-prefix": { "format": "regex", - "description": "A regular expression which is used to trim Git tags before processing. Defaults to '[vV]?'", + "description": "A regular expression which is used to trim Git tags before processing. Defaults to '[vV]?'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "[vV]?", "type": [ "null", @@ -383,7 +387,7 @@ }, "version-bump-reset-message": { "format": "regex", - "description": "The regular expression to match commit messages that reset the version bump baseline and suppress the configured branch increment. Defaults to '=semver:'", + "description": "The regular expression to match commit messages that reset the version bump baseline and suppress the configured branch increment. Defaults to '=semver:'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "=semver:", "type": [ "null", @@ -392,7 +396,7 @@ }, "version-in-branch-pattern": { "format": "regex", - "description": "A regular expression which is used to determine the version number in the branch name or commit message (e.g., v1.0.0-LTS). Defaults to '(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*'.", + "description": "A regular expression which is used to determine the version number in the branch name or commit message (e.g., v1.0.0-LTS). Defaults to '(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.", "default": "(?[vV]?\\d+(\\.\\d+)?(\\.\\d+)?).*", "type": [ "null", diff --git a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationProviderTests.CanWriteOutEffectiveConfiguration.approved.txt b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationProviderTests.CanWriteOutEffectiveConfiguration.approved.txt index 6d9dc6ae1a..abe425fc5e 100644 --- a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationProviderTests.CanWriteOutEffectiveConfiguration.approved.txt +++ b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationProviderTests.CanWriteOutEffectiveConfiguration.approved.txt @@ -133,6 +133,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/src/GitVersion.Configuration.Tests/Configuration/IgnoreConfigurationTests.cs b/src/GitVersion.Configuration.Tests/Configuration/IgnoreConfigurationTests.cs index f5782f7a9f..fe493930fa 100644 --- a/src/GitVersion.Configuration.Tests/Configuration/IgnoreConfigurationTests.cs +++ b/src/GitVersion.Configuration.Tests/Configuration/IgnoreConfigurationTests.cs @@ -48,6 +48,34 @@ public void ShouldSupportsOtherSequenceFormat() configuration.Ignore.Shas.ShouldBe(["b6c0c9fda88830ebcd563e500a5a7da5a1658e98", "6c19c7c219ecf8dbc468042baefa73a1b213e8b1"]); } + [Test] + public void CanDeserializeCompactTagsSequence() + { + const string yaml = "ignore:\n tags: ['^preview-', '^v0\\.']"; + + var configuration = this.serializer.ReadConfiguration(yaml); + + configuration.ShouldNotBeNull(); + configuration.Ignore.Tags.ShouldBe(["^preview-", "^v0\\."]); + } + + [Test] + public void CanDeserializeMultilineTagsSequence() + { + const string yaml = + """ + ignore: + tags: + - ^preview- + - ^v0\. + """; + + var configuration = this.serializer.ReadConfiguration(yaml); + + configuration.ShouldNotBeNull(); + configuration.Ignore.Tags.ShouldBe(["^preview-", "^v0\\."]); + } + [Test] public void WhenNotInConfigShouldHaveDefaults() { @@ -60,6 +88,47 @@ public void WhenNotInConfigShouldHaveDefaults() configuration.Ignore.Before.ShouldBe(null); configuration.Ignore.Paths.ShouldBeEmpty(); configuration.Ignore.Shas.ShouldBeEmpty(); + configuration.Ignore.Tags.ShouldBeEmpty(); + } + + [Test] + public void IsEmpty_WhenTagPatternIsConfigured_ReturnsFalse() + { + var ignoreConfig = new IgnoreConfiguration { Tags = ["^preview-"] }; + + ignoreConfig.IsEmpty.ShouldBeFalse(); + } + + [Test] + public void InvalidIgnoreTagExpression_ThrowsConfigurationExceptionWithPropertyAndPattern() + { + const string invalidExpression = "[invalid"; + + var exception = Should.Throw(() => GitFlowConfigurationBuilder.New + .WithIgnoreConfiguration(new IgnoreConfiguration { Tags = [invalidExpression] }) + .Build()); + + exception.Message.ShouldContain("ignore.tags"); + exception.Message.ShouldContain(invalidExpression); + } + + [Test] + public void Serialize_IgnoreTagPatterns_UsesTagsPropertyName() + { + var ignoreConfig = new IgnoreConfiguration { Tags = ["^preview-"] }; + + var yaml = this.serializer.Serialize(ignoreConfig); + + yaml.ShouldContain("tags:"); + yaml.ShouldContain("^preview-"); + } + + [Test] + public void IgnoreConfigurationBuilder_WithTags_PreservesCollection() + { + var ignoreConfig = IgnoreConfigurationBuilder.New.WithTags("^preview-", "^v0\\.").Build(); + + ignoreConfig.Tags.ShouldBe(["^preview-", "^v0\\."]); } [Test] diff --git a/src/GitVersion.Configuration.Tests/Workflows/approved/GitFlow/v1.yml b/src/GitVersion.Configuration.Tests/Workflows/approved/GitFlow/v1.yml index 6d9dc6ae1a..abe425fc5e 100644 --- a/src/GitVersion.Configuration.Tests/Workflows/approved/GitFlow/v1.yml +++ b/src/GitVersion.Configuration.Tests/Workflows/approved/GitFlow/v1.yml @@ -133,6 +133,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/src/GitVersion.Configuration.Tests/Workflows/approved/GitHubFlow/v1.yml b/src/GitVersion.Configuration.Tests/Workflows/approved/GitHubFlow/v1.yml index b3d5d08601..2474e1c6f6 100644 --- a/src/GitVersion.Configuration.Tests/Workflows/approved/GitHubFlow/v1.yml +++ b/src/GitVersion.Configuration.Tests/Workflows/approved/GitHubFlow/v1.yml @@ -82,6 +82,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/src/GitVersion.Configuration.Tests/Workflows/approved/TrunkBased/preview1.yml b/src/GitVersion.Configuration.Tests/Workflows/approved/TrunkBased/preview1.yml index fe8ebe9e39..5f62fd5600 100644 --- a/src/GitVersion.Configuration.Tests/Workflows/approved/TrunkBased/preview1.yml +++ b/src/GitVersion.Configuration.Tests/Workflows/approved/TrunkBased/preview1.yml @@ -71,6 +71,7 @@ commit-message-incrementing: Enabled ignore: paths: [] sha: [] + tags: [] increment: Inherit is-main-branch: false is-release-branch: false diff --git a/src/GitVersion.Configuration/BranchConfiguration.cs b/src/GitVersion.Configuration/BranchConfiguration.cs index 5b408bd999..4fd0a61bd5 100644 --- a/src/GitVersion.Configuration/BranchConfiguration.cs +++ b/src/GitVersion.Configuration/BranchConfiguration.cs @@ -42,7 +42,7 @@ internal record BranchConfiguration : IBranchConfiguration public CommitMessageIncrementMode? CommitMessageIncrementing { get; set; } [JsonPropertyName("regex")] - [JsonPropertyDescription("The regular expression pattern to use to match this branch.")] + [JsonPropertyDescription("The regular expression pattern to use to match this branch. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyFormat(Format.Regex)] public string? RegularExpression { get; set; } diff --git a/src/GitVersion.Configuration/Builders/ConfigurationBuilderBase.cs b/src/GitVersion.Configuration/Builders/ConfigurationBuilderBase.cs index 79dae92447..9032356167 100644 --- a/src/GitVersion.Configuration/Builders/ConfigurationBuilderBase.cs +++ b/src/GitVersion.Configuration/Builders/ConfigurationBuilderBase.cs @@ -476,6 +476,8 @@ private static void FinalizeBranchConfiguration(IGitVersionConfiguration configu private static void ValidateConfiguration(IGitVersionConfiguration configuration) { + ValidateIgnoreExpressions("ignore.tags", configuration.Ignore.Tags); + foreach (var (name, branchConfiguration) in configuration.Branches) { var helpUrl = $"{FileSystemHelper.Path.NewLine}See https://gitversion.net/docs/reference/configuration for more info"; @@ -494,6 +496,23 @@ private static void ValidateConfiguration(IGitVersionConfiguration configuration } } + private static void ValidateIgnoreExpressions(string propertyName, IEnumerable expressions) + { + foreach (var expression in expressions) + { + try + { + _ = RegexPatterns.Cache.GetOrAdd(expression); + } + catch (ArgumentException exception) + { + throw new ConfigurationException( + $"Configuration property '{propertyName}' contains invalid regular expression '{expression}'.", + exception); + } + } + } + protected record BranchMetaData { public required string Name { get; init; } diff --git a/src/GitVersion.Configuration/Builders/IgnoreConfigurationBuilder.cs b/src/GitVersion.Configuration/Builders/IgnoreConfigurationBuilder.cs index b6cdf38578..ee69fd7e5e 100644 --- a/src/GitVersion.Configuration/Builders/IgnoreConfigurationBuilder.cs +++ b/src/GitVersion.Configuration/Builders/IgnoreConfigurationBuilder.cs @@ -8,6 +8,8 @@ internal sealed class IgnoreConfigurationBuilder private HashSet shas = []; + private HashSet tags = []; + public IgnoreConfigurationBuilder WithBefore(DateTimeOffset? value) { this.before = value; @@ -32,9 +34,28 @@ public IgnoreConfigurationBuilder WithShas(HashSet value) return this; } + public IgnoreConfigurationBuilder WithTags(IEnumerable values) + { + WithTags(new HashSet(values)); + return this; + } + + public IgnoreConfigurationBuilder WithTags(params string[] values) + { + WithTags(new HashSet(values)); + return this; + } + + public IgnoreConfigurationBuilder WithTags(HashSet value) + { + this.tags = value; + return this; + } + public IIgnoreConfiguration Build() => new IgnoreConfiguration { Before = this.before, - Shas = this.shas + Shas = this.shas, + Tags = this.tags }; } diff --git a/src/GitVersion.Configuration/GitVersionConfiguration.cs b/src/GitVersion.Configuration/GitVersionConfiguration.cs index e1e1fdeb11..7e0b3fb6e2 100644 --- a/src/GitVersion.Configuration/GitVersionConfiguration.cs +++ b/src/GitVersion.Configuration/GitVersionConfiguration.cs @@ -35,13 +35,13 @@ internal sealed record GitVersionConfiguration : BranchConfiguration, IGitVersio public string? AssemblyFileVersioningFormat { get; set; } [JsonPropertyName("tag-prefix")] - [JsonPropertyDescription($"A regular expression which is used to trim Git tags before processing. Defaults to '{RegexPatterns.Configuration.DefaultTagPrefixRegexPattern}'")] + [JsonPropertyDescription($"A regular expression which is used to trim Git tags before processing. Defaults to '{RegexPatterns.Configuration.DefaultTagPrefixRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.Configuration.DefaultTagPrefixRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? TagPrefixPattern { get; set; } [JsonPropertyName("version-in-branch-pattern")] - [JsonPropertyDescription($"A regular expression which is used to determine the version number in the branch name or commit message (e.g., v1.0.0-LTS). Defaults to '{RegexPatterns.Configuration.DefaultVersionInBranchRegexPattern}'.")] + [JsonPropertyDescription($"A regular expression which is used to determine the version number in the branch name or commit message (e.g., v1.0.0-LTS). Defaults to '{RegexPatterns.Configuration.DefaultVersionInBranchRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.Configuration.DefaultVersionInBranchRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? VersionInBranchPattern { get; set; } @@ -59,31 +59,31 @@ public string? NextVersion private string? nextVersion; [JsonPropertyName("major-version-bump-message")] - [JsonPropertyDescription($"The regular expression to match commit messages with to perform a major version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultMajorRegexPattern}'")] + [JsonPropertyDescription($"The regular expression to match commit messages with to perform a major version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultMajorRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.VersionCalculation.DefaultMajorRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? MajorVersionBumpMessage { get; set; } [JsonPropertyName("minor-version-bump-message")] - [JsonPropertyDescription($"The regular expression to match commit messages with to perform a minor version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultMinorRegexPattern}'")] + [JsonPropertyDescription($"The regular expression to match commit messages with to perform a minor version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultMinorRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.VersionCalculation.DefaultMinorRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? MinorVersionBumpMessage { get; set; } [JsonPropertyName("patch-version-bump-message")] - [JsonPropertyDescription($"The regular expression to match commit messages with to perform a patch version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultPatchRegexPattern}'")] + [JsonPropertyDescription($"The regular expression to match commit messages with to perform a patch version increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultPatchRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.VersionCalculation.DefaultPatchRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? PatchVersionBumpMessage { get; set; } [JsonPropertyName("no-bump-message")] - [JsonPropertyDescription($"Used to tell GitVersion not to increment when in Mainline development mode. Defaults to '{RegexPatterns.VersionCalculation.DefaultNoBumpRegexPattern}'")] + [JsonPropertyDescription($"Used to tell GitVersion not to increment when in Mainline development mode. Defaults to '{RegexPatterns.VersionCalculation.DefaultNoBumpRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.VersionCalculation.DefaultNoBumpRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? NoBumpMessage { get; set; } [JsonPropertyName("version-bump-reset-message")] - [JsonPropertyDescription($"The regular expression to match commit messages that reset the version bump baseline and suppress the configured branch increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultVersionBumpResetRegexPattern}'")] + [JsonPropertyDescription($"The regular expression to match commit messages that reset the version bump baseline and suppress the configured branch increment. Defaults to '{RegexPatterns.VersionCalculation.DefaultVersionBumpResetRegexPattern}'. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] [JsonPropertyDefault(RegexPatterns.VersionCalculation.DefaultVersionBumpResetRegexPattern)] [JsonPropertyFormat(Format.Regex)] public string? VersionBumpResetMessage { get; set; } @@ -99,7 +99,7 @@ public string? NextVersion public string? CommitDateFormat { get; set; } [JsonPropertyName("merge-message-formats")] - [JsonPropertyDescription("Custom merge message formats to enable identification of merge messages that do not follow the built-in conventions.")] + [JsonPropertyDescription("Custom regular expression merge message formats to enable identification of merge messages that do not follow the built-in conventions. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] public Dictionary MergeMessageFormats { get; set; } = []; [JsonIgnore] diff --git a/src/GitVersion.Configuration/IgnoreConfiguration.cs b/src/GitVersion.Configuration/IgnoreConfiguration.cs index 408d435df6..eadf19114f 100644 --- a/src/GitVersion.Configuration/IgnoreConfiguration.cs +++ b/src/GitVersion.Configuration/IgnoreConfiguration.cs @@ -20,7 +20,7 @@ public string? BeforeString IReadOnlySet IIgnoreConfiguration.Paths => Paths; [JsonPropertyName("paths")] - [JsonPropertyDescription("A sequence of file paths to be excluded from the version calculations.")] + [JsonPropertyDescription("A sequence of regular expressions matching file paths to be excluded from the version calculations. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] public HashSet Paths { get; set; } = []; [JsonIgnore] @@ -30,6 +30,12 @@ public string? BeforeString [JsonPropertyDescription("A sequence of SHAs to be excluded from the version calculations.")] public HashSet Shas { get; set; } = []; + IReadOnlySet IIgnoreConfiguration.Tags => Tags; + + [JsonPropertyName("tags")] + [JsonPropertyDescription("A sequence of regular expressions matching friendly tag names to be excluded as version sources. Matching is case-insensitive by default; use (?-i) to enable case-sensitive matching.")] + public HashSet Tags { get; set; } = []; + [JsonIgnore] - public bool IsEmpty => Before == null && Paths.Count == 0 && Shas.Count == 0; + public bool IsEmpty => Before == null && Paths.Count == 0 && Shas.Count == 0 && Tags.Count == 0; } diff --git a/src/GitVersion.Core.Tests/Core/TaggedSemanticVersionRepositoryTests.cs b/src/GitVersion.Core.Tests/Core/TaggedSemanticVersionRepositoryTests.cs new file mode 100644 index 0000000000..5abaa7a23f --- /dev/null +++ b/src/GitVersion.Core.Tests/Core/TaggedSemanticVersionRepositoryTests.cs @@ -0,0 +1,70 @@ +using GitVersion.Configuration; +using GitVersion.Testing.Extensions; + +namespace GitVersion.Tests; + +[TestFixture] +public class TaggedSemanticVersionRepositoryTests : TestBase +{ + [Test] + public void GetTaggedSemanticVersions_IgnoredAndEligibleTagsShareCommit_ReturnsOnlyEligibleTag() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.MakeACommit(); + fixture.ApplyTag("1.0.0"); + fixture.ApplyTag("2.0.0"); + var sut = CreateSut(fixture); + + var actual = GetTagNames(sut, new IgnoreConfiguration { Tags = ["^2\\.0\\.0$"] }); + + actual.ShouldBe(["1.0.0"]); + } + + [Test] + public void GetTaggedSemanticVersions_MultiplePatterns_UsesCaseInsensitiveOrSemanticsOnFriendlyName() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.MakeACommit(); + fixture.ApplyTag("v1.0.0"); + fixture.ApplyTag("v2.0.0"); + fixture.ApplyTag("v3.0.0"); + var sut = CreateSut(fixture); + var ignore = new IgnoreConfiguration { Tags = ["^v1\\.", "^V2\\.", "^refs/tags/v3"] }; + + var actual = GetTagNames(sut, ignore, tagPrefix: "^v"); + + actual.ShouldBe(["v3.0.0"]); + } + + [TestCase(null)] + [TestCase("^preview-")] + public void GetTaggedSemanticVersions_EmptyOrNonMatchingTags_PreservesCandidates(string? expression) + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.MakeATaggedCommit("1.0.0"); + var sut = CreateSut(fixture); + var ignore = new IgnoreConfiguration { Tags = expression is null ? [] : [expression] }; + + var actual = GetTagNames(sut, ignore); + + actual.ShouldBe(["1.0.0"]); + } + + private static TaggedSemanticVersionRepository CreateSut(EmptyRepositoryFixture fixture) + { + var repositoryStore = new RepositoryStore( + NullLogger.Instance, + fixture.Repository.ToGitRepository()); + + return new(NullLogger.Instance, repositoryStore); + } + + private static string[] GetTagNames( + TaggedSemanticVersionRepository sut, + IIgnoreConfiguration ignore, + string tagPrefix = "") + => [.. sut.GetTaggedSemanticVersions(tagPrefix, SemanticVersionFormat.Strict, ignore) + .SelectMany(group => group) + .Select(version => version.Tag.Name.Friendly) + .Order(StringComparer.Ordinal)]; +} diff --git a/src/GitVersion.Core.Tests/IntegrationTests/IgnoreReferenceScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/IgnoreReferenceScenarios.cs new file mode 100644 index 0000000000..15bc7ac2e3 --- /dev/null +++ b/src/GitVersion.Core.Tests/IntegrationTests/IgnoreReferenceScenarios.cs @@ -0,0 +1,35 @@ +using GitVersion.Configuration; +using GitVersion.Testing.Extensions; + +namespace GitVersion.Tests.IntegrationTests; + +[TestFixture] +public class IgnoreReferenceScenarios : TestBase +{ + [Test] + public void GivenIgnoredTagAndEarlierEligibleTag_WhenCalculatingVersion_UsesEarlierTagAndCountsTargetCommit() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.MakeATaggedCommit("1.0.0"); + fixture.Repository.MakeATaggedCommit("2.0.0"); + fixture.Repository.MakeACommit(); + var configuration = GitFlowConfigurationBuilder.New + .WithIgnoreConfiguration(new IgnoreConfiguration { Tags = ["^2\\.0\\.0$"] }) + .Build(); + + fixture.AssertFullSemver("1.0.1-2", configuration); + } + + [Test] + public void GivenNonMatchingTagPattern_WhenCalculatingVersion_PreservesCurrentVersion() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.MakeATaggedCommit("1.0.0"); + fixture.Repository.MakeACommit(); + var configuration = GitFlowConfigurationBuilder.New + .WithIgnoreConfiguration(new IgnoreConfiguration { Tags = ["^preview-"] }) + .Build(); + + fixture.AssertFullSemver("1.0.1-1", configuration); + } +} diff --git a/src/GitVersion.Core/Configuration/IIgnoreConfiguration.cs b/src/GitVersion.Core/Configuration/IIgnoreConfiguration.cs index a8b5903bc0..530c38979e 100644 --- a/src/GitVersion.Core/Configuration/IIgnoreConfiguration.cs +++ b/src/GitVersion.Core/Configuration/IIgnoreConfiguration.cs @@ -12,6 +12,9 @@ public interface IIgnoreConfiguration /// Gets the set of commit SHAs that should be excluded from version calculation. IReadOnlySet Shas { get; } + /// Gets the regular expressions matching tag names that should be excluded as version sources. + IReadOnlySet Tags { get; } + /// Gets a value indicating whether this configuration contains no ignore rules. bool IsEmpty { get; } } diff --git a/src/GitVersion.Core/Core/RepositoryStore.cs b/src/GitVersion.Core/Core/RepositoryStore.cs index f2959ebfb4..61cfbec2ee 100644 --- a/src/GitVersion.Core/Core/RepositoryStore.cs +++ b/src/GitVersion.Core/Core/RepositoryStore.cs @@ -60,7 +60,7 @@ internal class RepositoryStore(ILogger logger, IGitRepository r this.logger.LogInformation("Using latest commit on specified branch"); } - commits = ignore.Filter(commits.ToArray()); + commits = ignore.Filter(commits); return commits.FirstOrDefault(); } diff --git a/src/GitVersion.Core/Core/TaggedSemanticVersionRepository.cs b/src/GitVersion.Core/Core/TaggedSemanticVersionRepository.cs index fca20d6dcb..131626c776 100644 --- a/src/GitVersion.Core/Core/TaggedSemanticVersionRepository.cs +++ b/src/GitVersion.Core/Core/TaggedSemanticVersionRepository.cs @@ -49,7 +49,7 @@ IEnumerable GetElements() { var semanticVersions = GetTaggedSemanticVersions(tagPrefix, format, ignore); - foreach (var commit in ignore.Filter(branch.Commits.ToArray())) + foreach (var commit in ignore.Filter(branch.Commits)) { foreach (var semanticVersion in semanticVersions[commit]) { @@ -89,7 +89,7 @@ public ILookup GetTaggedSemanticVersionsOfMerge using (this.logger.StartIndentedScope($"Getting tagged semantic versions by track merge target '{branch.Name.Canonical}'. " + $"TagPrefix: {tagPrefix} and Format: {format}")) { - var shaHashSet = new HashSet(ignore.Filter(branch.Commits.ToArray()).Select(element => element.Id.Sha)); + var shaHashSet = new HashSet(ignore.Filter(branch.Commits).Select(element => element.Id.Sha)); foreach (var semanticVersion in GetTaggedSemanticVersions(tagPrefix, format, ignore).SelectMany(v => v)) { @@ -125,7 +125,7 @@ IEnumerable GetElements() { this.logger.LogInformation("Getting tagged semantic versions. TagPrefix: {TagPrefix} and Format: {Format}", tagPrefix, format); - foreach (var tag in ignore.Filter(this.repositoryStore.Tags.ToArray())) + foreach (var tag in ignore.Filter(this.repositoryStore.Tags)) { if (SemanticVersion.TryParse(tag.Name.Friendly, tagPrefix, out var semanticVersion, format)) { diff --git a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs index 5bef7de98b..67d8d4bc0c 100644 --- a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs +++ b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs @@ -99,24 +99,29 @@ public IEnumerable ToFilters() } } - public IEnumerable Filter(ITag[] source) + public IEnumerable Filter(IEnumerable source) { ignoreConfig.NotNull(); source.NotNull(); - return !ignoreConfig.IsEmpty ? source.Where(element => ShouldBeIgnored(element.Commit, ignoreConfig)) : source; + return !ignoreConfig.IsEmpty + ? source.Where(element => ShouldBeIncluded(element.Commit, ignoreConfig) && !IsTagIgnored(element.Name.Friendly, ignoreConfig)) + : source; } - public IEnumerable Filter(ICommit[] source) + public IEnumerable Filter(IEnumerable source) { ignoreConfig.NotNull(); source.NotNull(); - return !ignoreConfig.IsEmpty ? source.Where(element => ShouldBeIgnored(element, ignoreConfig)) : source; + return !ignoreConfig.IsEmpty ? source.Where(element => ShouldBeIncluded(element, ignoreConfig)) : source; } } - private static bool ShouldBeIgnored(ICommit commit, IIgnoreConfiguration ignore) + private static bool IsTagIgnored(string tagName, IIgnoreConfiguration ignore) + => ignore.Tags.Any(expression => RegexPatterns.Cache.GetOrAdd(expression).IsMatch(tagName)); + + private static bool ShouldBeIncluded(ICommit commit, IIgnoreConfiguration ignore) => !ignore.ToFilters().Any(filter => filter.Exclude(commit, out _)); extension(EffectiveConfiguration configuration) diff --git a/src/GitVersion.Core/PublicAPI.Unshipped.txt b/src/GitVersion.Core/PublicAPI.Unshipped.txt index c8bb72b678..1bed15ef7d 100644 --- a/src/GitVersion.Core/PublicAPI.Unshipped.txt +++ b/src/GitVersion.Core/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ GitVersion.Configuration.EffectiveConfiguration.CustomVersionFormat.get -> strin GitVersion.Configuration.EffectiveConfiguration.VersionBumpResetMessage.get -> string? GitVersion.Configuration.IBranchConfiguration.CustomVersionFormat.get -> string? GitVersion.Configuration.IGitVersionConfiguration.VersionBumpResetMessage.get -> string? +GitVersion.Configuration.IIgnoreConfiguration.Tags.get -> System.Collections.Generic.IReadOnlySet! GitVersion.VersionCalculation.CommitMessageIncrement GitVersion.VersionCalculation.CommitMessageIncrement.CommitMessageIncrement() -> void GitVersion.VersionCalculation.CommitMessageIncrement.CommitMessageIncrement(GitVersion.VersionField Increment, bool VersionBumpNeedsToBeReset) -> void diff --git a/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MainlineVersionStrategy.cs b/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MainlineVersionStrategy.cs index ce1a3d6c0f..8af7a3e29a 100644 --- a/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MainlineVersionStrategy.cs +++ b/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MainlineVersionStrategy.cs @@ -85,7 +85,7 @@ public IEnumerable GetBaseVersions(EffectiveBranchConfiguration con configuration: branchConfiguration ); - var commitsInReverseOrder = Context.Configuration.Ignore.Filter(Context.CurrentBranchCommits.ToArray()); + var commitsInReverseOrder = Context.Configuration.Ignore.Filter(Context.CurrentBranchCommits); var taggedSemanticVersion = TaggedSemanticVersions.OfBranch; if (branchConfiguration.TrackMergeTarget == true) diff --git a/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MergeMessageVersionStrategy.cs b/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MergeMessageVersionStrategy.cs index 187a2c7ca2..48494d7379 100644 --- a/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MergeMessageVersionStrategy.cs +++ b/src/GitVersion.Core/VersionCalculation/VersionSearchStrategies/MergeMessageVersionStrategy.cs @@ -33,7 +33,7 @@ private IEnumerable GetBaseVersionsInternal(EffectiveBranchConfigur yield break; } - foreach (var commit in configuration.Value.Ignore.Filter(Context.CurrentBranchCommits.ToArray())) + foreach (var commit in configuration.Value.Ignore.Filter(Context.CurrentBranchCommits)) { if (!MergeMessage.TryParse(commit, Context.Configuration, out var mergeMessage) || mergeMessage.Version is null