Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #399 from saul/160-path-filters
Add path filtering support to version.json
- Loading branch information
Showing
11 changed files
with
813 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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")); | ||
} | ||
} |
Oops, something went wrong.