dependabot-nuget 0.302.0 → 0.303.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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Packages.props +5 -5
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscoveryTargetingPacks.props +10 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +96 -97
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +2 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +3 -0
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +1 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +8 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +7 -4
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +2 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +257 -37
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +12 -3
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationBase.cs +209 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationResult.cs +3 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +79 -24
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +11 -11
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +19 -5
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +24 -6
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackageReferenceUpdaterTests.cs +177 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateOperationBaseTests.cs +130 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +5 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +71 -5
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +87 -3
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +23 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +145 -147
  26. metadata +11 -7
@@ -1,9 +1,12 @@
1
1
  using System.Collections.Immutable;
2
+ using System.Text.Json;
2
3
 
3
4
  using Microsoft.Language.Xml;
4
5
 
6
+ using NuGet.Frameworks;
5
7
  using NuGet.Versioning;
6
8
 
9
+ using NuGetUpdater.Core.Updater;
7
10
  using NuGetUpdater.Core.Utilities;
8
11
 
9
12
  namespace NuGetUpdater.Core;
@@ -21,7 +24,7 @@ namespace NuGetUpdater.Core;
21
24
  /// </remarks>
22
25
  internal static class PackageReferenceUpdater
23
26
  {
24
- public static async Task UpdateDependencyAsync(
27
+ public static async Task<IEnumerable<UpdateOperationBase>> UpdateDependencyAsync(
25
28
  string repoRootPath,
26
29
  string projectPath,
27
30
  string dependencyName,
@@ -60,45 +63,67 @@ internal static class PackageReferenceUpdater
60
63
 
61
64
  if (!await DoesDependencyRequireUpdateAsync(repoRootPath, projectPath, tfms, topLevelDependencies, dependencyName, newDependencyVersion, experimentsManager, logger))
62
65
  {
63
- return;
66
+ return [];
64
67
  }
65
68
 
69
+ var updateOperations = new List<UpdateOperationBase>();
66
70
  var peerDependencies = await GetUpdatedPeerDependenciesAsync(repoRootPath, projectPath, tfms, dependencyName, newDependencyVersion, experimentsManager, logger);
67
71
  if (experimentsManager.UseLegacyDependencySolver)
68
72
  {
69
73
  if (isTransitive)
70
74
  {
71
- await UpdateTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, buildFiles, experimentsManager, logger);
75
+ var updatedFiles = await UpdateTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, buildFiles, experimentsManager, logger);
76
+ updateOperations.Add(new PinnedUpdate()
77
+ {
78
+ DependencyName = dependencyName,
79
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
80
+ UpdatedFiles = [.. updatedFiles],
81
+ });
72
82
  }
73
83
  else
74
84
  {
75
85
  if (peerDependencies is null)
76
86
  {
77
- return;
87
+ return updateOperations;
78
88
  }
79
89
 
80
- await UpdateTopLevelDepdendency(repoRootPath, buildFiles, tfms, dependencyName, previousDependencyVersion, newDependencyVersion, peerDependencies, experimentsManager, logger);
90
+ var topLevelUpdateOperations = await UpdateTopLevelDepdendency(repoRootPath, buildFiles, tfms, dependencyName, previousDependencyVersion, newDependencyVersion, peerDependencies, experimentsManager, logger);
91
+ updateOperations.AddRange(topLevelUpdateOperations);
81
92
  }
82
93
  }
83
94
  else
84
95
  {
85
96
  if (peerDependencies is null)
86
97
  {
87
- return;
98
+ return updateOperations;
88
99
  }
89
100
 
90
- await UpdateDependencyWithConflictResolution(repoRootPath, buildFiles, tfms, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, peerDependencies, experimentsManager, logger);
101
+ var conflictResolutionUpdateOperations = await UpdateDependencyWithConflictResolution(
102
+ repoRootPath,
103
+ buildFiles,
104
+ tfms,
105
+ projectPath,
106
+ dependencyName,
107
+ previousDependencyVersion,
108
+ newDependencyVersion,
109
+ isTransitive,
110
+ peerDependencies,
111
+ experimentsManager,
112
+ logger);
113
+ updateOperations.AddRange(conflictResolutionUpdateOperations);
91
114
  }
92
115
 
93
116
  if (!await AreDependenciesCoherentAsync(repoRootPath, projectPath, dependencyName, buildFiles, tfms, experimentsManager, logger))
94
117
  {
95
- return;
118
+ // should we return an empty set because we failed?
119
+ return updateOperations;
96
120
  }
97
121
 
98
122
  await SaveBuildFilesAsync(buildFiles, logger);
123
+ return updateOperations;
99
124
  }
