dependabot-nuget 0.321.2 → 0.322.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Packages.props +22 -22
  3. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/PackageMapper.cs +9 -0
  4. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Cli/Program.cs +21 -7
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs +19 -11
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/CloneCommand.cs +19 -9
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +21 -14
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/FrameworkCheckCommand.cs +8 -5
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/RunCommand.cs +29 -16
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +20 -19
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +2 -1
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscoveryTargetingPacks.props +2 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencySolver/IDependencySolver.cs +8 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencySolver/MSBuildDependencySolver.cs +32 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +1 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +10 -1
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +6 -0
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +3 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +5 -13
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/PrivateSourceTimedOutException.cs +12 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +4 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PrivateSourceTimedOut.cs +10 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/PullRequestTextGenerator.cs +1 -1
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandler.cs +1 -1
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/GroupUpdateAllVersionsHandler.cs +2 -2
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandler.cs +1 -1
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshSecurityUpdatePullRequestHandler.cs +1 -1
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshVersionUpdatePullRequestHandler.cs +1 -1
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +6 -3
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/FileWriters/FileWriterWorker.cs +376 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/FileWriters/IFileWriter.cs +14 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/FileWriters/XmlFileWriter.cs +477 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +9 -5
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationBase.cs +18 -7
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +26 -1
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +15 -0
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/DependencySolver/MSBuildDependencySolverTests.cs +633 -0
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +0 -2
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +0 -2
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/SdkProjectDiscoveryTests.cs +49 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +0 -1
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/EndToEndTests.cs +484 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/HttpApiHandlerTests.cs +1 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/JobErrorBaseTests.cs +7 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MessageReportTests.cs +11 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/PullRequestTextTests.cs +21 -22
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +1 -1
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +8 -0
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/DotNetToolsJsonUpdaterTests.cs +181 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/FileWriterTestsBase.cs +61 -0
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/FileWriterWorkerTests.cs +917 -0
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/FileWriterWorkerTests_MiscellaneousTests.cs +154 -0
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/TestFileWriterReturnsConstantResult.cs +20 -0
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/XmlFileWriterTests.cs +1620 -0
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/XmlFileWriterTests_CreateUpdatedVersionRangeTests.cs +25 -0
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/GlobalJsonUpdaterTests.cs +139 -0
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +1961 -1
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateOperationResultTests.cs +116 -0
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +16 -1043
  60. data/helpers/lib/NuGetUpdater/global.json +1 -1
  61. metadata +21 -10
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +0 -375
  63. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +0 -296
  64. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.LockFile.cs +0 -251
  65. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +0 -201
  66. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +0 -3821
  67. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +0 -2706
