Skip to content

Commit

Permalink
Merge pull request #399 from saul/160-path-filters
Browse files Browse the repository at this point in the history
Add path filtering support to version.json
  • Loading branch information
AArnott committed Jan 20, 2020
2 parents a7d04e1 + f455b01 commit 4079d89
Show file tree
Hide file tree
Showing 11 changed files with 813 additions and 25 deletions.
59 changes: 59 additions & 0 deletions doc/pathFilters.md
@@ -0,0 +1,59 @@
# Path filters

## Problem

Some repositories may contain more than project. This is sometimes referred to as a _mono repo_ (as opposed to having a repo for each project - _many repo_). Imagine a repository structured as:

- /
- Foo/
- version.json => `{"version": "1.0"}`
- Bar/
- version.json => `{"version": "2.1"}`
- Quux/
- version.json => `{"version": "4.3"}`
- README.md

With GitVersioning's default configuration, a commit to a given project's subtree will result in the version height bumping for all projects in the repository. This is typically not desirable. Intuitively, a commit to `Bar` should only cause a version bump for `Bar`, and not `Foo` or `Quux`.

## Solution

Path filters provide a way to filter which subtrees in the repository affect version height. Imagine the `version.json` files had a `pathFilter` property:

```json
{
"version": "1.0",
"pathFilters": ["."]
}
```

With this single path filter of `"."`, the version height for this project would only bump when a commit was made within that subtree. Now imagine all projects in the original example have this value for `pathFilters`. Consider the following commits to the repository, and note their effect on the version height for each project:

| Paths changed | Result |
| ---------------------------------------- | -------------------------------------------------------------------------- |
| `/README.md` | Commit does not affect any project. No versions change. |
| `/Bar/Program.cs`<br>`/Quux/Quux.csproj` | Commit affects both `Bar` and `Quux`. Their patch versions will bump by 1. |
| `/Bar/MyClass.cs` | Commit affects only `Bar`. `Bar`'s patch version will bump by 1. |

When absent, the implied value for `pathFilters` is:

```json
{
"pathFilters": [":/"]
}
```

This results in the entire repository tree being considered for version height calculations. This is the default behavior for GitVersioning.

## Path filter format

