Skip to content

Commit

Permalink
Add grouping to pull request (#12)
Browse files Browse the repository at this point in the history
Refactor the handling of the `Groups` property in the `Project` class by introducing a private backing field. This addresses potential issues with default value assignments and validation. Additionally, move the repository branch switching logic inside the group processing loop to ensure the correct branch context is used for each update.

---------

Co-authored-by: Marcin Torba <[email protected]>
  • Loading branch information
Maggus85 and torbacz authored Aug 25, 2024
1 parent 77efdf4 commit 92c78c3
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 78 deletions.
15 changes: 14 additions & 1 deletion DependencyUpdated.Core/Config/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ namespace DependencyUpdated.Core.Config;

public sealed class Project : IValidatableObject
{
private string[] _groups = ["*"];

public ProjectType Type { get; set; }

public string Name { get; set; } = default!;

public string[] DependencyConfigurations { get; set; } = ArraySegment<string>.Empty.ToArray();

public string[] Directories { get; set; } = ArraySegment<string>.Empty.ToArray();


public string[] Groups
{
get => _groups;
set => _groups = value;
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!Enum.IsDefined(Type))
Expand All @@ -28,5 +36,10 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
{
yield return new ValidationResult($"{nameof(Name)} must be provided");
}

if (Groups.Length == 0)
{
yield return new ValidationResult($"Missing ${nameof(Groups)}.");
}
}
}
3 changes: 3 additions & 0 deletions DependencyUpdated.Core/DependencyDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DependencyUpdated.Core;

public record DependencyDetails(string Name, Version Version);
8 changes: 6 additions & 2 deletions DependencyUpdated.Core/IProjectUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
namespace DependencyUpdated.Core;

public interface IProjectUpdater
{
public Task<IReadOnlyCollection<UpdateResult>> UpdateProject(string searchPath, Project projectConfiguration);
{
Task<ICollection<DependencyDetails>> ExtractAllPackagesThatNeedToBeUpdated(string fullPath, Project projectConfiguration);

IEnumerable<string> GetAllProjectFiles(string searchPath);

IReadOnlyCollection<UpdateResult> HandleProjectUpdate(string fullPath, ICollection<DependencyDetails> dependenciesToUpdate);
}
6 changes: 3 additions & 3 deletions DependencyUpdated.Core/IRepositoryProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ public interface IRepositoryProvider
{
public void SwitchToDefaultBranch(string repositoryPath);

public void SwitchToUpdateBranch(string repositoryPath, string projectName);
public void SwitchToUpdateBranch(string repositoryPath, string projectName, string group);

public void CommitChanges(string repositoryPath, string projectName);
public void CommitChanges(string repositoryPath, string projectName, string group);

public Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, string projectName);
public Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, string projectName, string group);
}
93 changes: 43 additions & 50 deletions DependencyUpdated.Projects.DotNet/DotNetUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,24 @@ internal sealed class DotNetUpdater(ILogger logger, IMemoryCache memoryCache) :
"*.nfproj",
"directory.build.props"
];

public async Task<IReadOnlyCollection<UpdateResult>> UpdateProject(string searchPath, Project projectConfiguration)
public IEnumerable<string> GetAllProjectFiles(string searchPath)
{
if (!Path.Exists(searchPath))
{
throw new FileNotFoundException("Search path not found", searchPath);
}

var projectFiles = GetAllProjectFiles(searchPath);
var allUpdates = new List<UpdateResult>();
foreach (var project in projectFiles)
{
var result = await HandleProjectUpdate(project, projectConfiguration);
allUpdates.AddRange(result);
}

return allUpdates;
return ValidDotnetPatterns.SelectMany(dotnetPattern =>
Directory.GetFiles(searchPath, dotnetPattern, SearchOption.AllDirectories));
}

private IEnumerable<string> GetAllProjectFiles(string searchPath)
public IReadOnlyCollection<UpdateResult> HandleProjectUpdate(string fullPath, ICollection<DependencyDetails> dependenciesToUpdate)
{
return ValidDotnetPatterns.SelectMany(dotnetPattern => Directory.GetFiles(searchPath, dotnetPattern, SearchOption.AllDirectories));
logger.Information("Processing: {FullPath} project", fullPath);
return UpdateCsProj(fullPath, dependenciesToUpdate);
}

