dependabot-nuget 0.315.0 → 0.316.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Run.cs +1 -1
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +6 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +3 -0
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/ClosePullRequest.cs +15 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/CreatePullRequest.cs +47 -0
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyGroup.cs +60 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +151 -23
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +4 -18
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PullRequestExistsForSecurityUpdate.cs +15 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/SecurityUpdateDependencyNotFound.cs +9 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/SecurityUpdateIgnored.cs +10 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/SecurityUpdateNotFound.cs +11 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/SecurityUpdateNotPossible.cs +13 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UpdatePullRequest.cs +6 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ModifiedFilesTracker.cs +151 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/PullRequestTextGenerator.cs +78 -32
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +99 -111
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandler.cs +169 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/GroupUpdateAllVersionsHandler.cs +271 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/IUpdateHandler.cs +22 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandler.cs +192 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshSecurityUpdatePullRequestHandler.cs +187 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshVersionUpdatePullRequestHandler.cs +175 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationBase.cs +43 -2
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ILogger.cs +17 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +15 -9
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MarkdownListBuilder.cs +65 -0
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/ApiModel/JobTests.cs +405 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/EndToEndTests.cs +92 -82
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/HttpApiHandlerTests.cs +5 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MessageReportTests.cs +67 -1
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs +445 -0
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/PullRequestMessageTests.cs +1 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/PullRequestTextTests.cs +260 -20
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +30 -2
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +69 -10
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandlerTests.cs +766 -0
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/GroupUpdateAllVersionsHandlerTests.cs +636 -0
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandlerTests.cs +513 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/RefreshSecurityUpdatePullRequestHandlerTests.cs +806 -0
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/RefreshVersionUpdatePullRequestHandlerTests.cs +589 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/UpdateHandlerSelectionTests.cs +183 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/UpdateHandlersTestsBase.cs +43 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdatedDependencyListTests.cs +2 -2
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateOperationBaseTests.cs +121 -7
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +6 -0
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +2 -2
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +51 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MarkdownListBuilderTests.cs +42 -0
  51. metadata +26 -4
