Skip to content

Commit 4446af3

Browse files
moved globbing into the internals of the EmitDependencyGraphTask (#397)
close #395
1 parent a47284f commit 4446af3

File tree

7 files changed

+118
-66
lines changed

7 files changed

+118
-66
lines changed

src/Incrementalist.Cmd/Commands/EmitDependencyGraphTask.cs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,47 @@ public EmitDependencyGraphTask(BuildSettings settings, MSBuildWorkspace workspac
3838
public MSBuildWorkspace Workspace { get; }
3939

4040
public ILogger Logger { get; }
41+
42+
// Post-process the build result
43+
BuildAnalysisResult FilterBuildResult(BuildAnalysisResult original, IReadOnlyList<SlnFileWithPath> allProjects)
44+
{
45+
var skipGlobs = Settings.SkipGlobs;
46+
var targetGlobs = Settings.TargetGlobs;
4147

42-
public async Task<BuildAnalysisResult> Run()
48+
if (targetGlobs.Count == 0 && skipGlobs.Count == 0)
49+
return original;
50+
51+
var projectsToRebuild = original switch
52+
{
53+
FullSolutionBuildResult full => allProjects.Select(c => c.Path).ToList(),
54+
IncrementalBuildResult incremental => incremental.AffectedProjects,
55+
_ => []
56+
};
57+
58+
// Need to process our globs
59+
60+
// globbing is designed to work with relative paths
61+
var relativePaths = projectsToRebuild.Select(c =>
62+
Settings.WorkingDirectory.ComputeRelativePathToMe(c)).ToList();
63+
64+
// we glob and then convert back into absolute paths
65+
var filteredProjects = GlobFilter.FilterProjects(relativePaths, skipGlobs, targetGlobs)
66+
.Select(c => c.ComputeAbsolutePath(Settings.WorkingDirectory)).ToList();
67+
68+
if (filteredProjects.Count != projectsToRebuild.Count)
69+
{
70+
// had at least 1 hit on a filter
71+
Logger.LogInformation(
72+
"Incrementalist selected {OriginalAffectedProjects} projects for rebuild, after filtering with globs: {FilteredAffectedProjects}",
73+
projectsToRebuild.Count, filteredProjects.Count);
74+
75+
return new IncrementalBuildResult(filteredProjects);
76+
}
77+
78+
return original;
79+
}
80+
81+
private async Task<(BuildAnalysisResult result, IReadOnlyList<SlnFileWithPath> allProjects)> RunInternal()
4382
{
4483
// start the cancellation timer.
4584
_cts.CancelAfter(Settings.TimeoutDuration);
@@ -71,7 +110,7 @@ public async Task<BuildAnalysisResult> Run()
71110
if (affectedFiles.Count == 0)
72111
{
73112
Logger.LogInformation("No files were affected by the changes");
74-
return new IncrementalBuildResult(Array.Empty<AbsolutePath>());
113+
return (new IncrementalBuildResult(Array.Empty<AbsolutePath>()), []);
75114
}
76115

77116
// Log the breakdown of modified files by type
@@ -96,7 +135,7 @@ public async Task<BuildAnalysisResult> Run()
96135
if (importDetector.RequiresFullSolutionBuild(affectedFiles.Keys))
97136
{
98137
Logger.LogInformation("Solution-wide changes detected. Full solution build required");
99-
return new FullSolutionBuildResult(new AbsolutePath(solution.FilePath!));
138+
return (new FullSolutionBuildResult(new AbsolutePath(solution.FilePath!)), projectFiles);
100139
}
101140

102141
// Get the list of affected project files directly
@@ -108,7 +147,7 @@ public async Task<BuildAnalysisResult> Run()
108147
if (directlyAffectedProjects.Count == solution.Projects.Count())
109148
{
110149
Logger.LogInformation("All projects are affected. Full solution build required");
111-
return new FullSolutionBuildResult(new AbsolutePath(solution.FilePath!));
150+
return (new FullSolutionBuildResult(new AbsolutePath(solution.FilePath!)), projectFiles);
112151
}
113152

114153
/* INCREMENTAL BUILDS */
@@ -121,7 +160,7 @@ public async Task<BuildAnalysisResult> Run()
121160
var projectsToRebuild = dependencyGraph.SelectMany(x => x.Value).Distinct().ToList();
122161

123162
// need to write a new cache
124-
return ComputeResult(projectsToRebuild);
163+
return (ComputeResult(projectsToRebuild), projectFiles);
125164

126165
BuildAnalysisResult ComputeResult(IReadOnlyList<AbsolutePath> projectFilePaths)
127166
{
@@ -143,5 +182,14 @@ BuildAnalysisResult ComputeResult(IReadOnlyList<AbsolutePath> projectFilePaths)
143182
return new IncrementalBuildResult(projectFilePaths);
144183
}
145184
}
185+
186+
public async Task<BuildAnalysisResult> Run()
187+
{
188+
var (buildResult, allProjects) = await RunInternal();
189+
190+
// Post-process the build result
191+
var filteredResult = FilterBuildResult(buildResult, allProjects);
192+
return filteredResult;
193+
}
146194
}
147195
}

