dependabot-nuget 0.321.2 → 0.321.3

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