@@ -0,0 +1,151 @@
1
+ using System.Collections.Immutable;
2
+
3
+ using NuGetUpdater.Core.Discover;
4
+ using NuGetUpdater.Core.Run.ApiModel;
5
+ using NuGetUpdater.Core.Utilities;
6
+
7
+ using static NuGetUpdater.Core.Utilities.EOLHandling;
8
+
9
+ namespace NuGetUpdater.Core.Run;
10
+
11
+ public class ModifiedFilesTracker
12
+ {
13
+ public readonly DirectoryInfo RepoContentsPath;
14
+ private WorkspaceDiscoveryResult? _currentDiscoveryResult = null;
15
+
16
+ private readonly Dictionary<string, string> _originalDependencyFileContents = [];
17
+ private readonly Dictionary<string, EOLType> _originalDependencyFileEOFs = [];
18
+ private readonly Dictionary<string, bool> _originalDependencyFileBOMs = [];
19
+ private string[] _nonProjectFiles = [];
20
+
21
+ public IReadOnlyDictionary<string, string> OriginalDependencyFileContents => _originalDependencyFileContents;
22
+ //public IReadOnlyDictionary<string, EOLType> OriginalDependencyFileEOFs => _originalDependencyFileEOFs;
23
+ public IReadOnlyDictionary<string, bool> OriginalDependencyFileBOMs => _originalDependencyFileBOMs;
24
+
25
+ public ModifiedFilesTracker(DirectoryInfo repoContentsPath)
26
+ {
27
+ RepoContentsPath = repoContentsPath;
28
+ }
29
+
30
+ public async Task StartTrackingAsync(WorkspaceDiscoveryResult discoveryResult)
31
+ {
32
+ if (_currentDiscoveryResult is not null)
33
+ {
34
+ throw new InvalidOperationException("Already tracking modified files.");
35
+ }
36
+
37
+ _currentDiscoveryResult = discoveryResult;
38
+
39
+ // track original contents for later handling
40
+ async Task TrackOriginalContentsAsync(string directory, string fileName)
41
+ {
42
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
43
+ var localFullPath = Path.Join(RepoContentsPath.FullName, repoFullPath);
44
+ var content = await File.ReadAllTextAsync(localFullPath);
45
+ var rawContent = await File.ReadAllBytesAsync(localFullPath);
46
+ _originalDependencyFileContents[repoFullPath] = content;
47
+ _originalDependencyFileEOFs[repoFullPath] = content.GetPredominantEOL();
48
+ _originalDependencyFileBOMs[repoFullPath] = rawContent.HasBOM();
49
+ }
50
+
51
+ foreach (var project in _currentDiscoveryResult.Projects)
52
+ {
53
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
54
+ await TrackOriginalContentsAsync(_currentDiscoveryResult.Path, project.FilePath);
55
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
56
+ {
57
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
58
+ await TrackOriginalContentsAsync(_currentDiscoveryResult.Path, extraFilePath);
59
+ }
60
+ }
61
+
62
+ _nonProjectFiles = new[]
63
+ {
64
+ _currentDiscoveryResult.GlobalJson?.FilePath,
65
+ _currentDiscoveryResult.DotNetToolsJson?.FilePath,
66
+ }.Where(f => f is not null).Cast<string>().ToArray();
67
+ foreach (var nonProjectFile in _nonProjectFiles)
68
+ {
69
+ await TrackOriginalContentsAsync(_currentDiscoveryResult.Path, nonProjectFile);
70
+ }
71
+ }
72
+
73
+ public async Task<ImmutableArray<DependencyFile>> StopTrackingAsync()
74
+ {
75
+ if (_currentDiscoveryResult is null)
76
+ {
77
+ throw new InvalidOperationException("No discovery result to track.");
78
+ }
79
+
80
+ var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
81
+ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
82
+ {
83
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
84
+ var localFullPath = Path.GetFullPath(Path.Join(RepoContentsPath.FullName, repoFullPath));
85
+ var originalContent = _originalDependencyFileContents[repoFullPath];
86
+ var updatedContent = await File.ReadAllTextAsync(localFullPath);
87
+
88
+ updatedContent = updatedContent.SetEOL(_originalDependencyFileEOFs[repoFullPath]);
89
+ var updatedRawContent = updatedContent.SetBOM(_originalDependencyFileBOMs[repoFullPath]);
90
+ await File.WriteAllBytesAsync(localFullPath, updatedRawContent);
91
+
92
+ if (updatedContent != originalContent)
93
+ {
94
+ var reportedContent = updatedContent;
95
+ var encoding = "utf-8";
96
+ if (_originalDependencyFileBOMs[repoFullPath])
97
+ {
98
+ reportedContent = Convert.ToBase64String(updatedRawContent);
99
+ encoding = "base64";
100
+ }
101
+
102
+ updatedDependencyFiles[localFullPath] = new DependencyFile()
103
+ {
104
+ Name = Path.GetFileName(repoFullPath),
105
+ Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
106
+ Content = reportedContent,
107
+ ContentEncoding = encoding,
108
+ };
109
+ }
110
+ }
111
+
112
+ foreach (var project in _currentDiscoveryResult.Projects)
113
+ {
114
+ await AddUpdatedFileIfDifferentAsync(_currentDiscoveryResult.Path, project.FilePath);
115
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
116
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
117
+ {
118
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
119
+ await AddUpdatedFileIfDifferentAsync(_currentDiscoveryResult.Path, extraFilePath);
120
+ }
121
+ }
122
+
123
+ foreach (var nonProjectFile in _nonProjectFiles)
124
+ {
125
+ await AddUpdatedFileIfDifferentAsync(_currentDiscoveryResult.Path, nonProjectFile);
126
+ }
127
+
128
+ _currentDiscoveryResult = null;
129
+
130
+ var updatedDependencyFileList = updatedDependencyFiles
131
+ .OrderBy(kvp => kvp.Key)
132
+ .Select(kvp => kvp.Value)
133
+ .ToImmutableArray();
134
+ return updatedDependencyFileList;
135
+ }
136
+
137
+ public static ImmutableArray<DependencyFile> MergeUpdatedFileSet(ImmutableArray<DependencyFile> setA, ImmutableArray<DependencyFile> setB)
138
+ {
139
+ static string GetFullName(DependencyFile df) => Path.Join(df.Directory, df.Name).NormalizePathToUnix();
140
+ var finalSet = setA.ToDictionary(GetFullName, df => df);
141
+ foreach (var dependencyFile in setB)
142
+ {
143
+ finalSet[GetFullName(dependencyFile)] = dependencyFile;
144
+ }
145
+
146
+ return finalSet
147
+ .OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
148
+ .Select(kvp => kvp.Value)
149
+ .ToImmutableArray();
150
+ }
151
+ }
@@ -1,10 +1,12 @@
1
1
  using System.Collections.Immutable;