@@ -0,0 +1,477 @@
1
+ using System.Collections.Immutable;
2
+ using System.Text.RegularExpressions;
3
+ using System.Xml.Linq;
4
+
5
+ using NuGet.Versioning;
6
+
7
+ using NuGetUpdater.Core.Utilities;
8
+
9
+ namespace NuGetUpdater.Core.Updater.FileWriters;
10
+
11
+ public class XmlFileWriter : IFileWriter
12
+ {
13
+ private const string IncludeAttributeName = "Include";
14
+ private const string UpdateAttributeName = "Update";
15
+ private const string VersionMetadataName = "Version";
16
+ private const string VersionOverrideMetadataName = "VersionOverride";
17
+
18
+ private const string ItemGroupElementName = "ItemGroup";
19
+ private const string GlobalPackageReferenceElementName = "GlobalPackageReference";
20
+ private const string PackageReferenceElementName = "PackageReference";
21
+ private const string PackageVersionElementName = "PackageVersion";
22
+ private const string PropertyGroupElementName = "PropertyGroup";
23
+
24
+ private readonly ILogger _logger;
25
+
26
+ // these file extensions are valid project entrypoints; everything else is ignored
27
+ private static readonly HashSet<string> SupportedProjectFileExtensions = new(StringComparer.OrdinalIgnoreCase)
28
+ {
29
+ ".csproj",
30
+ ".vbproj",
31
+ ".fsproj",
32
+ };
33
+
34
+ // these file extensions are valid additional files and can be updated; everything else is ignored
35
+ private static readonly HashSet<string> SupportedAdditionalFileExtensions = new(StringComparer.OrdinalIgnoreCase)
36
+ {
37
+ ".props",
38
+ ".targets",
39
+ };
40
+
41
+ public XmlFileWriter(ILogger logger)
42
+ {
43
+ _logger = logger;
44
+ }
45
+
46
+ public async Task<bool> UpdatePackageVersionsAsync(
47
+ DirectoryInfo repoContentsPath,
48
+ ImmutableArray<string> relativeFilePaths,
49
+ ImmutableArray<Dependency> originalDependencies,
50
+ ImmutableArray<Dependency> requiredPackageVersions,
51
+ bool addPackageReferenceElementForPinnedPackages
52
+ )
53
+ {
54
+ if (relativeFilePaths.IsDefaultOrEmpty)
55
+ {
56
+ _logger.Warn("No files to update; skipping XML update.");
57
+ return false;
58
+ }
59
+
60
+ var updatesPerformed = requiredPackageVersions.ToDictionary(d => d.Name, _ => false, StringComparer.OrdinalIgnoreCase);
61
+ var projectRelativePath = relativeFilePaths[0];
62
+ var projectExtension = Path.GetExtension(projectRelativePath);
63
+ if (!SupportedProjectFileExtensions.Contains(projectExtension))
64
+ {
65
+ _logger.Warn($"Project extension '{projectExtension}' not supported; skipping XML update.");
66
+ return false;
67
+ }
68
+
69
+ var filesAndContentsTasks = relativeFilePaths
70
+ .Where(path => SupportedProjectFileExtensions.Contains(Path.GetExtension(path)) || SupportedAdditionalFileExtensions.Contains(Path.GetExtension(path)))
71
+ .Select(async path =>
72
+ {
73
+ var content = await ReadFileContentsAsync(repoContentsPath, path);
74
+ var document = XDocument.Parse(content, LoadOptions.PreserveWhitespace);
75
+ return KeyValuePair.Create(path, document);
76
+ })
77
+ .ToArray();
78
+ var filesAndContents = (await Task.WhenAll(filesAndContentsTasks))
79
+ .ToDictionary();
80
+ foreach (var requiredPackageVersion in requiredPackageVersions)
81
+ {
82
+ var oldVersionString = originalDependencies.FirstOrDefault(d => d.Name.Equals(requiredPackageVersion.Name, StringComparison.OrdinalIgnoreCase))?.Version;
83
+ if (oldVersionString is null)
84
+ {
85
+ _logger.Warn($"Unable to find project dependency with name {requiredPackageVersion.Name}; skipping XML update.");
86
+ continue;
87
+ }
88
+
89
+ var oldVersion = NuGetVersion.Parse(oldVersionString);
90
+ var requiredVersion = NuGetVersion.Parse(requiredPackageVersion.Version!);
91
+
92
+ if (oldVersion == requiredVersion)
93
+ {
94
+ _logger.Info($"Dependency {requiredPackageVersion.Name} is already at version {requiredVersion}; no update needed.");
95
+ updatesPerformed[requiredPackageVersion.Name] = true;
96
+ continue;
97
+ }
98
+
99
+ // version numbers can be in attributes or elements and we may need to do some complicated navigation
100
+ // this object is used to perform the update once we've walked back as far as necessary
101
+ string? currentVersionString = null;
102
+ Action<string>? updateVersionLocation = null;
103
+
104
+ var packageReferenceElements = filesAndContents.Values
105
+ .SelectMany(doc => doc.Descendants().Where(e => e.Name.LocalName == PackageReferenceElementName || e.Name.LocalName == GlobalPackageReferenceElementName))
106
+ .Where(e =>
107
+ {
108
+ var attributeValue = e.Attribute(IncludeAttributeName)?.Value ?? e.Attribute(UpdateAttributeName)?.Value ?? string.Empty;
109
+ var packageNames = attributeValue.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
110
+ return packageNames.Any(name => name.Equals(requiredPackageVersion.Name, StringComparison.OrdinalIgnoreCase));
111
+ })
112
+ .ToArray();
113
+
114
+ if (packageReferenceElements.Length == 0)
115
+ {
116
+ // no matching `<PackageReference>` elements found; pin it as a transitive dependency
117
+ updatesPerformed[requiredPackageVersion.Name] = true; // all cases below add the dependency
118
+
119
+ // find last `<ItemGroup>` in the project...
120
+ Action addItemGroup = () => { }; // adding an ItemGroup to the project isn't always necessary, but it's much easier to prepare for it here
121
+ var projectDocument = filesAndContents[projectRelativePath];
122
+ var lastItemGroup = projectDocument.Root!.Elements()
123
+ .LastOrDefault(e => e.Name.LocalName.Equals(ItemGroupElementName, StringComparison.OrdinalIgnoreCase));
124
+ if (lastItemGroup is null)
125
+ {
126
+ _logger.Info($"No `<{ItemGroupElementName}>` element found in project; adding one.");
127
+ lastItemGroup = new XElement(XName.Get(ItemGroupElementName, projectDocument.Root.Name.NamespaceName));
128
+ addItemGroup = () => projectDocument.Root.Add(lastItemGroup);
129
+ }
130
+
131
+ // ...find where the new item should go...
132
+ var packageReferencesBeforeNew = lastItemGroup.Elements()
133
+ .Where(e => e.Name.LocalName.Equals(PackageReferenceElementName, StringComparison.OrdinalIgnoreCase))
134
+ .TakeWhile(e => (e.Attribute(IncludeAttributeName)?.Value ?? e.Attribute(UpdateAttributeName)?.Value ?? string.Empty).CompareTo(requiredPackageVersion.Name) < 0)
135
+ .ToArray();
136
+
137
+ // ...prepare a new `<PackageReference>` element...
138
+ var newElement = new XElement(
139
+ XName.Get(PackageReferenceElementName, projectDocument.Root.Name.NamespaceName),
140
+ new XAttribute(IncludeAttributeName, requiredPackageVersion.Name));
141
+
142
+ // ...add the `<PackageReference>` element if and where appropriate...
143
+ if (addPackageReferenceElementForPinnedPackages)
144
+ {
145
+ addItemGroup();
146
+ var lastPriorPackageReference = packageReferencesBeforeNew.LastOrDefault();
147
+ if (lastPriorPackageReference is not null)
148
+ {
149
+ AddAfterSiblingElement(lastPriorPackageReference, newElement);
150
+ }
151
+ else
152
+ {
153
+ // no prior package references; add to the front
154
+ var indent = GetIndentXTextFromElement(lastItemGroup, extraIndentationToAdd: " ");
155
+ lastItemGroup.AddFirst(indent, newElement);
156
+ }
157
+ }
158
+
159
+ // ...find the best place to add the version...
160
+ var matchingPackageVersionElement = filesAndContents.Values
161
+ .SelectMany(doc => doc.Descendants().Where(e => e.Name.LocalName.Equals(PackageVersionElementName, StringComparison.OrdinalIgnoreCase)))
162
+ .FirstOrDefault(e => (e.Attribute(IncludeAttributeName)?.Value ?? string.Empty).Trim().Equals(requiredPackageVersion.Name, StringComparison.OrdinalIgnoreCase));
163
+ if (matchingPackageVersionElement is not null)
164
+ {
165
+ // found matching `<PackageVersion>` element; if `Version` attribute is appropriate we're done, otherwise set `VersionOverride` attribute on new element
166
+ var versionAttribute = matchingPackageVersionElement.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals(VersionMetadataName, StringComparison.OrdinalIgnoreCase));
167
+ if (versionAttribute is not null &&
168
+ NuGetVersion.TryParse(versionAttribute.Value, out var existingVersion) &&
169
+ existingVersion == requiredVersion)
170
+ {
171
+ // version matches; no update needed
172
+ _logger.Info($"Dependency {requiredPackageVersion.Name} already set to {requiredVersion}; no override needed.");
173
+ }
174
+ else
175
+ {
176
+ // version doesn't match; use `VersionOverride` attribute on new element
177
+ _logger.Info($"Dependency {requiredPackageVersion.Name} set to {requiredVersion}; using `{VersionOverrideMetadataName}` attribute on new element.");
178
+ newElement.SetAttributeValue(VersionOverrideMetadataName, requiredVersion.ToString());
179
+ }
180
+ }
181
+ else
182
+ {
183
+ // no matching `<PackageVersion>` element; either add a new one, or directly set the `Version` attribute on the new element
184
+ var allPackageVersionElements = filesAndContents.Values
185
+ .SelectMany(doc => doc.Descendants().Where(e => e.Name.LocalName.Equals(PackageVersionElementName, StringComparison.OrdinalIgnoreCase)))
186
+ .ToArray();
187
+ if (allPackageVersionElements.Length > 0)
188
+ {
189
+ // add a new `<PackageVersion>` element
190
+ var newVersionElement = new XElement(XName.Get(PackageVersionElementName, projectDocument.Root.Name.NamespaceName),
191
+ new XAttribute(IncludeAttributeName, requiredPackageVersion.Name),
192
+ new XAttribute(VersionMetadataName, requiredVersion.ToString()));
193
+ var lastPriorPackageVersionElement = allPackageVersionElements
194
+ .TakeWhile(e => (e.Attribute(IncludeAttributeName)?.Value ?? string.Empty).Trim().CompareTo(requiredPackageVersion.Name) < 0)
195
+ .LastOrDefault();
196
+ if (lastPriorPackageVersionElement is not null)
197
+ {
198
+ _logger.Info($"Adding new `<{PackageVersionElementName}>` element for {requiredPackageVersion.Name} with version {requiredVersion}.");
199
+ AddAfterSiblingElement(lastPriorPackageVersionElement, newVersionElement);
200
+ }
201
+ else
202
+ {
203
+ // no prior package versions; add to the front of the document
204
+ _logger.Info($"Adding new `<{PackageVersionElementName}>` element for {requiredPackageVersion.Name} with version {requiredVersion} at the start of the document.");
205
+ var packageVersionGroup = allPackageVersionElements.First().Parent!;
206
+ var indent = GetIndentXTextFromElement(packageVersionGroup, extraIndentationToAdd: " ");
207
+ packageVersionGroup.AddFirst(indent, newVersionElement);
208
+ }
209
+ }
210
+ else
211
+ {
212
+ // add a direct `Version` attribute
213
+ newElement.SetAttributeValue(VersionMetadataName, requiredVersion.ToString());
214
+ }
215
+ }
216
+ }
217
+ else
218
+ {
219
+ // found matching `<PackageReference>` elements to update
220
+ foreach (var packageReferenceElement in packageReferenceElements)
221
+ {
222
+ // first check for matching `Version` attribute
223
+ var versionAttribute = packageReferenceElement.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals(VersionMetadataName, StringComparison.OrdinalIgnoreCase));
224
+ if (versionAttribute is not null)
225
+ {
226
+ currentVersionString = versionAttribute.Value;
227
+ updateVersionLocation = (version) => versionAttribute.Value = version;
228
+ goto doVersionUpdate;
229
+ }
230
+
231
+ // next check for `Version` child element
232
+ var versionElement = packageReferenceElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals(VersionMetadataName, StringComparison.OrdinalIgnoreCase));
233
+ if (versionElement is not null)
234
+ {
235
+ currentVersionString = versionElement.Value;
236
+ updateVersionLocation = (version) => versionElement.Value = version;
237
+ goto doVersionUpdate;
238
+ }
239
+
240
+ // check for matching `<PackageVersion>` element
241
+ var packageVersionElement = filesAndContents.Values
242
+ .SelectMany(doc => doc.Descendants().Where(e => e.Name.LocalName == PackageVersionElementName))
243
+ .FirstOrDefault(e => (e.Attribute(IncludeAttributeName)?.Value ?? string.Empty).Trim().Equals(requiredPackageVersion.Name, StringComparison.OrdinalIgnoreCase));
244
+ if (packageVersionElement is not null)
245
+ {
246
+ var packageVersionAttribute = packageVersionElement.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals(VersionMetadataName, StringComparison.OrdinalIgnoreCase));
247
+ if (packageVersionAttribute is not null)
248
+ {
249
+ currentVersionString = packageVersionAttribute.Value;
250
+ updateVersionLocation = (version) => packageVersionAttribute.Value = version;
251
+ goto doVersionUpdate;
252
+ }
253
+ else
254
+ {
255
+ var cpmVersionElement = packageVersionElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals(VersionMetadataName, StringComparison.OrdinalIgnoreCase));
256
+ if (cpmVersionElement is not null)
257
+ {
258
+ currentVersionString = cpmVersionElement.Value;
259
+ updateVersionLocation = (version) => cpmVersionElement.Value = version;
260
+ goto doVersionUpdate;
261
+ }
262
+ }
263
+ }
264
+
265
+ doVersionUpdate:
266
+ if (currentVersionString is not null && updateVersionLocation is not null)
267
+ {
268
+ var performedUpdate = false;
269
+ var candidateUpdateLocations = new Queue<(string VersionString, Action<string> Updater)>();
270
+ candidateUpdateLocations.Enqueue((currentVersionString, updateVersionLocation));
271
+
272
+ while (candidateUpdateLocations.TryDequeue(out var candidateUpdateLocation))
273
+ {
274
+ var candidateUpdateVersionString = candidateUpdateLocation.VersionString;
275
+ var candidateUpdater = candidateUpdateLocation.Updater;
276
+
277
+ if (NuGetVersion.TryParse(candidateUpdateVersionString, out var candidateUpdateVersion))
278
+ {
279
+ // most common: direct update
280
+ if (candidateUpdateVersion == requiredVersion)
281
+ {
282
+ // already up to date from a previous pass
283
+ updatesPerformed[requiredPackageVersion.Name] = true;
284
+ performedUpdate = true;
285
+ _logger.Info($"Dependency {requiredPackageVersion.Name} already set to {requiredVersion}; no update needed.");
286
+ break;
287
+ }
288
+ else if (candidateUpdateVersion == oldVersion)
289
+ {
290
+ // do the update here and call it good
291
+ candidateUpdater(requiredVersion.ToString());
292
+ updatesPerformed[requiredPackageVersion.Name] = true;
293
+ performedUpdate = true;
294
+ _logger.Info($"Updated dependency {requiredPackageVersion.Name} from version {oldVersion} to {requiredVersion}.");
295
+ break;
296
+ }
297
+ else
298
+ {
299
+ // no exact match found, but this may be a magic SDK package
300
+ var packageMapper = DotNetPackageCorrelationManager.GetPackageMapper();
301
+ var isSdkReplacementPackage = packageMapper.IsSdkReplacementPackage(requiredPackageVersion.Name);
302
+ if (isSdkReplacementPackage &&
303
+ candidateUpdateVersion < oldVersion && // version in XML is older than what was resolved by the SDK
304
+ oldVersion < requiredVersion) // this ensures we don't downgrade the wrong one
305
+ {
306
+ // If we're updating a top level SDK replacement package, the version listed in the project file won't
307
+ // necessarily match the resolved version that caused the update because the SDK might have replaced
308
+ // the package. To handle this scenario, we pretend the version we're searching for was actually found.
309
+ candidateUpdater(requiredVersion.ToString());
310
+ updatesPerformed[requiredPackageVersion.Name] = true;
311
+ performedUpdate = true;
312
+ _logger.Info($"Updated SDK-managed package {requiredPackageVersion.Name} from version {oldVersion} to {requiredVersion}.");
313
+ break;
314
+ }
315
+ }
316
+ }
317
+ else if (VersionRange.TryParse(candidateUpdateVersionString, out var candidateUpdateVersionRange))
318
+ {
319
+ // less common: version range
320
+ if (candidateUpdateVersionRange.Satisfies(oldVersion))
321
+ {
322
+ var updatedVersionRange = CreateUpdatedVersionRangeString(candidateUpdateVersionRange, oldVersion, requiredVersion);
323
+ candidateUpdater(updatedVersionRange);
324
+ updatesPerformed[requiredPackageVersion.Name] = true;
325
+ performedUpdate = true;
326
+ _logger.Info($"Updated dependency {requiredPackageVersion.Name} from version {oldVersion} to {requiredVersion}.");
327
+ break;
328
+ }
329
+ else if (candidateUpdateVersionRange.Satisfies(requiredVersion))
330
+ {
331
+ // already up to date from a previous pass
332
+ updatesPerformed[requiredPackageVersion.Name] = true;
333
+ performedUpdate = true;
334
+ _logger.Info($"Dependency {requiredPackageVersion.Name} version range '{candidateUpdateVersionRange}' already includes {requiredVersion}; no update needed.");
335
+ break;
336
+ }
337
+ }
338
+
339
+ // find something that looks like it contains a property expansion, even if it's surrounded by other text
340
+ var propertyInSubstringPattern = new Regex(@"(?<Prefix>[^$]*)\$\((?<PropertyName>[A-Za-z0-9_]+)\)(?<Suffix>.*$)");
341
+ // e.g., not-a-dollar-sign $ ( alphanumeric-or-underscore ) everything-else
342
+ var propertyMatch = propertyInSubstringPattern.Match(candidateUpdateVersionString);
343
+ if (propertyMatch.Success)
344
+ {
345
+ // this looks like a property; keep walking backwards with all possible elements
346
+ var propertyName = propertyMatch.Groups["PropertyName"].Value;
347
+ var propertyDefinitions = filesAndContents.Values
348
+ .SelectMany(doc => doc.Descendants().Where(e => e.Name.LocalName.Equals(propertyName, StringComparison.OrdinalIgnoreCase)))
349
+ .Where(e => e.Parent?.Name.LocalName.Equals(PropertyGroupElementName, StringComparison.OrdinalIgnoreCase) == true)
350
+ .ToArray();
351
+ foreach (var propertyDefinition in propertyDefinitions)
352
+ {
353
+ candidateUpdateLocations.Enqueue((propertyDefinition.Value, (version) => propertyDefinition.Value = version));
354
+ }
355
+ }
356
+ }
357
+
358
+ if (!performedUpdate)
359
+ {
360
+ _logger.Warn($"Unable to find appropriate location to update package {requiredPackageVersion.Name} to version {requiredPackageVersion.Version}; no update performed");
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ var performedAllUpdates = updatesPerformed.Values.All(v => v);
368
+ if (performedAllUpdates)
369
+ {
370
+ foreach (var (path, contents) in filesAndContents)
371
+ {
372
+ await WriteFileContentsAsync(repoContentsPath, path, contents.ToString());
373
+ }
374
+ }
375
+
376
+ return performedAllUpdates;
377
+ }
378
+
379
+ private static XText? GetIndentXTextFromElement(XElement element, string extraIndentationToAdd = "")
380
+ {
381
+ var indentText = (element.PreviousNode as XText)?.Value;
382
+ var indent = indentText is not null
383
+ ? new XText(indentText + extraIndentationToAdd)
384
+ : null;
385
+ return indent;
386
+ }
387
+
388
+ private static void AddAfterSiblingElement(XElement siblingElement, XElement newElement, string extraIndentationToAdd = "")
389
+ {
390
+ var indent = GetIndentXTextFromElement(siblingElement, extraIndentationToAdd);
391
+ XNode nodeToAddAfter = siblingElement;
392
+ var done = false;
393
+ while (!done && nodeToAddAfter.NextNode is not null)
394
+ {
395
+ // skip over XText and XComment nodes until we find a newline
396
+ switch (nodeToAddAfter.NextNode)
397
+ {
398
+ case XText text:
399
+ if (text.Value.Contains('\n'))
400
+ {
401
+ done = true;
402
+ }
403
+ else
404
+ {
405
+ nodeToAddAfter = nodeToAddAfter.NextNode;
406
+ }
407
+
408
+ break;
409
+ case XComment comment:
410
+ if (comment.Value.Contains('\n'))
411
+ {
412
+ done = true;
413
+ }
414
+ else
415
+ {
416
+ nodeToAddAfter = nodeToAddAfter.NextNode;
417
+ }
418
+
419
+ break;
420
+ default:
421
+ done = true;
422
+ break;
423
+ }
424
+ }
425
+
426
+ nodeToAddAfter.AddAfterSelf(indent, newElement);
427
+ }
428
+
429
+ private static async Task<string> ReadFileContentsAsync(DirectoryInfo repoContentsPath, string path)
430
+ {
431
+ var fullPath = Path.Join(repoContentsPath.FullName, path);
432
+ var contents = await File.ReadAllTextAsync(fullPath);
433
+ return contents;
434
+ }
435
+
436
+ private static async Task WriteFileContentsAsync(DirectoryInfo repoContentsPath, string path, string contents)
437
+ {
438
+ var fullPath = Path.Join(repoContentsPath.FullName, path);
439
+ await File.WriteAllTextAsync(fullPath, contents);
440
+ }
441
+
442
+ public static string CreateUpdatedVersionRangeString(VersionRange existingRange, NuGetVersion existingVersion, NuGetVersion requiredVersion)
443
+ {
444
+ var newMinVersion = requiredVersion;
445
+ Func<NuGetVersion, NuGetVersion, bool> maxVersionComparer = existingRange.IsMaxInclusive
446
+ ? (a, b) => a >= b
447
+ : (a, b) => a > b;
448
+ var newMaxVersion = existingVersion == existingRange.MaxVersion
449
+ ? requiredVersion
450
+ : existingRange.MaxVersion is not null && maxVersionComparer(existingRange.MaxVersion, requiredVersion)
451
+ ? existingRange.MaxVersion
452
+ : null;
453
+ var newRange = new VersionRange(
454
+ minVersion: newMinVersion,
455
+ includeMinVersion: true,
456
+ maxVersion: newMaxVersion,
457
+ includeMaxVersion: newMaxVersion is not null && existingRange.IsMaxInclusive
458
+ );
459
+
460
+ // special case common scenarios
461
+
462
+ // e.g., "[2.0.0, 2.0.0]" => "[2.0.0]"
463
+ if (newRange.MinVersion == newRange.MaxVersion &&
464
+ newRange.IsMaxInclusive)
465
+ {
466
+ return $"[{newRange.MinVersion}]";
467
+ }
468
+
469
+ // e.g., "[2.0.0, )" => "2.0.0"
470
+ if (newRange.MaxVersion is null)
471
+ {
472
+ return requiredVersion.ToString();
473
+ }
474
+
475
+ return newRange.ToString();
476
+ }
477
+ }
@@ -2,7 +2,7 @@ namespace NuGetUpdater.Core;
2
2
 