100
125
 
101
- public static async Task UpdateDependencyWithConflictResolution(
126
+ public static async Task<IEnumerable<UpdateOperationBase>> UpdateDependencyWithConflictResolution(
102
127
  string repoRootPath,
103
128
  ImmutableArray<ProjectBuildFile> buildFiles,
104
129
  string[] targetFrameworks,
@@ -111,17 +136,20 @@ internal static class PackageReferenceUpdater
111
136
  ExperimentsManager experimentsManager,
112
137
  ILogger logger)
113
138
  {
114
- var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray();
139
+ var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToImmutableArray();
115
140
  var isDependencyTopLevel = topLevelDependencies.Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase));
116
- var dependenciesToUpdate = new[] { new Dependency(dependencyName, newDependencyVersion, DependencyType.PackageReference) };
141
+ var dependenciesToUpdate = new[] { new Dependency(dependencyName, newDependencyVersion, DependencyType.PackageReference) }.ToImmutableArray();
142
+ var updateOperations = new List<UpdateOperationBase>();
117
143
 
118
144
  // update the initial dependency...
119
- TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger);
145
+ var (_, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger);
146
+ updateOperations.AddRange(updateOperationsPerformed);
120
147
 
121
148
  // ...and the peer dependencies...
122
149
  foreach (var (packageName, packageVersion) in peerDependencies.Where(kvp => string.Compare(kvp.Key, dependencyName, StringComparison.OrdinalIgnoreCase) != 0))
123
150
  {
124
- TryUpdateDependencyVersion(buildFiles, packageName, previousDependencyVersion: null, newDependencyVersion: packageVersion, logger);
151
+ (_, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, packageName, previousDependencyVersion: null, newDependencyVersion: packageVersion, logger);
152
+ updateOperations.AddRange(updateOperationsPerformed);
125
153
  }
126
154
 
127
155
  // ...and everything else
@@ -136,21 +164,164 @@ internal static class PackageReferenceUpdater
136
164
  continue;
137
165
  }
138
166
 
139
- var isDependencyInResolutionSet = resolvedDependencies.Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase));
167
+ var isDependencyInResolutionSet = resolvedDependencies.Value.Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase));
140
168
  if (isTransitive && !isDependencyTopLevel && isDependencyInResolutionSet)
141
169
  {
142
170
  // a transitive dependency had to be pinned; add it here
143
- await UpdateTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, buildFiles, experimentsManager, logger);
171
+ var updatedFiles = await UpdateTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, buildFiles, experimentsManager, logger);
144
172
  }
145
173
 
146
174
  // update all resolved dependencies that aren't the initial dependency
147
- foreach (var resolvedDependency in resolvedDependencies
175
+ foreach (var resolvedDependency in resolvedDependencies.Value
148
176
  .Where(d => !d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))
149
177
  .Where(d => d.Version is not null))