2
-
3
- using NuGet.Versioning;
2
+ using System.Text;
3
+ using System.Text.RegularExpressions;
4
4
 
5
5
  using NuGetUpdater.Core.Run.ApiModel;
6
6
  using NuGetUpdater.Core.Updater;
7
7
 
8
+ using DependencySet = (string Name, (NuGet.Versioning.NuGetVersion? OldVersion, NuGet.Versioning.NuGetVersion NewVersion)[] Versions);
9
+
8
10
  namespace NuGetUpdater.Core.Run;
9
11
 
10
12
  public class PullRequestTextGenerator
@@ -13,20 +15,58 @@ public class PullRequestTextGenerator
13
15
 
14
16
  public static string GetPullRequestTitle(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
15
17
  {
16
- // simple version looks like
17
- // Update Some.Package to 1.2.3
18
- // if multiple packages are updated to multiple versions, result looks like:
19
- // Update Package.A to 1.0.0, 2.0.0; Package.B to 3.0.0, 4.0.0
20
- var dependencySets = GetDependencySets(updateOperationsPerformed);
21
- var updatedPartTitles = dependencySets
22
- .Select(d => $"{d.Name} to {string.Join(", ", d.Versions.Select(v => v.ToString()))}")
23
- .ToArray();
24
- var title = $"{job.CommitMessageOptions?.Prefix}Update {string.Join("; ", updatedPartTitles)}";
18
+ var shortTitle = GetPullRequestShortTitle(job, updateOperationsPerformed, dependencyGroupName);
19
+ var titlePrefix = GetPullRequestTitlePrefix(job);
20
+ var fullTitle = $"{titlePrefix}{shortTitle}";
21
+ return fullTitle;
22
+ }
25
23
 
26
- // don't let the title get too long
27
- if (title.Length > MaxTitleLength && updatedPartTitles.Length >= 3)
24
+ private static string GetPullRequestTitlePrefix(Job job)
25
+ {
26
+ if (string.IsNullOrEmpty(job.CommitMessageOptions?.Prefix))
28
27
  {
29
- title = $"{job.CommitMessageOptions?.Prefix}Update {dependencySets[0].Name} and {dependencySets.Length - 1} other dependencies";
28
+ return string.Empty;
29
+ }
30
+
31
+ var prefix = job.CommitMessageOptions?.Prefix ?? string.Empty;
32
+ if (Regex.IsMatch(prefix, @"[a-z0-9\)\]]$", RegexOptions.IgnoreCase))
33
+ {
34
+ prefix += ":";
35
+ }
36
+
37
+ if (!prefix.EndsWith(" "))
38
+ {
39
+ prefix += " ";
40
+ }
41
+
42
+ return prefix;
43
+ }
44
+
45
+ private static string GetPullRequestShortTitle(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
46
+ {
47
+ string title;
48
+ var dependencySets = GetDependencySets(updateOperationsPerformed);
49
+ if (dependencyGroupName is not null)
50
+ {
51
+ title = $"Bump the {dependencyGroupName} group with {dependencySets.Length} update{(dependencySets.Length > 1 ? "s" : "")}";
52
+ }
53
+ else
54
+ {
55
+ if (dependencySets.Length == 1)
56
+ {
57
+ title = GetDependencySetBumpText(dependencySets[0], isCommitMessageDetail: false);
58
+ }
59
+ else
60
+ {
61
+ var dependencyNames = dependencySets.Select(d => d.Name).Distinct().OrderBy(n => n).ToArray();
62
+ title = $"Bump {string.Join(", ", dependencyNames.Take(dependencyNames.Length - 1))} and {dependencyNames[^1]}";
63
+
64
+ // don't let the title get too long
65
+ if (title.Length > MaxTitleLength && dependencyNames.Length >= 3)
66
+ {
67
+ title = $"Bump {dependencyNames[0]} and {dependencyNames.Length - 1} others";
68
+ }
69
+ }
30
70
  }
31
71
 
32
72
  return title;
@@ -34,28 +74,33 @@ public class PullRequestTextGenerator
34
74
 
35
75
  public static string GetPullRequestCommitMessage(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
36
76
  {
37
- // updating a single dependency looks like
38
- // Update Some.Package to 1.2.3
39
- // if multiple packages are updated, result looks like:
40
- // Update:
41
- // - Package.A to 1.0.0
42
- // - Package.B to 2.0.0
77
+ var sb = new StringBuilder();
78
+ sb.AppendLine(GetPullRequestTitle(job, updateOperationsPerformed, dependencyGroupName));
43
79
  var dependencySets = GetDependencySets(updateOperationsPerformed);
44
- if (dependencySets.Length == 1)
80
+ if (dependencySets.Length > 1 ||
81
+ dependencyGroupName is not null)
45
82
  {
46
- var depName = dependencySets[0].Name;
47
- var depVersions = dependencySets[0].Versions.Select(v => v.ToString());
48
- return $"Update {dependencySets[0].Name} to {string.Join(", ", depVersions)}";
83
+ // multiple updates performed, enumerate them
84
+ sb.AppendLine();
85
+ foreach (var dependencySet in dependencySets)
86
+ {
87
+ sb.AppendLine(GetDependencySetBumpText(dependencySet, isCommitMessageDetail: true));
88
+ }
49
89
  }
50
90
 
51
- var updatedParts = dependencySets
52
- .Select(d => $"- {d.Name} to {string.Join(", ", d.Versions.Select(v => v.ToString()))}")
53
- .ToArray();
54
- var message = string.Join("\n", ["Update:", .. updatedParts]);
55
- return message;
91
+ return sb.ToString().Replace("\r", "").TrimEnd();
92
+ }
93
+
94
+ private static string GetDependencySetBumpText(DependencySet dependencySet, bool isCommitMessageDetail)
95
+ {
96
+ var bumpSuffix = isCommitMessageDetail ? "s" : string.Empty; // "Bumps" for commit message details, "Bump" otherwise
97
+ var fromText = dependencySet.Versions.Length == 1 && dependencySet.Versions[0].OldVersion is not null
98
+ ? $"from {dependencySet.Versions[0].OldVersion} "
99
+ : string.Empty;
100
+ return $"Bump{bumpSuffix} {dependencySet.Name} {fromText}to {string.Join(", ", dependencySet.Versions.Select(v => v.NewVersion.ToString()))}";
56
101
  }
57
102
 
58
- private static (string Name, NuGetVersion[] Versions)[] GetDependencySets(ImmutableArray<UpdateOperationBase> updateOperationsPerformed)
103
+ private static DependencySet[] GetDependencySets(ImmutableArray<UpdateOperationBase> updateOperationsPerformed)
59
104
  {
60
105
  var dependencySets = updateOperationsPerformed
61
106
  .GroupBy(d => d.DependencyName, StringComparer.OrdinalIgnoreCase)
@@ -64,8 +109,9 @@ public class PullRequestTextGenerator
64
109
  {
65
110
  var name = g.Key;
66
111
  var versions = g
67
- .Select(d => d.NewVersion)
68
- .OrderBy(v => v)
112
+ .OrderBy(d => d.OldVersion)
113
+ .ThenBy(d => d.NewVersion)
114
+ .Select(d => (d.OldVersion, d.NewVersion))
69
115
  .ToArray();
70
116
  return (name, versions);
71
117
  })
@@ -1,4 +1,5 @@
1
1
  using System.Collections.Immutable;
2
+ using System.IO;
2
3
  using System.Text;
3
4
  using System.Text.Json;
4
5
  using System.Text.Json.Serialization;
@@ -10,11 +11,10 @@ using NuGet.Versioning;
10
11
  using NuGetUpdater.Core.Analyze;
11
12
  using NuGetUpdater.Core.Discover;
12
13
  using NuGetUpdater.Core.Run.ApiModel;
14
+ using NuGetUpdater.Core.Run.UpdateHandlers;
13
15
  using NuGetUpdater.Core.Updater;
14
16
  using NuGetUpdater.Core.Utilities;
15
17
 
16
- using static NuGetUpdater.Core.Utilities.EOLHandling;
17
-
18
18
  namespace NuGetUpdater.Core.Run;
19
19
 
20
20
  public class RunWorker
@@ -47,17 +47,74 @@ public class RunWorker
47
47
  {
48
48
  var jobFileContent = await File.ReadAllTextAsync(jobFilePath.FullName);
49
49
  var jobWrapper = Deserialize(jobFileContent);
50
- var result = await RunAsync(jobWrapper.Job, repoContentsPath, baseCommitSha);
51
- var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
52
- await File.WriteAllTextAsync(outputFilePath.FullName, resultJson);
50
+ var experimentsManager = ExperimentsManager.GetExperimentsManager(jobWrapper.Job.Experiments);
51
+ var result = await RunAsync(jobWrapper.Job, repoContentsPath, baseCommitSha, experimentsManager);
52
+ if (experimentsManager.UseLegacyUpdateHandler)
53
+ {
54
+ // only the legacy handler writes this file
55
+ var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
56
+ await File.WriteAllTextAsync(outputFilePath.FullName, resultJson);
57
+ }
53
58
  }
54
59
 
55
- public Task<RunResult> RunAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha)
60
+ public async Task<RunResult> RunAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha, ExperimentsManager experimentsManager)
56
61
  {
57
- return RunWithErrorHandlingAsync(job, repoContentsPath, baseCommitSha);
62
+ RunResult result;
63
+ if (experimentsManager.UseLegacyUpdateHandler)
64
+ {
65
+ result = await RunWithErrorHandlingAsync(job, repoContentsPath, baseCommitSha, experimentsManager);
66
+ }
67
+ else
68
+ {
69
+ await RunScenarioHandlersWithErrorHandlingAsync(job, repoContentsPath, baseCommitSha, experimentsManager);
70
+
71
+ // the group updater doesn't return this, so we provide an empty object
72
+ result = new RunResult()
73
+ {
74
+ Base64DependencyFiles = [],
75
+ BaseCommitSha = baseCommitSha,
76
+ };
77
+ }
78
+
79
+ return result;
58
80
  }
59
81
 
60
- private async Task<RunResult> RunWithErrorHandlingAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha)
82
+ private static readonly ImmutableArray<IUpdateHandler> UpdateHandlers =
83
+ [
84
+ GroupUpdateAllVersionsHandler.Instance,
85
+ RefreshGroupUpdatePullRequestHandler.Instance,
86
+ CreateSecurityUpdatePullRequestHandler.Instance,
87
+ RefreshSecurityUpdatePullRequestHandler.Instance,
88
+ RefreshVersionUpdatePullRequestHandler.Instance,
89
+ ];
90
+
91
+ public static IUpdateHandler GetUpdateHandler(Job job) =>
92
+ UpdateHandlers.FirstOrDefault(h => h.CanHandle(job)) ?? throw new InvalidOperationException("Unable to find appropriate update handler.");
93
+
94
+ private async Task RunScenarioHandlersWithErrorHandlingAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha, ExperimentsManager experimentsManager)
95
+ {
96
+ JobErrorBase? error = null;
97
+
98
+ try
99
+ {
100
+ var handler = GetUpdateHandler(job);
101
+ _logger.Info($"Starting update job of type {handler.TagName}");
102
+ await handler.HandleAsync(job, repoContentsPath, baseCommitSha, _discoveryWorker, _analyzeWorker, _updaterWorker, _apiHandler, experimentsManager, _logger);
103
+ }
104
+ catch (Exception ex)
105
+ {
106
+ error = JobErrorBase.ErrorFromException(ex, _jobId, repoContentsPath.FullName);
107
+ }
108
+
109
+ if (error is not null)
110
+ {
111
+ await _apiHandler.RecordUpdateJobError(error);
112
+ }
113
+
114
+ await _apiHandler.MarkAsProcessed(new(baseCommitSha));
115
+ }
116
+
117
+ private async Task<RunResult> RunWithErrorHandlingAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha, ExperimentsManager experimentsManager)
61
118
  {
62
119
  JobErrorBase? error = null;
63
120
  var currentDirectory = repoContentsPath.FullName; // used for error reporting below
@@ -71,7 +128,6 @@ public class RunWorker
71
128
  {
72
129
  MSBuildHelper.RegisterMSBuild(repoContentsPath.FullName, repoContentsPath.FullName, _logger);
73
130
 
74
- var experimentsManager = ExperimentsManager.GetExperimentsManager(job.Experiments);
75
131
  var allDependencyFiles = new Dictionary<string, DependencyFile>();
76
132
  foreach (var directory in job.GetAllDirectories())
77
133
  {
@@ -109,9 +165,7 @@ public class RunWorker
109
165
  private async Task<RunResult> RunForDirectory(Job job, DirectoryInfo repoContentsPath, string repoDirectory, string baseCommitSha, ExperimentsManager experimentsManager)
110
166
  {
111
167
  var discoveryResult = await _discoveryWorker.RunAsync(repoContentsPath.FullName, repoDirectory);
112
-
113
- _logger.Info("Discovery JSON content:");
114
- _logger.Info(JsonSerializer.Serialize(discoveryResult, DiscoveryWorker.SerializerOptions));
168
+ _logger.ReportDiscovery(discoveryResult);
115
169
 
116
170
  if (discoveryResult.Error is not null)
117
171
  {
@@ -125,50 +179,18 @@ public class RunWorker
125
179
  }
126
180
 
127
181
  // report dependencies
128
- var discoveredUpdatedDependencies = GetUpdatedDependencyListFromDiscovery(discoveryResult, repoContentsPath.FullName);
182
+ var discoveredUpdatedDependencies = GetUpdatedDependencyListFromDiscovery(discoveryResult);
129
183
  await _apiHandler.UpdateDependencyList(discoveredUpdatedDependencies);
130
184
 
131
185
  var incrementMetric = GetIncrementMetric(job);
132
186
  await _apiHandler.IncrementMetric(incrementMetric);
133
187
 
134
188
  // TODO: pull out relevant dependencies, then check each for updates and track the changes
135
- var originalDependencyFileContents = new Dictionary<string, string>();
136
- var originalDependencyFileEOFs = new Dictionary<string, EOLType>();
137
- var originalDependencyFileBOMs = new Dictionary<string, bool>();
138
189
  var actualUpdatedDependencies = new List<ReportedDependency>();
139
190
 
140
191
  // track original contents for later handling
141
- async Task TrackOriginalContentsAsync(string directory, string fileName)
142
- {
143
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
144
- var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
145
- var content = await File.ReadAllTextAsync(localFullPath);
146
- var rawContent = await File.ReadAllBytesAsync(localFullPath);
147
- originalDependencyFileContents[repoFullPath] = content;
148
- originalDependencyFileEOFs[repoFullPath] = content.GetPredominantEOL();
149
- originalDependencyFileBOMs[repoFullPath] = rawContent.HasBOM();
150
- }
151
-
152
- foreach (var project in discoveryResult.Projects)
153
- {
154
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
155
- await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
156
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
157
- {
158
- var extraFilePath = Path.Join(projectDirectory, extraFile);
159
- await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath);
160
- }
161
- }
162
-
163
- var nonProjectFiles = new[]
164
- {
165
- discoveryResult.GlobalJson?.FilePath,
166
- discoveryResult.DotNetToolsJson?.FilePath,
167
- }.Where(f => f is not null).Cast<string>().ToArray();
168
- foreach (var nonProjectFile in nonProjectFiles)
169
- {
170
- await TrackOriginalContentsAsync(discoveryResult.Path, nonProjectFile);
171
- }
192
+ var tracker = new ModifiedFilesTracker(repoContentsPath);
193
+ await tracker.StartTrackingAsync(discoveryResult);
172
194
 
173
195
  // do update
174
196
  var updateOperationsPerformed = new List<UpdateOperationBase>();
@@ -210,8 +232,7 @@ public class RunWorker
210
232
 
211
233
  var dependencyInfo = GetDependencyInfo(job, dependency);
212
234
  var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
213
- _logger.Info("Analysis content:");
214
- _logger.Info(JsonSerializer.Serialize(analysisResult, AnalyzeWorker.SerializerOptions));
235
+ _logger.ReportAnalysis(analysisResult);
215
236
 
216
237
  if (analysisResult.Error is not null)
217
238
  {
@@ -227,7 +248,7 @@ public class RunWorker
227
248
  .FirstOrDefault(d => d.Name.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase));
228
249
  if (updatedDependencyFromAnalysis is not null)
229
250
  {
230
- var existingPullRequest = job.GetExistingPullRequestForDependency(updatedDependencyFromAnalysis);
251
+ var existingPullRequest = job.GetExistingPullRequestForDependencies([updatedDependencyFromAnalysis], considerVersions: true);
231
252
  if (existingPullRequest is not null)
232
253
  {
233
254
  await SendApiMessage(new PullRequestExistsForLatestVersion(dependency.Name, analysisResult.UpdatedVersion));
@@ -261,6 +282,7 @@ public class RunWorker
261
282
  PreviousRequirements = previousDependency.Requirements,
262
283
  };
263
284
 
285
+ var projectDiscovery = discoveryResult.GetProjectDiscoveryFromPath(updateOperation.ProjectPath);
264
286
  var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, updateOperation.ProjectPath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: dependency.IsTransitive);
265
287
  if (updateResult.Error is not null)
266
288
  {
@@ -271,70 +293,19 @@ public class RunWorker
271
293
  actualUpdatedDependencies.Add(updatedDependency);
272
294
  }
273
295
 
274
- updateOperationsPerformed.AddRange(updateResult.UpdateOperations);
296
+ var patchedUpdateOperations = PatchInOldVersions(updateResult.UpdateOperations, projectDiscovery);
297
+ updateOperationsPerformed.AddRange(patchedUpdateOperations);
275
298
  }
276
299
  }
277
300
 
278
301
  // create PR - we need to manually check file contents; we can't easily use `git status` in tests
279
- var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
280
- async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
281
- {
282
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
283
- var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath));
284
- var originalContent = originalDependencyFileContents[repoFullPath];
285
- var updatedContent = await File.ReadAllTextAsync(localFullPath);
286
-
287
- updatedContent = updatedContent.SetEOL(originalDependencyFileEOFs[repoFullPath]);
288
- var updatedRawContent = updatedContent.SetBOM(originalDependencyFileBOMs[repoFullPath]);
289
- await File.WriteAllBytesAsync(localFullPath, updatedRawContent);
290
-
291
- if (updatedContent != originalContent)
292
- {
293
- var reportedContent = updatedContent;
294
- var encoding = "utf-8";
295
- if (originalDependencyFileBOMs[repoFullPath])
296
- {
297
- reportedContent = Convert.ToBase64String(updatedRawContent);
298
- encoding = "base64";
299
- }
300
-
301
- updatedDependencyFiles[localFullPath] = new DependencyFile()
302
- {
303
- Name = Path.GetFileName(repoFullPath),
304
- Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
305
- Content = reportedContent,
306
- ContentEncoding = encoding,
307
- };
308
- }
309
- }
310
-
311
- foreach (var project in discoveryResult.Projects)
312
- {
313
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
314
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
315
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
316
- {
317
- var extraFilePath = Path.Join(projectDirectory, extraFile);
318
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath);
319
- }
320
- }
321
-
322
- foreach (var nonProjectFile in nonProjectFiles)
323
- {
324
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, nonProjectFile);
325
- }
326
-
327
- var updatedDependencyFileList = updatedDependencyFiles
328
- .OrderBy(kvp => kvp.Key)
329
- .Select(kvp => kvp.Value)
330
- .ToArray();
331
-
302
+ var updatedDependencyFiles = await tracker.StopTrackingAsync();
332
303
  var normalizedUpdateOperationsPerformed = UpdateOperationBase.NormalizeUpdateOperationCollection(repoContentsPath.FullName, updateOperationsPerformed);