3
3
  internal static class GlobalJsonUpdater
4
4
  {
5
- public static async Task UpdateDependencyAsync(
5
+ public static async Task<string?> UpdateDependencyAsync(
6
6
  string repoRootPath,
7
7
  string workspacePath,
8
8
  string dependencyName,
@@ -13,7 +13,7 @@ internal static class GlobalJsonUpdater
13
13
  if (!MSBuildHelper.TryGetGlobalJsonPath(repoRootPath, workspacePath, out var globalJsonPath))
14
14
  {
15
15
  logger.Info(" No global.json file found.");
16
- return;
16
+ return null;
17
17
  }
18
18
 
19
19
  var globalJsonFile = GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger);
@@ -24,19 +24,20 @@ internal static class GlobalJsonUpdater
24
24
  if (!containsDependency)
25
25
  {
26
26
  logger.Info($" Dependency [{dependencyName}] not found.");
27
- return;
27
+ return null;
28
28
  }
29
29
 
30
30
  if (globalJsonFile.MSBuildSdks?.TryGetPropertyValue(dependencyName, out var version) != true
31
31
  || version?.GetValue<string>() is not string versionString)
32
32
  {
33
33
  logger.Info(" Unable to determine dependency version.");
34
- return;
34
+ return null;
35
35
  }
36
36
 
37
37
  if (versionString != previousDependencyVersion)
38
38
  {
39
- return;
39
+ logger.Info($" Expected old version of {previousDependencyVersion} but found {versionString}.");
40
+ return null;
40
41
  }
41
42
 
42
43
  globalJsonFile.UpdateProperty(["msbuild-sdks", dependencyName], newDependencyVersion);
@@ -44,6 +45,9 @@ internal static class GlobalJsonUpdater
44
45
  if (await globalJsonFile.SaveAsync())
45
46
  {
46
47
  logger.Info($" Saved [{globalJsonFile.RelativePath}].");
48
+ return globalJsonFile.Path;
47
49
  }
50
+
51
+ return null;
48
52
  }
49
53
  }