150
178
  {
151
- TryUpdateDependencyVersion(buildFiles, resolvedDependency.Name, previousDependencyVersion: null, newDependencyVersion: resolvedDependency.Version!, logger);
179
+ (_, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, resolvedDependency.Name, previousDependencyVersion: null, newDependencyVersion: resolvedDependency.Version!, logger);
180
+ updateOperations.AddRange(updateOperationsPerformed);
181
+ }
182
+
183
+ updateOperationsPerformed = await ComputeUpdateOperations(repoRootPath, projectPath, tfm, topLevelDependencies, dependenciesToUpdate, resolvedDependencies.Value, experimentsManager, logger);
184
+ updateOperations.AddRange(updateOperationsPerformed.Select(u => u with { UpdatedFiles = [projectFile.Path] }));
185
+ }
186
+ }
187
+
188
+ return updateOperations;
189
+ }
190
+
191
+ internal static async Task<IEnumerable<UpdateOperationBase>> ComputeUpdateOperations(
192
+ string repoRoot,
193
+ string projectPath,
194
+ string targetFramework,
195
+ ImmutableArray<Dependency> topLevelDependencies,
196
+ ImmutableArray<Dependency> requestedUpdates,
197
+ ImmutableArray<Dependency> resolvedDependencies,
198
+ ExperimentsManager experimentsManager,
199
+ ILogger logger
200
+ )
201
+ {
202
+ var topLevelNames = topLevelDependencies.Select(d => d.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
203
+ var requestedVersions = requestedUpdates.ToDictionary(d => d.Name, d => NuGetVersion.Parse(d.Version!), StringComparer.OrdinalIgnoreCase);
204
+ var resolvedVersions = resolvedDependencies
205
+ .Select(d => (d.Name, NuGetVersion.TryParse(d.Version, out var version), version))
206
+ .Where(d => d.Item2)
207
+ .ToDictionary(d => d.Item1, d => d.Item3!, StringComparer.OrdinalIgnoreCase);
208
+
209
+ var (packageParents, packageVersions) = await GetPackageGraphForDependencies(repoRoot, projectPath, targetFramework, resolvedDependencies, experimentsManager, logger);
210
+ var updateOperations = new List<UpdateOperationBase>();
211
+ foreach (var (requestedDependencyName, requestedDependencyVersion) in requestedVersions)
212
+ {
213
+ var isDependencyTopLevel = topLevelNames.Contains(requestedDependencyName);
214
+ var isDependencyInResolvedSet = resolvedVersions.ContainsKey(requestedDependencyName);
215
+ switch ((isDependencyTopLevel, isDependencyInResolvedSet))
216
+ {
217
+ case (true, true):
218
+ // direct update performed
219
+ var resolvedVer = resolvedVersions[requestedDependencyName];
220
+ updateOperations.Add(new DirectUpdate()
221
+ {
222
+ DependencyName = requestedDependencyName,
223
+ NewVersion = resolvedVer,
224
+ UpdatedFiles = [],
225
+ });
226
+ break;
227
+ case (false, true):
228
+ // pinned transitive update
229
+ updateOperations.Add(new PinnedUpdate()
230
+ {
231
+ DependencyName = requestedDependencyName,
232
+ NewVersion = resolvedVersions[requestedDependencyName],
233
+ UpdatedFiles = [],
234
+ });
235
+ break;
236
+ case (false, false):
237
+ // walk the first parent all the way up to find a top-level dependency that resulted in the desired change
238
+ string? rootPackageName = null;
239
+ var currentPackageName = requestedDependencyName;
240
+ while (packageParents.TryGetValue(currentPackageName, out var parentSet))
241
+ {
242
+ currentPackageName = parentSet.First();
243
+ if (topLevelNames.Contains(currentPackageName))
244
+ {
245
+ rootPackageName = currentPackageName;
246
+ break;
247
+ }
248
+ }
249
+
250
+ if (rootPackageName is not null)
251
+ {
252
+ updateOperations.Add(new ParentUpdate()
253
+ {
254
+ DependencyName = requestedDependencyName,
255
+ NewVersion = requestedVersions[requestedDependencyName],
256
+ UpdatedFiles = [],
257
+ ParentDependencyName = rootPackageName,
258
+ ParentNewVersion = packageVersions[rootPackageName],
259
+ });
260
+ }
261
+ break;
262
+ case (true, false):
263
+ // dependency is top-level, but not in the resolved versions; this can happen if an unrelated package has a wildcard
264
+ break;
265
+ }
266
+ }
267
+
268
+ return [.. updateOperations];
269
+ }
270
+
271
+ private static async Task<(Dictionary<string, HashSet<string>> PackageParents, Dictionary<string, NuGetVersion> PackageVersions)> GetPackageGraphForDependencies(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> topLevelDependencies, ExperimentsManager experimentsManager, ILogger logger)
272
+ {
273
+ var packageParents = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
274
+ var packageVersions = new Dictionary<string, NuGetVersion>(StringComparer.OrdinalIgnoreCase);
275
+ var tempDir = Directory.CreateTempSubdirectory("_package_graph_for_dependencies_");
276
+ try
277
+ {
278
+ // generate project.assets.json
279
+ var parsedTargetFramework = NuGetFramework.Parse(targetFramework);
280
+ var tempProject = await MSBuildHelper.CreateTempProjectAsync(tempDir, repoRoot, projectPath, targetFramework, topLevelDependencies, experimentsManager, logger, importDependencyTargets: !experimentsManager.UseDirectDiscovery);
281
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["build", tempProject, "/t:_ReportDependencies"], tempDir.FullName, experimentsManager);
282
+ var assetsJsonPath = Path.Join(tempDir.FullName, "obj", "project.assets.json");
283
+ var assetsJsonContent = await File.ReadAllTextAsync(assetsJsonPath);
284
+
285
+ // build reverse dependency graph
286
+ var assets = JsonDocument.Parse(assetsJsonContent).RootElement;
287
+ foreach (var tfmObject in assets.GetProperty("targets").EnumerateObject())
288
+ {
289
+ var reportedTargetFramework = NuGetFramework.Parse(tfmObject.Name);
290
+ if (reportedTargetFramework != parsedTargetFramework)
291
+ {
292
+ // not interested in this target framework
293
+ continue;
294
+ }
295
+
296
+ foreach (var parentObject in tfmObject.Value.EnumerateObject())
297
+ {
298
+ var parts = parentObject.Name.Split('/');
299
+ var parentName = parts[0];
300
+ var parentVersion = parts[1];
301
+ packageVersions[parentName] = NuGetVersion.Parse(parentVersion);
302
+
303
+ if (parentObject.Value.TryGetProperty("dependencies", out var dependencies))
304
+ {
305
+ foreach (var childObject in dependencies.EnumerateObject())
306
+ {
307
+ var childName = childObject.Name;
308
+ var parentSet = packageParents.GetOrAdd(childName, () => new(StringComparer.OrdinalIgnoreCase));
309
+ parentSet.Add(parentName);
310
+ }
311
+ }
152
312
  }
153
313
  }
314
+
315
+ return (packageParents, packageVersions);
316
+ }
317
+ catch (Exception ex)
318
+ {
319
+ logger.Error($"Error while generating package graph: {ex.Message}");
320
+ throw;
321
+ }
322
+ finally
323
+ {
324
+ tempDir.Delete(recursive: true);
154
325
  }
155
326
  }