Path filters take on a variety of formats, and can specify paths relative to the `version.json` or relative to the root of the repository. See the [Path filter format](#path-filter-format) section for more information.

Multiple path filters may also be specified. The order is irrelevant. After a path matches any non-exclude path filter, it will be run through all exclude path filter. If it matches, the path is ignored.

| Path filter | Description |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `file-here.txt`<br>`./quux.txt`<br>`./sub-dir/foo.txt`<br>`../subdir/inclusion.txt` | File will be included. Path is relative to the `version.json` file. |
| `sub-dir`<br>`../sub-dir` | Directory will be included. Path is relative to the `version.json` file. |
| `:/dir/file.txt` | File will be included. Path is absolute (i.e., relative to the root of the repository). |
| `:!bar.txt`<br>`:^../foo/baz.txt` | File will be excluded. Path is relative to the `version.json` file. `:!` and `:^` prefixes are synonymous. |
| `:!/root-file.txt` | File will be excluded. Path is absolute (i.e., relative to the root of the repository). |
5 changes: 5 additions & 0 deletions doc/versionJson.md
Expand Up @@ -33,6 +33,9 @@ The content of the version.json file is a JSON serialized object with these prop
"nugetPackageVersion": {
"semVer": 1 // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1.
},
"pathFilters": [
// optional list of paths to consider when calculating version height.
]
"publicReleaseRefSpec": [
"^refs/heads/master$", // we release out of master
"^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N
Expand Down Expand Up @@ -70,3 +73,5 @@ The `publicReleaseRefSpec` field causes builds out of certain branches or tags
to automatically drop the `-gabc123` git commit ID suffix from the version, making it
convenient to build releases out of these refs with a friendly version number
that assumes linear versioning.

[Learn more about pathFilters](pathFilters.md).
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -58,6 +58,7 @@ Also some special [cloud build considerations](doc/cloudbuild.md).

This package calculates the version based on a combination of the version.json file,
the git 'height' of the version, and the git commit ID.
The height can optionally be incremented only for those [commits that change certain paths](doc/pathFilters.md).

### Version generation

Expand Down
106 changes: 106 additions & 0 deletions src/NerdBank.GitVersioning.Tests/FilterPathTests.cs
@@ -0,0 +1,106 @@
using System;
using Nerdbank.GitVersioning;
using Xunit;

public class FilterPathTests
{
[Theory]
[InlineData("./", "foo", "foo")]
[InlineData("../relative-dir", "foo", "relative-dir")]
[InlineData("../../some/dir/here", "foo/multi/wow", "foo/some/dir/here")]
[InlineData("relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData("./relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData("./dir\\hi/relativepath.txt", "foo", "foo/dir/hi/relativepath.txt")]
[InlineData(".\\relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":!/absolutepath.txt", "foo", "absolutepath.txt")]
[InlineData(":!\\absolutepath.txt", "foo", "absolutepath.txt")]
[InlineData("../bar/relativepath.txt", "foo", "bar/relativepath.txt")]
[InlineData("/", "foo", "")]
[InlineData("/absolute/file.txt", "foo", "absolute/file.txt")]
[InlineData(":/", "foo", "")]
[InlineData(":/absolutepath.txt", "foo", "absolutepath.txt")]
[InlineData(":/bar/absolutepath.txt", "foo", "bar/absolutepath.txt")]
[InlineData(":\\bar\\absolutepath.txt", "foo", "bar/absolutepath.txt")]
public void CanBeParsedToRepoRelativePath(string pathSpec, string relativeTo, string expected)
{
Assert.Equal(expected, new FilterPath(pathSpec, relativeTo).RepoRelativePath);
}

[Theory]
[InlineData(":!.", "foo", "foo")]
[InlineData(":!.", "foo", "foo/")]
[InlineData(":!.", "foo", "foo/relativepath.txt")]
[InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^./relativepath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^../bar", "foo", "bar")]
[InlineData(":^../bar", "foo", "bar/")]
[InlineData(":^../bar", "foo", "bar/somefile.txt")]
[InlineData(":^/absolute.txt", "foo", "absolute.txt")]
public void PathsCanBeExcluded(string pathSpec, string relativeTo, string repoRelativePath)
{
Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath));
Assert.True(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath));
}

[Theory]
[InlineData(":!.", "foo", "foo.txt")]
[InlineData(":^relativepath.txt", "foo", "foo2/relativepath.txt")]
[InlineData(":^/absolute.txt", "foo", "absolute.txt.bak")]
[InlineData(":^/absolute.txt", "foo", "absolute")]

// Not exclude paths
[InlineData(":/absolute.txt", "foo", "absolute.txt")]
[InlineData("/absolute.txt", "foo", "absolute.txt")]
[InlineData("../root.txt", "foo", "root.txt")]
[InlineData("relativepath.txt", "foo", "foo/relativepath.txt")]
public void NonMatchingPathsAreNotExcluded(string pathSpec, string relativeTo, string repoRelativePath)
{
Assert.False(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath));
Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath));
}

[Theory]
[InlineData(":!.", "foo", "Foo")]
[InlineData(":!.", "foo", "Foo/")]
[InlineData(":!.", "foo", "Foo/relativepath.txt")]
[InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")]
[InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")]
[InlineData(":^../bar", "foo", "Bar")]
[InlineData(":^../bar", "foo", "Bar/")]
[InlineData(":^../bar", "foo", "Bar/SomeFile.txt")]
[InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")]
public void PathsCanBeExcludedCaseInsensitive(string pathSpec, string relativeTo, string repoRelativePath)
{
Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath));
}

[Theory]
[InlineData(":!.", "foo", "Foo")]
[InlineData(":!.", "foo", "Foo/")]
[InlineData(":!.", "foo", "Foo/relativepath.txt")]
[InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")]
[InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")]
[InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")]
[InlineData(":^../bar", "foo", "Bar")]
[InlineData(":^../bar", "foo", "Bar/")]
[InlineData(":^../bar", "foo", "Bar/SomeFile.txt")]
[InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")]
public void NonMatchingPathsAreNotExcludedCaseSensitive(string pathSpec, string relativeTo, string repoRelativePath)
{
Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath));
}

[Fact]
public void InvalidPathspecsThrow()
{
Assert.Throws<ArgumentNullException>(() => new FilterPath(null, ""));
Assert.Throws<ArgumentException>(() => new FilterPath("", ""));
Assert.Throws<FormatException>(() => new FilterPath(":?", ""));
Assert.Throws<FormatException>(() => new FilterPath("../foo.txt", ""));
Assert.Throws<FormatException>(() => new FilterPath(".././a/../../foo.txt", "foo"));
}
}

0 comments on commit 4079d89

Please sign in to comment.