@@ -21,7 +21,18 @@ public abstract record UpdateOperationBase
21
21
  public required NuGetVersion NewVersion { get; init; }
22
22
  public required ImmutableArray<string> UpdatedFiles { get; init; }
23
23
 
24
- public abstract string GetReport();
24
+ protected abstract string GetReportText();
25
+
26
+ public string GetReport(bool includeFileNames)
27
+ {
28
+ var report = GetReportText();
29
+ if (includeFileNames)
30
+ {
31
+ report += $" in {string.Join(", ", UpdatedFiles)}";
32
+ }
33
+
34
+ return report;
35
+ }
25
36
 
26
37
  public ReportedDependency ToReportedDependency(string projectPath, IEnumerable<ReportedDependency> previouslyReportedDependencies, IEnumerable<Dependency> updatedDependencies)
27
38
  {
@@ -49,9 +60,9 @@ public abstract record UpdateOperationBase
49
60
  };
50
61
  }
51
62
 
52
- internal static string GenerateUpdateOperationReport(IEnumerable<UpdateOperationBase> updateOperations)
63
+ internal static string GenerateUpdateOperationReport(IEnumerable<UpdateOperationBase> updateOperations, bool includeFileNames = true)
53
64
  {
54
- var updateMessages = updateOperations.Select(u => u.GetReport()).ToImmutableArray();
65
+ var updateMessages = updateOperations.Select(u => u.GetReport(includeFileNames)).Distinct(StringComparer.OrdinalIgnoreCase).ToImmutableArray();
55
66
  if (updateMessages.Length == 0)
56
67
  {
57
68
  return string.Empty;
@@ -144,12 +155,12 @@ public abstract record UpdateOperationBase
144
155
  public record DirectUpdate : UpdateOperationBase
145
156
  {
146
157
  public override string Type => nameof(DirectUpdate);
147
- public override string GetReport()
158
+ protected override string GetReportText()
148
159
  {
149
160
  var fromText = OldVersion is null
150
161
  ? string.Empty
151
162
  : $"from {OldVersion} ";
152
- return $"Updated {DependencyName} {fromText}to {NewVersion} in {string.Join(", ", UpdatedFiles)}";
163
+ return $"Updated {DependencyName} {fromText}to {NewVersion}";
153
164
  }
154
165
 
155
166
  public sealed override string ToString() => GetString();
@@ -158,7 +169,7 @@ public record DirectUpdate : UpdateOperationBase
158
169
  public record PinnedUpdate : UpdateOperationBase
159
170
  {
160
171
  public override string Type => nameof(PinnedUpdate);
161
- public override string GetReport() => $"Pinned {DependencyName} at {NewVersion} in {string.Join(", ", UpdatedFiles)}";
172
+ protected override string GetReportText() => $"Pinned {DependencyName} at {NewVersion}";
162
173
  public sealed override string ToString() => GetString();
163
174
  }
164
175
 
@@ -168,7 +179,7 @@ public record ParentUpdate : UpdateOperationBase, IEquatable<UpdateOperationBase
168
179
  public required string ParentDependencyName { get; init; }
169
180
  public required NuGetVersion ParentNewVersion { get; init; }
170
181
 
171
- public override string GetReport() => $"Updated {DependencyName} to {NewVersion} indirectly via {ParentDependencyName}/{ParentNewVersion} in {string.Join(", ", UpdatedFiles)}";
182
+ protected override string GetReportText() => $"Updated {DependencyName} to {NewVersion} indirectly via {ParentDependencyName}/{ParentNewVersion}";
172
183
 
173
184
  bool IEquatable<UpdateOperationBase>.Equals(UpdateOperationBase? other)
174
185
  {
@@ -1,11 +1,15 @@
1
1
  using System.Collections.Immutable;
2
- using System.Net;
3
2
  using System.Text.Json;
4
3
  using System.Text.Json.Serialization;
5
4
 
5
+ using NuGet.Versioning;
6
+
6
7
  using NuGetUpdater.Core.Analyze;
8
+ using NuGetUpdater.Core.DependencySolver;
9
+ using NuGetUpdater.Core.Discover;
7
10
  using NuGetUpdater.Core.Run.ApiModel;
8
11
  using NuGetUpdater.Core.Updater;
12
+ using NuGetUpdater.Core.Updater.FileWriters;
9
13
  using NuGetUpdater.Core.Utilities;
10
14
 
11
15
  namespace NuGetUpdater.Core;
@@ -73,6 +77,27 @@ public class UpdaterWorker : IUpdaterWorker
73
77
  workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
74
78
  }
75
79
 
80
+ if (_experimentsManager.UseNewFileUpdater)
81
+ {
82
+ var worker = new FileWriterWorker(
83
+ new DiscoveryWorker(_jobId, _experimentsManager, _logger),
84
+ new MSBuildDependencySolver(new DirectoryInfo(repoRootPath), new FileInfo(workspacePath), _experimentsManager, _logger),
85
+ new XmlFileWriter(_logger),
86
+ _logger
87
+ );
88
+ var updateOperations = await worker.RunAsync(
89
+ new DirectoryInfo(repoRootPath),
90
+ new FileInfo(workspacePath),
91
+ dependencyName,
92
+ NuGetVersion.Parse(previousDependencyVersion),
93
+ NuGetVersion.Parse(newDependencyVersion)
94
+ );
95
+ return new UpdateOperationResult()
96
+ {
97
+ UpdateOperations = updateOperations,
98
+ };
99
+ }
100
+
76
101
  if (!isTransitive)
77
102
  {
78
103
  await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);