333
304
  var report = UpdateOperationBase.GenerateUpdateOperationReport(normalizedUpdateOperationsPerformed);
334
305
  _logger.Info(report);
335
306
 
336
307
  var sortedUpdatedDependencies = actualUpdatedDependencies.OrderBy(d => d.Name, StringComparer.OrdinalIgnoreCase).ToArray();
337
- var resultMessage = GetPullRequestApiMessage(job, updatedDependencyFileList, sortedUpdatedDependencies, normalizedUpdateOperationsPerformed, baseCommitSha);
308
+ var resultMessage = GetPullRequestApiMessage(job, [.. updatedDependencyFiles], sortedUpdatedDependencies, normalizedUpdateOperationsPerformed, baseCommitSha);
338
309
  switch (resultMessage)
339
310
  {
340
311
  case ClosePullRequest close:
@@ -366,11 +337,11 @@ public class RunWorker
366
337
 
367
338
  var result = new RunResult()
368
339
  {
369
- Base64DependencyFiles = originalDependencyFileContents.OrderBy(kvp => kvp.Key).Select(kvp =>
340
+ Base64DependencyFiles = tracker.OriginalDependencyFileContents.OrderBy(kvp => kvp.Key).Select(kvp =>
370
341
  {
371
342
  var fullPath = kvp.Key.FullyNormalizedRootedPath();
372
343
  var rawContent = Encoding.UTF8.GetBytes(kvp.Value);
373
- if (originalDependencyFileBOMs[kvp.Key])
344
+ if (tracker.OriginalDependencyFileBOMs[kvp.Key])
374
345
  {
375
346
  rawContent = Encoding.UTF8.GetPreamble().Concat(rawContent).ToArray();
376
347
  }
@@ -388,6 +359,22 @@ public class RunWorker
388
359
  return result;
389
360
  }
390
361
 
362
+ internal static ImmutableArray<UpdateOperationBase> PatchInOldVersions(ImmutableArray<UpdateOperationBase> updateOperations, ProjectDiscoveryResult? projectDiscovery)
363
+ {
364
+ if (projectDiscovery is null)
365
+ {
366
+ return updateOperations;
367
+ }
368
+
369
+ var originalPackageVersions = projectDiscovery
370
+ .Dependencies
371
+ .ToDictionary(d => d.Name, d => d.Version is null ? null : NuGetVersion.Parse(d.Version), StringComparer.OrdinalIgnoreCase);
372
+ var patchedUpdateOperations = updateOperations
373
+ .Select(uo => uo with { OldVersion = originalPackageVersions.GetValueOrDefault(uo.DependencyName) })
374
+ .ToImmutableArray();
375
+ return patchedUpdateOperations;
376
+ }
377
+
391
378
  private async Task SendApiMessage(MessageBase? message)
392
379
  {
393
380
  if (message is null)
@@ -433,7 +420,7 @@ public class RunWorker
433
420
  if (existingPullRequest is null && updatedFiles.Length == 0)
434
421
  {
435
422
  // it's possible that we were asked to update a specific package, but it's no longer there; in that case find _that_ specific PR
436
- var requestedUpdates = (job.Dependencies ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
423
+ var requestedUpdates = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
437
424
  existingPullRequest = existingPullRequests.FirstOrDefault(pr => pr.Item2.Select(d => d.DependencyName).All(requestedUpdates.Contains));
438
425
  }
439
426
 
@@ -489,6 +476,7 @@ public class RunWorker
489
476
  CommitMessage = PullRequestTextGenerator.GetPullRequestCommitMessage(job, updateOperationsPerformed, dependencyGroupName: null),
490
477
  PrTitle = PullRequestTextGenerator.GetPullRequestTitle(job, updateOperationsPerformed, dependencyGroupName: null),
491
478
  PrBody = PullRequestTextGenerator.GetPullRequestBody(job, updateOperationsPerformed, dependencyGroupName: null),
479
+ DependencyGroup = null,
492
480
  };
493
481
  }
494
482
  }
@@ -638,7 +626,7 @@ public class RunWorker
638
626
  // not vulnerable => no longer needed
639
627
  var specificJobDependencies = job.SecurityAdvisories
640
628
  .Select(a => a.DependencyName)
641
- .Concat(job.Dependencies ?? [])
629
+ .Concat(job.Dependencies)
642
630
  .ToHashSet(StringComparer.OrdinalIgnoreCase);
643
631
  if (specificJobDependencies.Contains(dependency.Name))
644
632
  {
@@ -652,7 +640,7 @@ public class RunWorker
652
640
  {
653
641
  // not a security update, so only update if...
654
642
  // ...we've been explicitly asked to update this
655
- if ((job.Dependencies ?? []).Any(d => d.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)))
643
+ if (job.Dependencies.Any(d => d.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)))
656
644
  {
657
645
  return true;
658
646
  }
@@ -708,7 +696,7 @@ public class RunWorker
708
696
  return dependencyInfo;
709
697
  }
710
698
 
711
- internal static UpdatedDependencyList GetUpdatedDependencyListFromDiscovery(WorkspaceDiscoveryResult discoveryResult, string pathToContents)
699
+ internal static UpdatedDependencyList GetUpdatedDependencyListFromDiscovery(WorkspaceDiscoveryResult discoveryResult)
712
700
  {
713
701
  string GetFullRepoPath(string path)
714
702
  {