private async Task<IReadOnlyCollection<UpdateResult>> HandleProjectUpdate(string fullPath, Project projectConfiguration)
public async Task<ICollection<DependencyDetails>> ExtractAllPackagesThatNeedToBeUpdated(string fullPath, Project projectConfiguration)
{
logger.Information("Processing: {FullPath} project", fullPath);
var nugets = ParseCsproj(fullPath);
var nugetsToUpdate = new Dictionary<string, NuGetVersion>();
var returnList = new List<UpdateResult>();

var returnList = new List<DependencyDetails>();
foreach (var nuget in nugets)
{
logger.Verbose("Processing {PackageName}:{PackageVersion}", nuget.Key, nuget.Value);
Expand All @@ -62,36 +50,33 @@ private async Task<IReadOnlyCollection<UpdateResult>> HandleProjectUpdate(string
if (latestVersion.Version > nuget.Value.Version)
{
logger.Information("{PacakgeName} new version {Version} available", nuget.Key, latestVersion);
nugetsToUpdate.Add(nuget.Key, latestVersion);
returnList.Add(new UpdateResult(nuget.Key, nuget.Value.ToNormalizedString(),
latestVersion.ToNormalizedString()));
returnList.Add(new DependencyDetails(nuget.Key, latestVersion.Version));
}
}

UpdateCsProj(fullPath, nugetsToUpdate);
return returnList;
}

private void UpdateCsProj(string fullPath, Dictionary<string, NuGetVersion> nugetsToUpdate)
private IReadOnlyCollection<UpdateResult> UpdateCsProj(string fullPath, ICollection<DependencyDetails> packagesToUpdate)
{
var document = XDocument.Load(fullPath);
var results = new List<UpdateResult>();
var document = XDocument.Load(fullPath, LoadOptions.PreserveWhitespace);

var nugetsToUpdate = packagesToUpdate.ToDictionary(x => x.Name, x => x.Version);

if (document.Root is null)
{
throw new InvalidOperationException("Root object is null");
}

var packageReferences = document.Root.Elements("ItemGroup")
.Elements("PackageReference");

foreach (var packageReference in packageReferences)
{
var includeAttribute = packageReference.Attribute("Include");
var versionAttribute = packageReference.Attribute("Version");
if (includeAttribute is null)
{
continue;
}

if (versionAttribute is null)
if (includeAttribute is null || versionAttribute is null)
{
continue;
}
Expand All @@ -103,7 +88,8 @@ private void UpdateCsProj(string fullPath, Dictionary<string, NuGetVersion> nuge
continue;
}

versionAttribute.SetValue(newVersion.ToNormalizedString());
versionAttribute.SetValue(newVersion.ToString());
results.Add(new UpdateResult(packageName, versionAttribute.Value, newVersion.ToString()));
}

var settings = new XmlWriterSettings
Expand All @@ -112,8 +98,14 @@ private void UpdateCsProj(string fullPath, Dictionary<string, NuGetVersion> nuge
Indent = true,
};

if (results.Count == 0)
{
return results;
}

using var xmlWriter = XmlWriter.Create(fullPath, settings);
document.Save(xmlWriter);
return results;
}

private async Task<NuGetVersion?> GetLatestVersion(string packageId, Project projectConfiguration)
Expand Down Expand Up @@ -148,24 +140,24 @@ private void UpdateCsProj(string fullPath, Dictionary<string, NuGetVersion> nuge
var version = default(NuGetVersion?);
foreach (var repository in repositories)
{
var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
var versions = await findPackageByIdResource.GetAllVersionsAsync(
packageId,
new SourceCacheContext(),
NullLogger.Instance,
CancellationToken.None);
var maxVersion = versions.Where(x => !x.IsPrerelease).Max();
if (version is null || (maxVersion is not null && maxVersion >= version))
{
version = maxVersion;
}
var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
var versions = await findPackageByIdResource.GetAllVersionsAsync(
packageId,
new SourceCacheContext(),
NullLogger.Instance,
CancellationToken.None);
var maxVersion = versions.Where(x => !x.IsPrerelease).Max();
if (version is null || (maxVersion is not null && maxVersion >= version))
{
version = maxVersion;
}
}

memoryCache.Set(packageId, version);
return version;
}

private static Dictionary<string, NuGetVersion> ParseCsproj(string path)
private static Dictionary<string, DependencyDetails> ParseCsproj(string path)
{
var document = XDocument.Load(path);
if (document.Root is null)
Expand All @@ -175,16 +167,17 @@ private static Dictionary<string, NuGetVersion> ParseCsproj(string path)
var packageReferences = document.Root.Elements("ItemGroup")
.Elements("PackageReference");

var nugets = new Dictionary<string, NuGetVersion>();
var nugets = new Dictionary<string, DependencyDetails>();
foreach (var packageReference in packageReferences)
{
var includeAttribute = packageReference.Attribute("Include");
var versionAttribute = packageReference.Attribute("Version");

if (includeAttribute != null && versionAttribute != null)
{
var version = NuGetVersion.Parse(versionAttribute.Value);
nugets[includeAttribute.Value] = version;
var version = NuGetVersion.Parse(versionAttribute.Value).Version;
nugets[includeAttribute.Value] =
new DependencyDetails(includeAttribute.Value, version);
}
}

Expand Down
19 changes: 10 additions & 9 deletions DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public void SwitchToDefaultBranch(string repositoryPath)
}
}

public void SwitchToUpdateBranch(string repositoryPath, string projectName)
public void SwitchToUpdateBranch(string repositoryPath, string projectName, string group)
{
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName);
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName, group);
logger.Information("Switching {Repository} to branch {Branch}", repositoryPath, gitBranchName);
using (var repo = new Repository(repositoryPath))
{
Expand All @@ -53,9 +53,10 @@ public void SwitchToUpdateBranch(string repositoryPath, string projectName)
}
}