156
327
 
@@ -227,7 +398,8 @@ internal static class PackageReferenceUpdater
227
398
  return true;
228
399
  }
229
400
 
230
- private static async Task UpdateTransitiveDependencyAsync(
401
+ /// <returns>The updated files.</returns>
402
+ private static async Task<IEnumerable<string>> UpdateTransitiveDependencyAsync(
231
403
  string repoRootPath,
232
404
  string projectPath,
233
405
  string dependencyName,
@@ -237,16 +409,19 @@ internal static class PackageReferenceUpdater
237
409
  ILogger logger
238
410
  )
239
411
  {
412
+ IEnumerable<string> updatedFiles;
240
413
  var directoryPackagesWithPinning = buildFiles.OfType<ProjectBuildFile>()
241
414
  .FirstOrDefault(bf => IsCpmTransitivePinningEnabled(bf));
242
415
  if (directoryPackagesWithPinning is not null)
243
416
  {
244
- PinTransitiveDependency(directoryPackagesWithPinning, dependencyName, newDependencyVersion, logger);
417
+ updatedFiles = PinTransitiveDependency(directoryPackagesWithPinning, dependencyName, newDependencyVersion, logger);
245
418
  }
246
419
  else
247
420
  {
248
- await AddTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, experimentsManager, logger);
421
+ updatedFiles = await AddTransitiveDependencyAsync(repoRootPath, projectPath, dependencyName, newDependencyVersion, experimentsManager, logger);
249
422
  }
423
+
424
+ return updatedFiles;
250
425
  }
251
426
 
252
427
  private static bool IsCpmTransitivePinningEnabled(ProjectBuildFile buildFile)
@@ -271,7 +446,8 @@ internal static class PackageReferenceUpdater
271
446
  return isTransitivePinningEnabled is not null && string.Equals(isTransitivePinningEnabled, "true", StringComparison.OrdinalIgnoreCase);
272
447
  }
273
448
 
