Skip to content

Commit

Permalink
feat: exclude projects (#82)
Browse files Browse the repository at this point in the history
Adds feature to exclude projects to CLI using `--exclude`.

Co-authored-by: xIceFox <[email protected]>
  • Loading branch information
leonardochaia and xIceFox authored Jun 15, 2023
1 parent 206f946 commit 84c86fb
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 15 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,39 @@ WRITE: /home/lchaia/dev/dotnet-affected/affected.proj
WRITE: /home/lchaia/dev/dotnet-affected/affected.json
```

## Excluding Projects

Projects can be excluded by using the `--exclude` (shorthand `-e`) argument. It expects a dotnet Regular Expression
that will be matched against each Project's Full Path.

In the below example, `dotnet-affected.Tests` is excluded due to the regular expression provided.
```shell
$ dotnet affected --dry-run --verbose -e .Tests.
1 files have changed referenced by 1 projects
0 NuGet Packages have changed
1 projects are affected by these changes
1 projects were excluded
Changed Projects
Name Path
dotnet-affected /home/lchaia/dev/dotnet-affected/src/dotnet-affected/dotnet-affected.csproj

Affected Projects
Name Path
dotnet-affected.Benchmarks /home/lchaia/dev/dotnet-affected/benchmarks/dotnet-affected.Benchmarks/dotnet-affected.Benchmarks.csproj

Excluded Projects
Name Path
dotnet-affected.Tests /home/lchaia/dev/dotnet-affected/test/dotnet-affected.Tests/dotnet-affected.Tests.csproj
DRY-RUN: WRITE /home/lchaia/dev/dotnet-affected/affected.proj
DRY-RUN: CONTENTS:
<Project Sdk="Microsoft.Build.Traversal/3.0.3">
<ItemGroup>
<ProjectReference Include="/home/lchaia/dev/dotnet-affected/benchmarks/dotnet-affected.Benchmarks/dotnet-affected.Benchmarks.csproj" />
<ProjectReference Include="/home/lchaia/dev/dotnet-affected/src/dotnet-affected/dotnet-affected.csproj" />
</ItemGroup>
</Project>
```

## Continuous Integration

For usage in CI, it's recommended to use the `--from` and `--to` options with the environment variables provided by your
Expand Down
8 changes: 8 additions & 0 deletions src/DotnetAffected.Abstractions/AffectedSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ public class AffectedSummary
/// <param name="filesThatChanged"></param>
/// <param name="projectsWithChangedFiles"></param>
/// <param name="affectedProjects"></param>
/// <param name="excludedProjects"></param>
/// <param name="changedPackages"></param>
public AffectedSummary(
string[] filesThatChanged,
ProjectGraphNode[] projectsWithChangedFiles,
ProjectGraphNode[] affectedProjects,
ProjectGraphNode[] excludedProjects,
PackageChange[] changedPackages)
{
FilesThatChanged = filesThatChanged;
ProjectsWithChangedFiles = projectsWithChangedFiles;
AffectedProjects = affectedProjects;
ExcludedProjects = excludedProjects;
ChangedPackages = changedPackages;
}

Expand All @@ -41,6 +44,11 @@ public AffectedSummary(
/// </summary>
public ProjectGraphNode[] AffectedProjects { get; }

/// <summary>
/// Gets a list of projects that had changes or were affected but were excluded from discovery.
/// </summary>
public ProjectGraphNode[] ExcludedProjects { get; }

/// <summary>
/// Gets the list of packages that changed.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/DotnetAffected.Core/AffectedOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ public class AffectedOptions : IDiscoveryOptions
/// <param name="solutionPath"></param>
/// <param name="fromRef"></param>
/// <param name="toRef"></param>
/// <param name="exclusionRegex"></param>
public AffectedOptions(
string? repositoryPath = null,
string? solutionPath = null,
string? fromRef = null,
string? toRef = null)
string? toRef = null,
string? exclusionRegex = null)
{
RepositoryPath = DetermineRepositoryPath(repositoryPath, solutionPath);
SolutionPath = solutionPath;
FromRef = fromRef ?? string.Empty;
ToRef = toRef ?? string.Empty;
ExclusionRegex = exclusionRegex;
}

/// <summary>
Expand All @@ -48,6 +51,11 @@ public AffectedOptions(
/// </summary>
public string ToRef { get; }

/// <summary>
/// Gets the regular expression to use for excluding projects.
/// </summary>
public string? ExclusionRegex { get; }

private static string DetermineRepositoryPath(string? repositoryPath, string? solutionPath)
{
// the argument takes precedence.
Expand Down
61 changes: 54 additions & 7 deletions src/DotnetAffected.Core/Processor/AffectedProcessorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Build.Graph;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace DotnetAffected.Core.Processor
{
Expand All @@ -18,27 +19,41 @@ internal abstract class AffectedProcessorBase
public AffectedSummary Process(AffectedProcessorContext context)
{
// Get files that changed according to changes provider.
context.ChangedFiles = DiscoverChangedFiles(context).ToArray();
context.ChangedFiles = DiscoverChangedFiles(context)
.ToArray();

// Map the files that changed to their corresponding project/s.
context.ChangedProjects = DiscoverProjectsForFiles(context).ToArray();
var excludedProjects = new List<ProjectGraphNode>();
context.ChangedProjects = ApplyExclusionPattern(
DiscoverProjectsForFiles(context),
context.Options,
excludedProjects);

// Get packages that have changed, either from central package management or from the project file
context.ChangedPackages = DiscoverPackageChanges(context);

// Determine which projects are affected by the projects and packages that have changed.
context.AffectedProjects = DiscoverAffectedProjects(context);
context.AffectedProjects = ApplyExclusionPattern(
DiscoverAffectedProjects(context),
context.Options,
excludedProjects);

// Output a summary of the operation.
return new AffectedSummary(context.ChangedFiles, context.ChangedProjects, context.AffectedProjects, context.ChangedPackages);
return new AffectedSummary(
context.ChangedFiles,
context.ChangedProjects,
context.AffectedProjects,
excludedProjects.Distinct()
.ToArray(),
context.ChangedPackages);
}

/// <summary>
/// Discover which files have changes
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected virtual IEnumerable<string> DiscoverChangedFiles(AffectedProcessorContext context)
protected virtual IEnumerable<string> DiscoverChangedFiles(AffectedProcessorContext context)
=> context.ChangesProvider.GetChangedFiles(context.RepositoryPath, context.FromRef, context.ToRef);

/// <summary>
Expand All @@ -49,11 +64,43 @@ protected virtual IEnumerable<string> DiscoverChangedFiles(AffectedProcessorCont
protected virtual IEnumerable<ProjectGraphNode> DiscoverProjectsForFiles(AffectedProcessorContext context)
{
// We init now because we want the graph to initialize late (lazy)
var provider = context.ChangedProjectsProvider ?? new PredictionChangedProjectsProvider(context.Graph, context.Options);
var provider = context.ChangedProjectsProvider ??
new PredictionChangedProjectsProvider(context.Graph, context.Options);
// Match which files belong to which of our known projects
return provider.GetReferencingProjects(context.ChangedFiles);
}

/// <summary>
/// Applies the <see cref="AffectedOptions.ExclusionRegex"/> to exclude
/// projects that matches the regular expression.
/// </summary>
/// <param name="inputProjects">List of projects that changed.</param>
/// <param name="options">Affected options.</param>
/// <param name="excludedProjects">Collection of excluded projects</param>
/// <returns>Project lis excluding the ones that matches the exclusion regex.</returns>
protected virtual ProjectGraphNode[] ApplyExclusionPattern(
IEnumerable<ProjectGraphNode> inputProjects,
AffectedOptions options,
ICollection<ProjectGraphNode> excludedProjects)
{
var pattern = options.ExclusionRegex;

if (string.IsNullOrEmpty(pattern))
return inputProjects.ToArray();

var changedProjects = new List<ProjectGraphNode>();
var regex = new Regex(pattern);
foreach (var project in inputProjects)
{
if (regex.IsMatch(project.GetFullPath()))
excludedProjects.Add(project);
else
changedProjects.Add(project);
}

return changedProjects.ToArray();
}

/// <summary>
/// Discover which packages have changed. <br/>
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/dotnet-affected/Commands/AffectedGlobalOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ internal static class AffectedGlobalOptions
description: "A branch or commit to compare against --to.");

public static readonly ToOption ToOption = new(FromOption);

public static readonly Option<string> ExclusionRegexOption = new(
new[]
{
"--exclude", "-e"
},
description: "A dotnet Regular Expression used to exclude discovered and affected projects.");
}

internal sealed class ToOption : Option<string>
Expand Down
3 changes: 2 additions & 1 deletion src/dotnet-affected/Commands/AffectedRootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public AffectedRootCommand()
this.AddGlobalOption(AffectedGlobalOptions.AssumeChangesOption);
this.AddGlobalOption(AffectedGlobalOptions.FromOption);
this.AddGlobalOption(AffectedGlobalOptions.ToOption);
this.AddGlobalOption(AffectedGlobalOptions.ExclusionRegexOption);

this.AddOption(FormatOption);
this.AddOption(DryRunOption);
Expand Down Expand Up @@ -74,7 +75,7 @@ public FormatOption()
})
{
this.Description = "Space-seperated output file formats. Possible values: <traversal, text, json>.";

this.SetDefaultValue(new[]
{
"traversal"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ protected override AffectedOptions GetBoundValue(BindingContext bindingContext)
parseResult.GetValueForOption(AffectedGlobalOptions.RepositoryPathOptions),
parseResult.GetValueForOption(AffectedGlobalOptions.SolutionPathOption),
parseResult.GetValueForOption(AffectedGlobalOptions.FromOption),
parseResult.GetValueForOption(AffectedGlobalOptions.ToOption)
parseResult.GetValueForOption(AffectedGlobalOptions.ToOption),
parseResult.GetValueForOption(AffectedGlobalOptions.ExclusionRegexOption)
);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/dotnet-affected/Views/AffectedInfoView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public AffectedInfoView(AffectedSummary summary)
$"referenced by {summary.ProjectsWithChangedFiles.Count()} projects"));
Add(new ContentView($"{summary.ChangedPackages.Count()} NuGet Packages have changed"));
Add(new ContentView($"{summary.AffectedProjects.Count()} projects are affected by these changes"));
Add(new ContentView($"{summary.ExcludedProjects.Count()} projects were excluded"));

Add(new WithChangesAndAffectedView(summary));
}
Expand Down
13 changes: 8 additions & 5 deletions src/dotnet-affected/Views/WithChangesAndAffectedView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ public WithChangesAndAffectedView(AffectedSummary summary)

Add(new ContentView("\nAffected Projects"));

if (!summary.AffectedProjects.Any())
{
if (summary.AffectedProjects.Any())
Add(new ProjectInfoTable(summary.AffectedProjects));
else
Add(new ContentView("No projects where affected by any of the changed projects."));
return;
}

Add(new ProjectInfoTable(summary.AffectedProjects));
if (summary.ExcludedProjects.Any())
{
Add(new ContentView("\nExcluded Projects"));
Add(new ProjectInfoTable(summary.ExcludedProjects));
}
}
}
}
Loading

0 comments on commit 84c86fb

Please sign in to comment.