public void CommitChanges(string repositoryPath, string projectName)

public void CommitChanges(string repositoryPath, string projectName, string group)
{
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName);
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName, group);
logger.Information("Commiting {Repository} to branch {Branch}", repositoryPath, gitBranchName);
using (var repo = new Repository(repositoryPath))
{
Expand All @@ -80,11 +81,11 @@ public void CommitChanges(string repositoryPath, string projectName)
}
}

public async Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, string projectName)
public async Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, string projectName, string group)
{
var prTitile = "[AutoUpdate] Update dependencies";
var prTitile = $"[AutoUpdate] Update dependencies - {projectName}";
var prDescription = CreatePrDescription(updates);
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName);
var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName, group);
var configValue = config.Value.AzureDevOps;
var sourceBranch = $"refs/heads/{gitBranchName}";
var targetBranch = $"refs/heads/{configValue.TargetBranchName}";
Expand Down Expand Up @@ -176,8 +177,8 @@ private string CreatePrDescription(IReadOnlyCollection<UpdateResult> updates)
return stringBuilder.ToString();
}

private static string CreateGitBranchName(string projectName, string branchName)
private static string CreateGitBranchName(string projectName, string branchName, string group)
{
return $"{projectName.ToLower()}/{branchName.ToLower()}";
return $"{projectName.ToLower()}/{branchName.ToLower()}/{group.ToLower().Replace("*","-asterix-")}";
}
}
63 changes: 50 additions & 13 deletions DependencyUpdated/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Enumeration;
using CommandLine;
using DependencyUpdated.Core;
using DependencyUpdated.Core.Config;
Expand All @@ -17,13 +18,13 @@ public static class Program
private static IConfiguration _configuration = default!;
private static IServiceProvider _serviceProvider = default!;

public static void Main(string[] args)
public static async Task Main(string[] args)
{
Parser.Default.ParseArguments<Options>(args)
.WithParsed(RunApplication);
await Parser.Default.ParseArguments<Options>(args)
.WithParsedAsync(RunApplication);
}

private static void RunApplication(Options options)
private static async Task RunApplication(Options options)
{
Configure(options);
ConfigureServices();
Expand All @@ -47,23 +48,59 @@ private static void RunApplication(Options options)
foreach (var configEntry in config.Value.Projects)
{
var updater = _serviceProvider.GetRequiredKeyedService<IProjectUpdater>(configEntry.Type);

foreach (var project in configEntry.Directories)
{
repositoryProvider.SwitchToDefaultBranch(repositoryPath);
var projectName = configEntry.Name;
repositoryProvider.SwitchToUpdateBranch(repositoryPath, projectName);

var updates = updater.UpdateProject(project, configEntry).Result;
if (!Path.Exists(project))
{
throw new FileNotFoundException("Search path not found", project);
}
var allDepencenciesToUpdate = new List<DependencyDetails>();
var projectFiles = updater.GetAllProjectFiles(project).ToArray();

foreach (var projectFile in projectFiles)
{
var dependencyToUpdate = await updater.ExtractAllPackagesThatNeedToBeUpdated(projectFile, configEntry);
allDepencenciesToUpdate.AddRange(dependencyToUpdate);
}

if (updates.Count == 0)
if (allDepencenciesToUpdate.Count == 0)
{
continue;
}

repositoryProvider.CommitChanges(repositoryPath, projectName);
repositoryProvider.SubmitPullRequest(updates, projectName).Wait();
var uniqueListOfDependencies = allDepencenciesToUpdate.DistinctBy(x => x.Name).ToList();

foreach (var group in configEntry.Groups)
{
var matchesForGroup = uniqueListOfDependencies
.Where(x => FileSystemName.MatchesSimpleExpression(group, x.Name)).ToArray();
uniqueListOfDependencies.RemoveAll(x => FileSystemName.MatchesSimpleExpression(group, x.Name));

if (matchesForGroup.Length == 0)
{
continue;
}

var projectName = configEntry.Name;
repositoryProvider.SwitchToDefaultBranch(repositoryPath);
repositoryProvider.SwitchToUpdateBranch(repositoryPath, projectName, group);

var allUpdates = new List<UpdateResult>();
foreach (var projectFile in projectFiles)
{
var updateResults = updater.HandleProjectUpdate(projectFile, matchesForGroup);
allUpdates.AddRange(updateResults);
}

if (allUpdates.Count != 0)
{
repositoryProvider.CommitChanges(repositoryPath, projectName, group);
repositoryProvider.SubmitPullRequest(allUpdates.DistinctBy(x=>x.PackageName).ToArray(), projectName, group).Wait();
}
}
}
}
}
}

private static void Configure(Options appOptions)
Expand Down
2 changes: 2 additions & 0 deletions DependencyUpdated/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"DependencyConfigurations": [
],
"Directories": [
],
"Groups": [
]
}
]
Expand Down

0 comments on commit 92c78c3

Please sign in to comment.