src/Incrementalist.Cmd/GlobFilter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ public static class GlobFilter
1515
/// <param name="skipGlobs">Filter out any projects that match these glob patterns.</param>
1616
/// <param name="targetGlobs">Only include projects that match these glob patterns.</param>
1717
/// <returns>The final set of filtered project paths.</returns>
18-
public static IReadOnlyList<RelativePath> FilterProjects(IReadOnlyList<RelativePath> originalProjects, string[] skipGlobs, string[] targetGlobs)
18+
public static IReadOnlyList<RelativePath> FilterProjects(IReadOnlyList<RelativePath> originalProjects, IReadOnlyList<string> skipGlobs, IReadOnlyList<string> targetGlobs)
1919
{
20-
if (skipGlobs.Length == 0 && targetGlobs.Length == 0)
20+
if (skipGlobs.Count == 0 && targetGlobs.Count == 0)
2121
return originalProjects;
2222

2323
IEnumerable<RelativePath> currentProjects = originalProjects;
2424

2525
// 1. Apply targetGlobs (inclusion filter)
26-
if (targetGlobs.Length > 0)
26+
if (targetGlobs.Count > 0)
2727
{
2828
var targetMatcher = new Matcher(StringComparison.OrdinalIgnoreCase); // Use case-insensitive matching for file paths
2929
targetMatcher.AddIncludePatterns(targetGlobs);
@@ -32,7 +32,7 @@ public static IReadOnlyList<RelativePath> FilterProjects(IReadOnlyList<RelativeP
3232
}
3333

3434
// 2. Apply skipGlobs (exclusion filter)
35-
if (skipGlobs.Length > 0)
35+
if (skipGlobs.Count > 0)
3636
{
3737
var skipMatcher = new Matcher(StringComparison.OrdinalIgnoreCase);
3838
skipMatcher.AddIncludePatterns(skipGlobs); // Add skip patterns to identify matches for exclusion

src/Incrementalist.Cmd/Program.cs

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ private static async Task AnalyzeFolderDiff(ListFoldersOptions options, Absolute
189189

190190
var settings = new BuildSettings(options.GitBranch!, normalized,
191191
workingFolder,
192+
options.SkipGlobs?.ToArray() ?? [],
193+
options.TargetGlobs?.ToArray() ?? [],
192194
TimeSpan.FromMinutes(options.TimeoutMinutes));
193195
var emitTask = new EmitAffectedFoldersTask(settings, logger);
194196
var affectedFiles = (await emitTask.Run());
@@ -226,11 +228,13 @@ private static async Task ProcessSln(RunOptions options, RelativePath sln, Absol
226228
logger.LogInformation("Starting analysis of solution: {Solution}", sln);
227229

228230
var settings = new BuildSettings(options.GitBranch!, sln, workingFolder,
231+
options.SkipGlobs?.ToArray() ?? [],
232+
options.TargetGlobs?.ToArray() ?? [],
229233
TimeSpan.FromMinutes(options.TimeoutMinutes));
230234

231235
logger.LogInformation("Beginning dependency analysis...");
232236
var emitTask = new EmitDependencyGraphTask(settings, msBuild, logger);
233-
var buildResult = FilterBuildResult(await emitTask.Run());
237+
var buildResult = await emitTask.Run();
234238

235239
var analysisTime = stopwatch.Elapsed;
236240
logger.LogInformation("Solution analysis completed in {Duration:g}", analysisTime);
@@ -251,7 +255,7 @@ private static async Task ProcessSln(RunOptions options, RelativePath sln, Absol
251255

252256
switch (buildResult)
253257
{
254-
case FullSolutionBuildResult _:
258+
case FullSolutionBuildResult:
255259
buildType = "Full solution build";
256260
projectsToRebuild = msBuild.CurrentSolution.Projects.Where(p => p.FilePath is not null)
257261
.Select(p => new AbsolutePath(p.FilePath!)).ToList();
@@ -290,48 +294,6 @@ private static async Task ProcessSln(RunOptions options, RelativePath sln, Absol
290294
logger.LogInformation("{AffectedProjects} affected projects: {AllProjectList}", projectsToRebuild.Count, affectedFilesStr);
291295
}
292296
}
293-
294-
return;
295-
296-
// Post-process the build result
297-
BuildAnalysisResult FilterBuildResult(BuildAnalysisResult original)
298-
{
299-
var skipGlobs = options.SkipGlobs?.ToArray() ?? [];
300-
var targetGlobs = options.TargetGlobs?.ToArray() ?? [];
301-
302-
if (targetGlobs.Length == 0 && skipGlobs.Length == 0)
303-
return original;
304-
305-
var projectsToRebuild = original switch
306-
{
307-
FullSolutionBuildResult full => msBuild.CurrentSolution.Projects.Where(p => p.FilePath is not null)
308-
.Select(p => new AbsolutePath(p.FilePath!)).ToList(),
309-
IncrementalBuildResult incremental => incremental.AffectedProjects,
310-
_ => []
311-
};
312-
313-
// Need to process our globs
314-
315-
// globbing is designed to work with relative paths
316-
var relativePaths = projectsToRebuild.Select(c =>
317-
settings.WorkingDirectory.ComputeRelativePathToMe(c)).ToList();
318-
319-
// we glob and then convert back into absolute paths
320-
var filteredProjects = GlobFilter.FilterProjects(relativePaths, skipGlobs, targetGlobs)
321-
.Select(c => c.ComputeAbsolutePath(settings.WorkingDirectory)).ToList();
322-
323-
if (filteredProjects.Count != projectsToRebuild.Count)
324-
{
325-
// had at least 1 hit on a filter
326-
logger.LogInformation(
327-
"Incrementalist selected {OriginalAffectedProjects} projects for rebuild, after filtering with globs: {FilteredAffectedProjects}",
328-
projectsToRebuild.Count, filteredProjects.Count);
329-
330-
return new IncrementalBuildResult(filteredProjects);
331-
}
332-
333-
return original;
334-
}
335297
}
336298

337299
private static void HandleAffectedFiles(SlnOptions options, string affectedFilesStr, int affectedFilesCount,

src/Incrementalist.Tests/Commands/RunDotNetCommandTaskTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ public RunDotNetCommandTaskTests(ITestOutputHelper outputHelper)
3232

3333
public void Dispose()
3434
{
35-
_repository?.Dispose();
35+
_repository.Dispose();
3636
}
3737

3838
[Fact]
3939
public async Task Should_Execute_Command_Successfully()
4040
{
4141
// Arrange
42-
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath);
42+
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath,[], []);
4343
var projectPath = new AbsolutePath(Path.Combine(_repository.BasePath.Path, "test.csproj"));
4444
await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sdk"">
4545
<PropertyGroup>
@@ -60,7 +60,7 @@ await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sd
6060
public async Task Should_Handle_Failed_Command()
6161
{
6262
// Arrange
63-
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath);
63+
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], []);
6464
var task = new RunDotNetCommandTask(settings, _logger, ["build", "--invalid-option"], true, false);
6565

6666
// Act
@@ -76,7 +76,7 @@ public async Task Should_Handle_Failed_Command()
7676
public async Task Should_Run_Commands_In_Parallel()
7777
{
7878
// Arrange
79-
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath);
79+
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], []);
8080
var projects = new List<AbsolutePath>();
8181
for (int i = 1; i <= 3; i++)
8282
{
@@ -104,7 +104,7 @@ await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sd
104104
public async Task Should_Stop_On_First_Failure_When_ContinueOnError_False()
105105
{
106106
// Arrange
107-
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath);
107+
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], []);
108108
var task = new RunDotNetCommandTask(settings, _logger, ["invalid-command"], false, false);
109109
var projects = new[] { "project1.csproj", "project2.csproj" }.Select(c =>
110110
new AbsolutePath(Path.Combine(_repository.BasePath.Path, c))).ToList();
@@ -120,7 +120,7 @@ public async Task Should_Stop_On_First_Failure_When_ContinueOnError_False()
120120
public async Task Should_Execute_Full_Solution_Build()
121121
{
122122
// Arrange
123-
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath);
123+
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], []);
124124
var solutionPath = new AbsolutePath(Path.Combine(_repository.BasePath.Path, "test.sln"));
125125

