Skip to content

Commit 0347da5

Browse files
authored
Merge pull request #1397 from dotnet/aarnott-nbgv-path-filters-command
feat: Implement nbgv path-filters command for monorepo pathFilters automation
2 parents f111c01 + 181b1a9 commit 0347da5

6 files changed

Lines changed: 540 additions & 4 deletions

File tree

docfx/docs/nbgv-cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,6 @@ usage: nbgv <command> [<args>]
250250
prepare-release Prepares a release by creating a release branch for
251251
the current version and adjusting the version on the
252252
current branch.
253+
path-filters Manages the pathFilters property in version.json files
254+
based on MSBuild project references and imports.
253255
```

docfx/docs/path-filters.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,138 @@ Multiple path filters may also be specified. The order is irrelevant. After a pa
5757
| `/root-file.txt`<br>`:/dir/file.txt` | File will be included. Path is absolute (i.e., relative to the root of the repository). |
5858
| `:!bar.txt`<br>`:^../foo/baz.txt` | File will be excluded. Path is relative to the `version.json` file. `:!` and `:^` prefixes are synonymous. |
5959
| `:!/root-file.txt` | File will be excluded. Path is absolute (i.e., relative to the root of the repository). |
60+
61+
## Managing path filters with `nbgv path-filters`
62+
63+
For repositories with multiple projects and version.json files, manually maintaining `pathFilters` can be error-prone. The `nbgv path-filters` command automates this process by analyzing your MSBuild project structure and computing the correct path filters based on project references and shared build files.
64+
65+
### When to use path-filters command
66+
67+
Use the `nbgv path-filters` command in the following scenarios:
68+
69+
- **Monorepo with multiple projects** - You have multiple projects in different directories, each with their own `version.json`
70+
- **Complex project dependencies** - Projects reference each other, and you need path filters to reflect these dependencies
71+
- **Shared build files** - You use `Directory.Build.props` or other shared MSBuild imports that should be tracked by multiple projects
72+
- **Maintaining accuracy** - You want to ensure path filters automatically stay in sync with your project structure
73+
74+
### How it works
75+
76+
The `nbgv path-filters` command uses the MSBuild project graph API to:
77+
78+
1. **Discover project files** - Finds all MSBuild project files (`.csproj`, `.vbproj`, etc.) associated with each `version.json`
79+
2. **Compute transitive dependencies** - Uses the MSBuild project graph to determine the complete set of projects that each project depends on
80+
3. **Include shared build files** - Identifies MSBuild imports like `Directory.Build.props` that reside within the repository
81+
4. **Respect boundaries** - Stops searching for projects at directories containing their own `version.json` files, ensuring clean separation of concerns
82+
5. **Generate path filters** - Converts the discovered projects and files into appropriate `pathFilters` entries
83+
84+
### Important behaviors
85+
86+
- **Only processes version.json files with projects** - A `version.json` file with no associated MSBuild projects is skipped and left unchanged
87+
- **Respects version.json hierarchy** - When searching for projects under a `version.json`, the search stops at subdirectories that have their own `version.json` files
88+
- **Includes project directories** - Path filters include entire project directories (e.g., `/ProjectA`) rather than individual `.csproj` files so that all source changes under those directories result in a new version of the project
89+
- **Filters ignored files** - Automatically excludes files that match `.gitignore` patterns (including generated directories like `obj/` and `bin/`)
90+
91+
### Usage
92+
93+
#### Check current path filters
94+
95+
To see what path filters should be present without making changes:
96+
97+
```ps1
98+
nbgv path-filters check
99+
```
100+
101+
This command will:
102+
- Compare the computed path filters against what's currently in each `version.json`
103+
- Display mismatches (missing or extra filters)
104+
- Exit with non-zero code if any mismatches are found (useful for CI validation)
105+
106+
#### Update path filters
107+
108+
To automatically compute and update all `version.json` files:
109+
110+
```ps1
111+
nbgv path-filters update
112+
```
113+
114+
This command will:
115+
- Compute the correct path filters for each `version.json`
116+
- Update each file that needs changes
117+
- Display which files were updated
118+
- Skip any `version.json` files that have no associated projects
119+
120+
#### Specify which version.json files to process
121+
122+
By default, both commands search from the current directory. You can specify specific paths:
123+
124+
```ps1
125+
nbgv path-filters check ./src/ProjectA ./src/ProjectB
126+
```
127+
128+
#### Include additional project file extensions
129+
130+
By default, the tool searches for `.csproj` and `.vbproj` files. You can include other extensions:
131+
132+
```ps1
133+
nbgv path-filters update --ext .fsproj --ext .csproj
134+
```
135+
136+
### Example
137+
138+
Consider a monorepo with this structure:
139+
140+
```
141+
/
142+
version.json (version: "1.0")
143+
Directory.Build.props
144+
/ProjectA
145+
version.json (version: "2.0")
146+
ProjectA.csproj
147+
/ProjectB
148+
version.json (version: "3.0")
149+
ProjectB.csproj
150+
(ProjectB.csproj references ProjectA.csproj)
151+
```
152+
153+
Running `nbgv path-filters update` would produce:
154+
155+
**Root version.json** - Left unchanged (has no projects directly under it)
156+
157+
**ProjectA/version.json**:
158+
```json
159+
{
160+
"version": "2.0",
161+
"pathFilters": [
162+
"/ProjectA",
163+
"/Directory.Build.props"
164+
]
165+
}
166+
```
167+
168+
**ProjectB/version.json**:
169+
```json
170+
{
171+
"version": "3.0",
172+
"pathFilters": [
173+
"/ProjectA",
174+
"/ProjectB",
175+
"/Directory.Build.props"
176+
]
177+
}
178+
```
179+
180+
Note that ProjectB's filters include ProjectA because ProjectB depends on it. Any change to ProjectA's source files will now correctly trigger a version bump for ProjectB as well.
181+
182+
### CI Integration
183+
184+
You can use the `path-filters check` command in your CI pipeline to validate that `pathFilters` are correctly maintained:
185+
186+
```ps1
187+
nbgv path-filters check
188+
if ($LASTEXITCODE -ne 0) {
189+
Write-Error "Path filters are out of date. Run 'nbgv path-filters update' locally."
190+
exit 1
191+
}
192+
```
193+
194+
This ensures that developers keep path filters in sync with project structure changes.

src/NerdBank.GitVersioning/GitContext.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ public static GitContext Create(string path, string? committish = null, Engine e
228228
}
229229
}
230230

231+
/// <summary>
232+
/// Determines whether a file would be ignored by git based on common .gitignore patterns.
233+
/// </summary>
234+
/// <param name="path">The absolute file path to check.</param>
235+
/// <returns>True if the file is ignored by git; false otherwise.</returns>
236+
public virtual bool IsIgnored(string path) => false;
237+
231238
/// <inheritdoc />
232239
public void Dispose()
233240
{
@@ -294,7 +301,7 @@ internal static bool TryFindGitPaths(string? path, [NotNullWhen(true)] out strin
294301

295302
internal abstract Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight);
296303

297-
internal string GetRepoRelativePath(string absolutePath)
304+
internal string GetRepoRelativePath(string absolutePath, bool replaceBackslashes = false)
298305
{
299306
string? repoRoot = this.WorkingTreePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
300307

@@ -303,8 +310,9 @@ internal string GetRepoRelativePath(string absolutePath)
303310
throw new ArgumentException($"Path '{absolutePath}' is not within repository '{repoRoot}'", nameof(absolutePath));
304311
}
305312

306-
return absolutePath.Substring(repoRoot.Length)
313+
string result = absolutePath.Substring(repoRoot.Length)
307314
.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
315+
return replaceBackslashes ? result.Replace('\\', '/') : result;
308316
}
309317

310318
/// <summary>

src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ public static LibGit2Context Create(string path, string? committish = null)
8888
};
8989
}
9090

91+
/// <inheritdoc />
92+
public override bool IsIgnored(string path) => this.Repository.Ignore.IsPathIgnored(this.GetRepoRelativePath(path, replaceBackslashes: true));
93+
9194
/// <inheritdoc />
9295
public override void ApplyTag(string name) => this.Repository.Tags.Add(name, this.Commit);
9396

0 commit comments

Comments
 (0)