274
- private static void PinTransitiveDependency(ProjectBuildFile directoryPackages, string dependencyName, string newDependencyVersion, ILogger logger)
449
+ /// <returns>The updated files.</returns>
450
+ private static IEnumerable<string> PinTransitiveDependency(ProjectBuildFile directoryPackages, string dependencyName, string newDependencyVersion, ILogger logger)
275
451
  {
276
452
  var existingPackageVersionElement = directoryPackages.ItemNodes
277
453
  .Where(e => e.Name.Equals("PackageVersion", StringComparison.OrdinalIgnoreCase) &&
@@ -288,7 +464,7 @@ internal static class PackageReferenceUpdater
288
464
  if (lastPackageVersion is null)
289
465
  {
290
466
  logger.Info($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not pinned.");
291
- return;
467
+ return [];
292
468
  }
293
469
 
294
470
  var lastItemGroup = lastPackageVersion.Parent;
@@ -324,7 +500,7 @@ internal static class PackageReferenceUpdater
324
500
  else
325
501
  {
326
502
  logger.Info(" Existing PackageVersion element version was already correct.");
327
- return;
503
+ return [];
328
504
  }
329
505
 
330
506
  updatedItemGroup = lastItemGroup.ReplaceChildElement(existingPackageVersionElement, updatedPackageVersionElement);
@@ -332,10 +508,14 @@ internal static class PackageReferenceUpdater
332
508
 
333
509
  var updatedXml = directoryPackages.Contents.ReplaceNode(lastItemGroup.AsNode, updatedItemGroup.AsNode);
334
510
  directoryPackages.Update(updatedXml);
511
+
512
+ return [directoryPackages.Path];
335
513
  }
336
514
 
337
- private static async Task AddTransitiveDependencyAsync(string repoRootPath, string projectPath, string dependencyName, string newDependencyVersion, ExperimentsManager experimentsManager, ILogger logger)
515
+ /// <returns>The updated files.</returns>
516
+ private static async Task<IEnumerable<string>> AddTransitiveDependencyAsync(string repoRootPath, string projectPath, string dependencyName, string newDependencyVersion, ExperimentsManager experimentsManager, ILogger logger)
338
517
  {
518
+ var updatedFiles = new[] { projectPath }; // assume this worked unless...
339
519
  var projectDirectory = Path.GetDirectoryName(projectPath)!;
340
520
  await MSBuildHelper.HandleGlobalJsonAsync(projectDirectory, repoRootPath, experimentsManager, async () =>
341
521
  {
@@ -351,10 +531,13 @@ internal static class PackageReferenceUpdater
351
531
  if (exitCode != 0)
352
532
  {
353
533
  logger.Warn($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
534
+ updatedFiles = [];
354
535
  }
355
536
 
356
537
  return exitCode;
357
538
  }, logger, retainMSBuildSdks: true);
539
+
540
+ return updatedFiles;
358
541
  }
359
542
 
360
543
  /// <summary>
@@ -371,7 +554,7 @@ internal static class PackageReferenceUpdater
371
554
  ILogger logger)
372
555
  {
373
556
  var newDependency = new[] { new Dependency(dependencyName, newDependencyVersion, DependencyType.Unknown) };
374
- var tfmsAndDependencies = new Dictionary<string, Dependency[]>();
557
+ var tfmsAndDependencies = new Dictionary<string, ImmutableArray<Dependency>>();
375
558
  foreach (var tfm in tfms)
376
559
  {
377
560
  var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, newDependency, experimentsManager, logger);
@@ -415,7 +598,7 @@ internal static class PackageReferenceUpdater
415
598
  return packagesAndVersions;
416
599
  }
417
600
 
418
- private static async Task UpdateTopLevelDepdendency(
601
+ private static async Task<IEnumerable<UpdateOperationBase>> UpdateTopLevelDepdendency(
419
602
  string repoRootPath,
420
603
  ImmutableArray<ProjectBuildFile> buildFiles,
421
604
  string[] targetFrameworks,
@@ -427,25 +610,29 @@ internal static class PackageReferenceUpdater
427
610
  ILogger logger)
428
611
  {
429
612
  // update dependencies...
430
- var result = TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger);
431
- if (result == UpdateResult.NotFound)
613
+ var updateOperations = new List<UpdateOperationBase>();
614
+ var (updateResult, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger);
615
+ if (updateResult == UpdateResult.NotFound)
432
616
  {
433
617
  logger.Info($" Root package [{dependencyName}/{previousDependencyVersion}] was not updated; skipping dependencies.");
434
- return;
618
+ return [];
435
619
  }
436
620
 
621
+ updateOperations.AddRange(updateOperationsPerformed);
622
+
437
623
  foreach (var (packageName, packageVersion) in peerDependencies.Where(kvp => string.Compare(kvp.Key, dependencyName, StringComparison.OrdinalIgnoreCase) != 0))
438
624
  {
439
- TryUpdateDependencyVersion(buildFiles, packageName, previousDependencyVersion: null, newDependencyVersion: packageVersion, logger);
625
+ (_, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, packageName, previousDependencyVersion: null, newDependencyVersion: packageVersion, logger);
626
+ updateOperations.AddRange(updateOperationsPerformed);
440
627
  }
441
628
 
442
629
  // ...and make them all coherent
443
- Dependency[] updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray();
630
+ var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToImmutableArray();
444
631
  foreach (ProjectBuildFile projectFile in buildFiles)
445
632
  {
446
633
  foreach (string tfm in targetFrameworks)
447
634
  {
448
- var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsWithBruteForce(repoRootPath, projectFile.Path, tfm, updatedTopLevelDependencies, experimentsManager, logger);
635
+ var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsWithBruteForce(repoRootPath, projectFile.Path, tfm, topLevelDependencies, experimentsManager, logger);
449
636
  if (resolvedDependencies is null)
450
637
  {
451
638
  logger.Info($" Unable to resolve dependency conflicts for {projectFile.Path}.");
@@ -453,7 +640,7 @@ internal static class PackageReferenceUpdater
453
640
  }
454
641
 
455
642
  // ensure the originally requested dependency was resolved to the correct version
456
- var specificResolvedDependency = resolvedDependencies.Where(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
643
+ var specificResolvedDependency = resolvedDependencies.Value.Where(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
457
644
  if (specificResolvedDependency is null)
458
645
  {
459
646
  logger.Info($" Unable to resolve requested dependency for {dependencyName} in {projectFile.Path}.");
@@ -467,17 +654,24 @@ internal static class PackageReferenceUpdater
467
654
  }
468
655
 
469
656
  // update all versions
470
- foreach (Dependency resolvedDependency in resolvedDependencies
657
+ foreach (Dependency resolvedDependency in resolvedDependencies.Value
471
658
  .Where(d => !d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))
472
659
  .Where(d => d.Version is not null))
473
660
  {
474
- TryUpdateDependencyVersion(buildFiles, resolvedDependency.Name, previousDependencyVersion: null, newDependencyVersion: resolvedDependency.Version!, logger);
661
+ (_, updateOperationsPerformed) = TryUpdateDependencyVersion(buildFiles, resolvedDependency.Name, previousDependencyVersion: null, newDependencyVersion: resolvedDependency.Version!, logger);
662
+ updateOperations.AddRange(updateOperationsPerformed);
475
663
  }
664
+
665
+ updateOperationsPerformed = await ComputeUpdateOperations(repoRootPath, projectFile.Path, tfm, topLevelDependencies, [new Dependency(dependencyName, newDependencyVersion, DependencyType.PackageReference)], resolvedDependencies.Value, experimentsManager, logger);
666
+ updateOperations.AddRange(updateOperationsPerformed.Select(u => u with { UpdatedFiles = [projectFile.Path] }));
476
667
  }
477
668
  }
669
+
670
+ return updateOperations;
478
671
  }
479
672
 
480
- private static UpdateResult TryUpdateDependencyVersion(
673
+ /// <returns>The updated files.</returns>
674
+ private static (UpdateResult, IEnumerable<UpdateOperationBase>) TryUpdateDependencyVersion(
481
675
  ImmutableArray<ProjectBuildFile> buildFiles,
482
676
  string dependencyName,
483
677
  string? previousDependencyVersion,
@@ -488,6 +682,7 @@ internal static class PackageReferenceUpdater
488
682
  var foundUnsupported = false;
489
683
  var updateWasPerformed = false;
490
684
  var propertyNames = new List<string>();
685
+ var updateOperations = new List<UpdateOperationBase>();
491
686
 
492
687
  // First we locate all the PackageReference, GlobalPackageReference, or PackageVersion which set the Version
493
688
  // or VersionOverride attribute. In the simplest case we can update the version attribute directly then move
@@ -526,6 +721,12 @@ internal static class PackageReferenceUpdater
526
721
  {
527
722
  logger.Info($" Found incorrect [{packageNode.Name}] version attribute in [{buildFile.RelativePath}].");
528
723
  updateNodes.Add(versionAttribute);
724
+ updateOperations.Add(new DirectUpdate()
725
+ {
726
+ DependencyName = dependencyName,
727
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
728
+ UpdatedFiles = [buildFile.Path],
729
+ });
529
730
  }
530
731
  else if (previousDependencyVersion == null && NuGetVersion.TryParse(currentVersion, out var previousVersion))
531
732
  {
@@ -536,6 +737,12 @@ internal static class PackageReferenceUpdater
536
737
 
537
738
  logger.Info($" Found incorrect peer [{packageNode.Name}] version attribute in [{buildFile.RelativePath}].");
538
739
  updateNodes.Add(versionAttribute);
740
+ updateOperations.Add(new DirectUpdate()
741
+ {
742
+ DependencyName = dependencyName,
743
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
744
+ UpdatedFiles = [buildFile.Path],
745
+ });
539
746
  }
540
747
  }
541
748
  else if (string.Equals(currentVersion, newDependencyVersion, StringComparison.Ordinal))
@@ -566,6 +773,12 @@ internal static class PackageReferenceUpdater
566
773
  if (versionElement is XmlElementSyntax elementSyntax)
567
774
  {
568
775
  updateNodes.Add(elementSyntax);
776
+ updateOperations.Add(new DirectUpdate()
777
+ {
778
+ DependencyName = dependencyName,
779
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
780
+ UpdatedFiles = [buildFile.Path],
781
+ });
569
782
  }
570
783
  else
571
784
  {
@@ -583,6 +796,12 @@ internal static class PackageReferenceUpdater
583
796
  if (versionElement is XmlElementSyntax elementSyntax)
584
797
  {
585
798
  updateNodes.Add(elementSyntax);
799
+ updateOperations.Add(new DirectUpdate()
800
+ {
801
+ DependencyName = dependencyName,
802
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
803
+ UpdatedFiles = [buildFile.Path],
804
+ });
586
805
  }
587
806
  else
588
807
  {
@@ -706,13 +925,14 @@ internal static class PackageReferenceUpdater
706
925
  }
707
926
  }
708
927
 
709
- return updateWasPerformed
928
+ var updateResult = updateWasPerformed
710
929
  ? UpdateResult.Updated
711
930
  : foundCorrect
712
931
  ? UpdateResult.Correct
713
932
  : foundUnsupported
714
933
  ? UpdateResult.NotSupported
715
934
  : UpdateResult.NotFound;
935
+ return (updateResult, updateOperations);
716
936
  }
717
937
 
718
938
  private static IEnumerable<IXmlElementSyntax> FindPackageNodes(
@@ -7,6 +7,7 @@ using System.Xml.XPath;
7
7
  using Microsoft.Language.Xml;
8
8
 
9
9
  using NuGet.CommandLine;
10
+ using NuGet.Versioning;
10
11
 
11
12
  using NuGetUpdater.Core.Updater;
12
13
  using NuGetUpdater.Core.Utilities;
@@ -25,7 +26,7 @@ namespace NuGetUpdater.Core;
25
26
  /// <remarks>
26
27
  internal static partial class PackagesConfigUpdater
27
28
  {
28
- public static async Task UpdateDependencyAsync(
29
+ public static async Task<IEnumerable<UpdateOperationBase>> UpdateDependencyAsync(
29
30
  string repoRootPath,
30
31
  string projectPath,
31
32
  string dependencyName,
@@ -44,7 +45,7 @@ internal static partial class PackagesConfigUpdater
44
45
  if (packagesSubDirectory is null)
45
46
  {
46
47
  logger.Info($" Project [{projectPath}] does not reference this dependency.");
47
- return;
48
+ return [];
48
49
  }
49
50
 
50
51
  logger.Info($" Using packages directory [{packagesSubDirectory}] for project [{projectPath}].");
@@ -95,10 +96,18 @@ internal static partial class PackagesConfigUpdater
95
96
  projectBuildFile.NormalizeDirectorySeparatorsInProject();
96
97
 
97
98
  // Update binding redirects
98
- await BindingRedirectManager.UpdateBindingRedirectsAsync(projectBuildFile, dependencyName, newDependencyVersion);
99
+ var updatedConfigFiles = await BindingRedirectManager.UpdateBindingRedirectsAsync(projectBuildFile, dependencyName, newDependencyVersion);
99
100
 
100
101
  logger.Info(" Writing project file back to disk");
101
102
  await projectBuildFile.SaveAsync();
103
+
104
+ var updateResult = new DirectUpdate()
105
+ {
106
+ DependencyName = dependencyName,
107
+ NewVersion = NuGetVersion.Parse(newDependencyVersion),
108
+ UpdatedFiles = [projectPath, packagesConfigPath, .. updatedConfigFiles],
109
+ };
110
+ return [updateResult];
102
111
  }
103
112
 
104
113
  private static void RunNugetUpdate(List<string> updateArgs, List<string> restoreArgs, string projectDirectory, ILogger logger)