126126
// Create a minimal valid solution file
@@ -153,7 +153,7 @@ public async Task Should_Execute_Full_Solution_Build()
153153
public async Task Should_Execute_Full_Slnx_Build()
154154
{
155155
// Arrange
156-
var settings = new BuildSettings("master", new RelativePath("test.slnx"), _repository.BasePath);
156+
var settings = new BuildSettings("master", new RelativePath("test.slnx"), _repository.BasePath, [], []);
157157
var solutionPath = new AbsolutePath(Path.Combine(_repository.BasePath.Path, "test.slnx"));
158158

159159
// Create a minimal valid solution file

src/Incrementalist.Tests/Dependencies/EmitDependencyGraphSpecs.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public EmitDependencyGraphSpecs(ITestOutputHelper outputHelper, MSBuildFixture f
3535
_logger = new TestOutputLogger(outputHelper);
3636
}
3737

38-
private BuildSettings GetBuildSettings() =>
39-
new BuildSettings(PrimaryBranch, _generatedTestSolution.FilePath, Repository.BasePath);
38+
private BuildSettings GetBuildSettings(string[]? skipGlobs = null, string[]? targetGlobs = null) =>
39+
new BuildSettings(PrimaryBranch, _generatedTestSolution.FilePath, Repository.BasePath, skipGlobs ?? [], targetGlobs ?? []);
4040

4141
public const string ProjectBTests = "ProjectB.Tests";
4242
public const string ProjectB = "ProjectB";
@@ -132,6 +132,36 @@ public async Task ShouldDetectSolutionWideChanges(string fileName, string fileCo
132132
Assert.IsType<FullSolutionBuildResult>(result);
133133
}
134134

135+
/// <summary>
136+
/// Reproduction for https://github.com/petabridge/Incrementalist/issues/395
137+
/// </summary>
138+
[Fact]
139+
public async Task ShouldPerformGlobbingOnSolutionWideChanges()
140+
{
141+
// arrange
142+
// will trigger a full rebuild
143+
var newFile = new SampleFile("Directory.Build.props", SolutionFileSamples.DirectoryBuildProps);
144+
Repository
145+
.WriteFile(newFile)
146+
.Commit("Added new file"); // should create the diffs
147+
148+
// should only target the tests project
149+
var buildSettings = GetBuildSettings(["src/**/*.csproj"]);
150+
151+
var cmd = new EmitDependencyGraphTask(buildSettings, _workspace, _logger);
152+
153+
// act
154+
var result = await cmd.Run();
155+
156+
// assert
157+
Assert.NotNull(result);
158+
Assert.IsType<IncrementalBuildResult>(result);
159+
var actualAffectedProjects = ((IncrementalBuildResult)result).AffectedProjects
160+
.Select(c => Path.GetFileNameWithoutExtension(c.Path)).ToList();
161+
var expectedAffectedProjects = new[] { ProjectBTests };
162+
Assert.Equivalent(expectedAffectedProjects, actualAffectedProjects);
163+
}
164+
135165
public Task InitializeAsync()
136166
{
137167
Repository

src/Incrementalist.Tests/Dependencies/FSharpProjectsTrackingSpecs.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task FSharpProjectDiff_should_be_tracked()
4848
.Commit("Updated both project files");
4949

5050
var logger = new TestOutputLogger(_outputHelper);
51-
var settings = new BuildSettings("master", new RelativePath("FSharpSolution.sln"), Repository.BasePath);
51+
var settings = new BuildSettings("master", new RelativePath("FSharpSolution.sln"), Repository.BasePath, [], []);
5252
var emitTask = new EmitDependencyGraphTask(settings, _workspace, logger);
5353
var buildResult = await emitTask.Run();
5454

src/Incrementalist/BuildSettings.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// -----------------------------------------------------------------------
66

77
using System;
8+
using System.Collections.Generic;
89
using System.Diagnostics.Contracts;
910
using Microsoft.Extensions.Logging;
1011

@@ -13,16 +14,17 @@ namespace Incrementalist
1314
/// <summary>
1415
/// The settings used for this execution of incremental build analysis.
1516
/// </summary>
16-
public class BuildSettings
17+
public sealed class BuildSettings
1718
{
1819
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1);
1920

20-
public BuildSettings(string targetBranch, RelativePath solutionFile, AbsolutePath workingDirectory,
21-
TimeSpan? timeoutDuration = null)
21+
public BuildSettings(string targetBranch, RelativePath solutionFile, AbsolutePath workingDirectory, IReadOnlyList<string> skipGlobs, IReadOnlyList<string> targetGlobs, TimeSpan? timeoutDuration = null)
2222
{
2323
TargetBranch = targetBranch;
2424
SolutionFile = solutionFile;
2525
WorkingDirectory = workingDirectory;
26+
SkipGlobs = skipGlobs;
27+
TargetGlobs = targetGlobs;
2628
TimeoutDuration = timeoutDuration ?? DefaultTimeout;
2729
}
2830

@@ -50,5 +52,15 @@ public BuildSettings(string targetBranch, RelativePath solutionFile, AbsolutePat
5052
/// prior to cancelling it.
5153
/// </summary>
5254
public TimeSpan TimeoutDuration { get; }
55+
56+
/// <summary>
57+
/// Globs to skip when searching for project files.
58+
/// </summary>
59+
public IReadOnlyList<string> SkipGlobs { get; }
60+
61+
/// <summary>
62+
/// Exclude all projects that don't match the given globs.
63+
/// </summary>
64+
public IReadOnlyList<string> TargetGlobs { get; }
5365
}
5466
}

0 commit comments

Comments
 (0)