dependabot-nuget 0.285.0 → 0.287.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Build.props +5 -1
  3. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/NuGet.CommandLine.csproj +1 -0
  4. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Configuration/NuGet.Configuration.csproj +1 -0
  5. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.LibraryModel/NuGet.LibraryModel.csproj +1 -0
  6. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Packaging/NuGet.Packaging.csproj +1 -1
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/RunCommand.cs +8 -1
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +7 -3
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +11 -0
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +2 -2
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +24 -5
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +15 -4
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +9 -38
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +10 -8
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +52 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +15 -8
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/IAnalyzeWorker.cs +9 -0
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/IDiscoveryWorker.cs +8 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/IUpdaterWorker.cs +9 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +78 -61
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +21 -10
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +37 -5
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +5 -3
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +5 -6
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +51 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +302 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +269 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +577 -43
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +168 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestAnalyzeWorker.cs +37 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestDiscoveryWorker.cs +35 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestUpdaterWorker.cs +39 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +104 -3
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +51 -13
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DirsProj.cs +4 -2
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +62 -18
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +1 -1
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/PathHelperTests.cs +14 -0
  39. data/helpers/lib/NuGetUpdater/global.json +1 -1
  40. data/lib/dependabot/nuget/file_updater.rb +8 -3
  41. data/lib/dependabot/nuget/native_helpers.rb +11 -12
  42. metadata +12 -6
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/DependencySolverEnvironment.cs +0 -12
@@ -13,6 +13,9 @@ public class RunWorker
13
13
  {
14
14
  private readonly IApiHandler _apiHandler;
15
15
  private readonly ILogger _logger;
16
+ private readonly IDiscoveryWorker _discoveryWorker;
17
+ private readonly IAnalyzeWorker _analyzeWorker;
18
+ private readonly IUpdaterWorker _updaterWorker;
16
19
 
17
20
  internal static readonly JsonSerializerOptions SerializerOptions = new()
18
21
  {
@@ -21,10 +24,13 @@ public class RunWorker
21
24
  Converters = { new JsonStringEnumConverter() },
22
25
  };
23
26
 
24
- public RunWorker(IApiHandler apiHandler, ILogger logger)
27
+ public RunWorker(IApiHandler apiHandler, IDiscoveryWorker discoverWorker, IAnalyzeWorker analyzeWorker, IUpdaterWorker updateWorker, ILogger logger)
25
28
  {
26
29
  _apiHandler = apiHandler;
27
30
  _logger = logger;
31
+ _discoveryWorker = discoverWorker;
32
+ _analyzeWorker = analyzeWorker;
33
+ _updaterWorker = updateWorker;
28
34
  }
29
35
 
30
36
  public async Task RunAsync(FileInfo jobFilePath, DirectoryInfo repoContentsPath, string baseCommitSha, FileInfo outputFilePath)
@@ -55,12 +61,13 @@ public class RunWorker
55
61
  {
56
62
  MSBuildHelper.RegisterMSBuild(repoContentsPath.FullName, repoContentsPath.FullName);
57
63
 
64
+ var experimentsManager = ExperimentsManager.GetExperimentsManager(job.Experiments);
58
65
  var allDependencyFiles = new Dictionary<string, DependencyFile>();
59
66
  foreach (var directory in job.GetAllDirectories())
60
67
  {
61
68
  var localPath = PathHelper.JoinPath(repoContentsPath.FullName, directory);
62
69
  lastUsedPackageSourceUrls = NuGetContext.GetPackageSourceUrls(localPath);
63
- var result = await RunForDirectory(job, repoContentsPath, directory, baseCommitSha);
70
+ var result = await RunForDirectory(job, repoContentsPath, directory, baseCommitSha, experimentsManager);
64
71
  foreach (var dependencyFile in result.Base64DependencyFiles)
65
72
  {
66
73
  var uniqueKey = Path.GetFullPath(Path.Join(dependencyFile.Directory, dependencyFile.Name)).NormalizePathToUnix().EnsurePrefix("/");
@@ -102,10 +109,9 @@ public class RunWorker
102
109
  return runResult;
103
110
  }
104
111
 
105
- private async Task<RunResult> RunForDirectory(Job job, DirectoryInfo repoContentsPath, string repoDirectory, string baseCommitSha)
112
+ private async Task<RunResult> RunForDirectory(Job job, DirectoryInfo repoContentsPath, string repoDirectory, string baseCommitSha, ExperimentsManager experimentsManager)
106
113
  {
107
- var discoveryWorker = new DiscoveryWorker(_logger);
108
- var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, repoDirectory);
114
+ var discoveryResult = await _discoveryWorker.RunAsync(repoContentsPath.FullName, repoDirectory);
109
115
 
110
116
  _logger.Log("Discovery JSON content:");
111
117
  _logger.Log(JsonSerializer.Serialize(discoveryResult, DiscoveryWorker.SerializerOptions));
@@ -128,23 +134,31 @@ public class RunWorker
128
134
  });
129
135
 
130
136
  // track original contents for later handling
131
- foreach (var project in discoveryResult.Projects)
137
+ async Task TrackOriginalContentsAsync(string directory, string fileName, string? replacementFileName = null)
132
138
  {
133
- // TODO: include global.json, etc.
134
- var path = Path.Join(discoveryResult.Path, project.FilePath).NormalizePathToUnix().EnsurePrefix("/");
135
- var localPath = Path.Join(repoContentsPath.FullName, discoveryResult.Path, project.FilePath);
136
- var content = await File.ReadAllTextAsync(localPath);
137
- originalDependencyFileContents[path] = content;
138
-
139
- // track packages.config if it exists
140
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
141
- var packagesConfigPath = Path.Join(repoContentsPath.FullName, discoveryResult.Path, projectDirectory, "packages.config");
142
- var normalizedPackagesConfigPath = Path.Join(discoveryResult.Path, projectDirectory, "packages.config").NormalizePathToUnix().EnsurePrefix("/");
143
- if (File.Exists(packagesConfigPath))
139
+ var repoFullPath = Path.Join(directory, fileName);
140
+ if (replacementFileName is not null)
141
+ {
142
+ repoFullPath = Path.Join(Path.GetDirectoryName(repoFullPath)!, replacementFileName);
143
+ }
144
+
145
+ repoFullPath = repoFullPath.FullyNormalizedRootedPath();
146
+ var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
147
+
148
+ if (!File.Exists(localFullPath))
144
149
  {
145
- var packagesConfigContent = await File.ReadAllTextAsync(packagesConfigPath);
146
- originalDependencyFileContents[normalizedPackagesConfigPath] = packagesConfigContent;
150
+ return;
147
151
  }
152
+
153
+ var content = await File.ReadAllTextAsync(localFullPath);
154
+ originalDependencyFileContents[repoFullPath] = content;
155
+ }
156
+
157
+ foreach (var project in discoveryResult.Projects)
158
+ {
159
+ await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
160
+ await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath, replacementFileName: "packages.config");
161
+ // TODO: include global.json, etc.
148
162
  }
149
163
 
150
164
  // do update
@@ -166,7 +180,6 @@ public class RunWorker
166
180
  continue;
167
181
  }
168
182
 
169
- var analyzeWorker = new AnalyzeWorker(_logger);
170
183
  var dependencyInfo = new DependencyInfo()
171
184
  {
172
185
  Name = dependency.Name,
@@ -175,16 +188,18 @@ public class RunWorker
175
188
  IgnoredVersions = [],
176
189
  Vulnerabilities = [],
177
190
  };
178
- var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
191
+ var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
179
192
  // TODO: log analysisResult
180
193
  if (analysisResult.CanUpdate)
181
194
  {
182
- var dependencyLocation = Path.GetFullPath(Path.Join(discoveryResult.Path, project.FilePath).NormalizePathToUnix().EnsurePrefix("/"));
195
+ var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath);
183
196
  if (dependency.Type == DependencyType.PackagesConfig)
184
197
  {
185
- dependencyLocation = Path.Combine(Path.GetDirectoryName(dependencyLocation)!, "packages.config");
198
+ dependencyLocation = Path.Join(Path.GetDirectoryName(dependencyLocation)!, "packages.config");
186
199
  }
187
200
 
201
+ dependencyLocation = dependencyLocation.FullyNormalizedRootedPath();
202
+
188
203
  // TODO: this is inefficient, but not likely causing a bottleneck
189
204
  var previousDependency = discoveredUpdatedDependencies.Dependencies
190
205
  .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation);
@@ -201,7 +216,7 @@ public class RunWorker
201
216
  Groups = previousDependency.Requirements.Single().Groups,
202
217
  Source = new RequirementSource()
203
218
  {
204
- SourceUrl = analysisResult.UpdatedDependencies.Single(d => d.Name == dependency.Name).InfoUrl,
219
+ SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl,
205
220
  },
206
221
  }
207
222
  ],
@@ -209,9 +224,8 @@ public class RunWorker
209
224
  PreviousRequirements = previousDependency.Requirements,
210
225
  };
211
226
 
212
- var updateWorker = new UpdaterWorker(_logger);
213
- var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).NormalizePathToUnix();
214
- var updateResult = await updateWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: false);
227
+ var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
228
+ var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: false);
215
229
  // TODO: need to report if anything was actually updated
216
230
  if (updateResult.ErrorType is null || updateResult.ErrorType == ErrorType.None)
217
231
  {
@@ -228,44 +242,41 @@ public class RunWorker
228
242
 
229
243
  // create PR - we need to manually check file contents; we can't easily use `git status` in tests
230
244
  var updatedDependencyFiles = new List<DependencyFile>();
231
- foreach (var project in discoveryResult.Projects)
245
+ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName, string? replacementFileName = null)
232
246
  {
233
- var projectPath = Path.Join(discoveryResult.Path, project.FilePath).NormalizePathToUnix().EnsurePrefix("/");
234
- var localProjectPath = Path.Join(repoContentsPath.FullName, discoveryResult.Path, project.FilePath);
235
- var updatedProjectContent = await File.ReadAllTextAsync(localProjectPath);
236
- var originalProjectContent = originalDependencyFileContents[projectPath];
237
-
238
- if (updatedProjectContent != originalProjectContent)
247
+ var repoFullPath = Path.Join(directory, fileName);
248
+ if (replacementFileName is not null)
239
249
  {
240
- updatedDependencyFiles.Add(new DependencyFile()
241
- {
242
- Name = project.FilePath,
243
- Content = updatedProjectContent,
244
- Directory = Path.GetDirectoryName(projectPath)!.NormalizeUnixPathParts(),
245
- });
250
+ repoFullPath = Path.Join(Path.GetDirectoryName(repoFullPath)!, replacementFileName);
246
251
  }
247
252
 
248
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
249
- var packagesConfigPath = Path.Join(repoContentsPath.FullName, discoveryResult.Path, projectDirectory, "packages.config");
250
- var normalizedPackagesConfigPath = Path.Join(discoveryResult.Path, projectDirectory, "packages.config").NormalizePathToUnix().EnsurePrefix("/");
253
+ repoFullPath = repoFullPath.FullyNormalizedRootedPath();
254
+ var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
251
255
 
252
- if (File.Exists(packagesConfigPath))
256
+ if (!File.Exists(localFullPath))
253
257
  {
254
- var updatedPackagesConfigContent = await File.ReadAllTextAsync(packagesConfigPath);
255
- var originalPackagesConfigContent = originalDependencyFileContents[normalizedPackagesConfigPath];
258
+ return;
259
+ }
256
260
 
257
- if (updatedPackagesConfigContent != originalPackagesConfigContent)
261
+ var originalContent = originalDependencyFileContents[repoFullPath];
262
+ var updatedContent = await File.ReadAllTextAsync(localFullPath);
263
+ if (updatedContent != originalContent)
264
+ {
265
+ updatedDependencyFiles.Add(new DependencyFile()
258
266
  {
259
- updatedDependencyFiles.Add(new DependencyFile()
260
- {
261
- Name = Path.Join(projectDirectory!, "packages.config"),
262
- Content = updatedPackagesConfigContent,
263
- Directory = Path.GetDirectoryName(normalizedPackagesConfigPath)!.NormalizeUnixPathParts(),
264
- });
265
- }
267
+ Name = Path.GetFileName(repoFullPath),
268
+ Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
269
+ Content = updatedContent,
270
+ });
266
271
  }
267
272
  }
268
273
 
274
+ foreach (var project in discoveryResult.Projects)
275
+ {
276
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
277
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath, replacementFileName: "packages.config");
278
+ }
279
+
269
280
  if (updatedDependencyFiles.Count > 0)
270
281
  {
271
282
  var createPullRequest = new CreatePullRequest()
@@ -292,11 +303,15 @@ public class RunWorker
292
303
 
293
304
  var result = new RunResult()
294
305
  {
295
- Base64DependencyFiles = originalDependencyFileContents.Select(kvp => new DependencyFile()
306
+ Base64DependencyFiles = originalDependencyFileContents.Select(kvp =>
296
307
  {
297
- Name = Path.GetFileName(kvp.Key),
298
- Content = Convert.ToBase64String(Encoding.UTF8.GetBytes(kvp.Value)),
299
- Directory = Path.GetFullPath(Path.GetDirectoryName(kvp.Key)!).NormalizePathToUnix(),
308
+ var fullPath = kvp.Key.FullyNormalizedRootedPath();
309
+ return new DependencyFile()
310
+ {
311
+ Name = Path.GetFileName(fullPath),
312
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes(kvp.Value)),
313
+ Directory = Path.GetDirectoryName(fullPath)!.NormalizePathToUnix(),
314
+ };
300
315
  }).ToArray(),
301
316
  BaseCommitSha = baseCommitSha,
302
317
  };
@@ -308,7 +323,7 @@ public class RunWorker
308
323
  string GetFullRepoPath(string path)
309
324
  {
310
325
  // ensures `path\to\file` is `/path/to/file`
311
- return Path.Join(discoveryResult.Path, path).NormalizePathToUnix().NormalizeUnixPathParts().EnsurePrefix("/");
326
+ return Path.Join(discoveryResult.Path, path).FullyNormalizedRootedPath();
312
327
  }
313
328
 
314
329
  var auxiliaryFiles = new List<string>();
@@ -328,7 +343,7 @@ public class RunWorker
328
343
  foreach (var project in discoveryResult.Projects)
329
344
  {
330
345
  var projectDirectory = Path.GetDirectoryName(project.FilePath);
331
- var pathToPackagesConfig = Path.Join(pathToContents, discoveryResult.Path, projectDirectory, "packages.config").NormalizePathToUnix().EnsurePrefix("/");
346
+ var pathToPackagesConfig = Path.Join(pathToContents, discoveryResult.Path, projectDirectory, "packages.config");
332
347
 
333
348
  if (File.Exists(pathToPackagesConfig))
334
349
  {
@@ -346,7 +361,9 @@ public class RunWorker
346
361
  Name = d.Name,
347
362
  Requirements = d.IsTransitive ? [] : [new ReportedRequirement()
348
363
  {
349
- File = d.Type == DependencyType.PackagesConfig ? Path.Combine(Path.GetDirectoryName(GetFullRepoPath(p.FilePath))!, "packages.config"): GetFullRepoPath(p.FilePath),
364
+ File = d.Type == DependencyType.PackagesConfig
365
+ ? Path.Join(Path.GetDirectoryName(GetFullRepoPath(p.FilePath))!, "packages.config").FullyNormalizedRootedPath()
366
+ : GetFullRepoPath(p.FilePath),
350
367
  Requirement = d.Version!,
351
368
  Groups = ["dependencies"],
352
369
  }],
@@ -26,6 +26,7 @@ internal static class PackageReferenceUpdater
26
26
  string previousDependencyVersion,
27
27
  string newDependencyVersion,
28
28
  bool isTransitive,
29
+ ExperimentsManager experimentsManager,
29
30
  ILogger logger)
30
31
  {
31
32
  // PackageReference project; modify the XML directly
@@ -42,11 +43,7 @@ internal static class PackageReferenceUpdater
42
43
  }
43
44
 
44
45
  var peerDependencies = await GetUpdatedPeerDependenciesAsync(repoRootPath, projectPath, tfms, dependencyName, newDependencyVersion, logger);
45
- if (MSBuildHelper.UseNewDependencySolver())
46
- {
47
- await UpdateDependencyWithConflictResolution(repoRootPath, buildFiles, tfms, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, peerDependencies, logger);
48
- }
49
- else
46
+ if (experimentsManager.UseLegacyDependencySolver)
50
47
  {
51
48
  if (isTransitive)
52
49
  {
@@ -62,6 +59,10 @@ internal static class PackageReferenceUpdater
62
59
  await UpdateTopLevelDepdendency(repoRootPath, buildFiles, tfms, dependencyName, previousDependencyVersion, newDependencyVersion, peerDependencies, logger);
63
60
  }
64
61
  }
62
+ else
63
+ {
64
+ await UpdateDependencyWithConflictResolution(repoRootPath, buildFiles, tfms, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, peerDependencies, logger);
65
+ }
65
66
 
66
67
  if (!await AreDependenciesCoherentAsync(repoRootPath, projectPath, dependencyName, logger, buildFiles, tfms))
67
68
  {
@@ -673,11 +674,21 @@ internal static class PackageReferenceUpdater
673
674
  ProjectBuildFile buildFile,
674
675
  string packageName)
675
676
  => buildFile.PackageItemNodes.Where(e =>
676
- string.Equals(
677
- (e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase))?.Trim(),
678
- packageName,
679
- StringComparison.OrdinalIgnoreCase) &&
680
- (e.GetAttributeOrSubElementValue("Version", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("VersionOverride", StringComparison.OrdinalIgnoreCase)) is not null);
677
+ {
678
+ // Attempt to get "Include" or "Update" attribute values
679
+ var includeOrUpdateValue = e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase)
680
+ ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase);
681
+ // Trim and split if there's a valid value
682
+ var packageNames = includeOrUpdateValue?
683
+ .Trim()
684
+ .Split(';', StringSplitOptions.RemoveEmptyEntries)
685
+ .Select(t => t.Trim())
686
+ .Where(t => t.Equals(packageName.Trim(), StringComparison.OrdinalIgnoreCase));
687
+ // Check if there's a matching package name and a non-null version attribute
688
+ return packageNames?.Any() == true &&
689
+ (e.GetAttributeOrSubElementValue("Version", StringComparison.OrdinalIgnoreCase)
690
+ ?? e.GetAttributeOrSubElementValue("VersionOverride", StringComparison.OrdinalIgnoreCase)) is not null;
691
+ });
681
692
 
682
693
  private static async Task<bool> AreDependenciesCoherentAsync(string repoRootPath, string projectPath, string dependencyName, ILogger logger, ImmutableArray<ProjectBuildFile> buildFiles, string[] tfms)
683
694
  {
@@ -1,5 +1,6 @@
1
1
  using System.Diagnostics;
2
2
  using System.Text;
3
+ using System.Text.RegularExpressions;
3
4
  using System.Xml.Linq;
4
5
  using System.Xml.XPath;
5
6
 
@@ -213,8 +214,8 @@ internal static class PackagesConfigUpdater
213
214
  var hintPathSubString = $"{dependencyName}.{dependencyVersion}";
214
215
 
215
216
  string? partialPathMatch = null;
216
- var hintPathNodes = projectBuildFile.Contents.Descendants().Where(e => e.IsHintPathNodeForDependency(dependencyName));
217
- foreach (var hintPathNode in hintPathNodes)
217
+ var specificHintPathNodes = projectBuildFile.Contents.Descendants().Where(e => e.IsHintPathNodeForDependency(dependencyName)).ToArray();
218
+ foreach (var hintPathNode in specificHintPathNodes)
218
219
  {
219
220
  var hintPath = hintPathNode.GetContentValue();
220
221
  var hintPathSubStringLocation = hintPath.IndexOf(hintPathSubString, StringComparison.OrdinalIgnoreCase);
@@ -255,18 +256,49 @@ internal static class PackagesConfigUpdater
255
256
  if (hasPackage)
256
257
  {
257
258
  // the dependency exists in the packages.config file, so it must be the second case
258
- // the vast majority of projects found in the wild use this, and since we have nothing to look for, we'll just have to hope
259
- partialPathMatch = "../packages";
259
+ // at this point there's no perfect way to determine what the packages path is, but there's a really good chance that
260
+ // for any given package it looks something like this:
261
+ // ..\..\packages\Package.Name.[version]\lib\Tfm\Package.Name.dll
262
+ var genericHintPathNodes = projectBuildFile.Contents.Descendants().Where(IsHintPathNode).ToArray();
263
+ if (genericHintPathNodes.Length > 0)
264
+ {
265
+ foreach (var hintPathNode in genericHintPathNodes)
266
+ {
267
+ var hintPath = hintPathNode.GetContentValue();
268
+ var match = Regex.Match(hintPath, @"^(?<PackagesPath>.*)[/\\](?<PackageNameAndVersion>[^/\\]+)[/\\]lib[/\\](?<Tfm>[^/\\]+)[/\\](?<AssemblyName>[^/\\]+)$");
269
+ // e.g., ..\..\packages \ Some.Package.1.2.3 \ lib\ net45 \ Some.Package.dll
270
+ if (match.Success)
271
+ {
272
+ partialPathMatch = match.Groups["PackagesPath"].Value;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ else
278
+ {
279
+ // we know the dependency is used, but we have absolutely no idea where the packages path is, so we'll default to something reasonable
280
+ partialPathMatch = "../packages";
281
+ }
260
282
  }
261
283
  }
262
284
 
263
285
  return partialPathMatch?.NormalizePathToUnix();
264
286
  }
265
287
 
266
- private static bool IsHintPathNodeForDependency(this IXmlElementSyntax element, string dependencyName)
288
+ private static bool IsHintPathNode(this IXmlElementSyntax element)
267
289
  {
268
290
  if (element.Name.Equals("HintPath", StringComparison.OrdinalIgnoreCase) &&
269
291
  element.Parent.Name.Equals("Reference", StringComparison.OrdinalIgnoreCase))
292
+ {
293
+ return true;
294
+ }
295
+
296
+ return false;
297
+ }
298
+
299
+ private static bool IsHintPathNodeForDependency(this IXmlElementSyntax element, string dependencyName)
300
+ {
301
+ if (element.IsHintPathNode())
270
302
  {
271
303
  // the include attribute will look like one of the following:
272
304
  // <Reference Include="Some.Dependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=abcd">
@@ -7,8 +7,9 @@ using NuGetUpdater.Core.Updater;
7
7
 
8
8
  namespace NuGetUpdater.Core;
9
9
 
10
- public class UpdaterWorker
10
+ public class UpdaterWorker : IUpdaterWorker
11
11
  {
12
+ private readonly ExperimentsManager _experimentsManager;
12
13
  private readonly ILogger _logger;
13
14
  private readonly HashSet<string> _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase);
14
15
 
@@ -18,8 +19,9 @@ public class UpdaterWorker
18
19
  Converters = { new JsonStringEnumConverter() },
19
20
  };
20
21
 
21
- public UpdaterWorker(ILogger logger)
22
+ public UpdaterWorker(ExperimentsManager experimentsManager, ILogger logger)
22
23
  {
24
+ _experimentsManager = experimentsManager;
23
25
  _logger = logger;
24
26
  }
25
27
 
@@ -221,7 +223,7 @@ public class UpdaterWorker
221
223
  }
222
224
 
223
225
  // Some repos use a mix of packages.config and PackageReference
224
- await PackageReferenceUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _logger);
226
+ await PackageReferenceUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _experimentsManager, _logger);
225
227
 
226
228
  // Update lock file if exists
227
229
  if (File.Exists(Path.Combine(Path.GetDirectoryName(projectPath), "packages.lock.json")))
@@ -261,7 +261,11 @@ internal static partial class MSBuildHelper
261
261
  ? evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')')
262
262
  : evaluationResult.EvaluatedValue;
263
263
 
264
- yield return new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate);
264
+ // If at this point we have a semicolon in the name then split it and yield multiple dependencies.
265
+ foreach (var splitName in name.Split(';', StringSplitOptions.RemoveEmptyEntries))
266
+ {
267
+ yield return new Dependency(splitName.Trim(), packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate);
268
+ }
265
269
  }
266
270
  }
267
271
 
@@ -335,11 +339,6 @@ internal static partial class MSBuildHelper
335
339
  }
336
340
  }
337
341
 
338
- internal static bool UseNewDependencySolver()
339
- {
340
- return Environment.GetEnvironmentVariable("UseNewNugetPackageResolver") == "true";
341
- }
342
-
343
342
  internal static async Task<Dependency[]?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, ILogger logger)
344
343
  {
345
344
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
@@ -63,6 +63,8 @@ internal static class PathHelper
63
63
  return result;
64
64
  }
65
65
 
66
+ public static string FullyNormalizedRootedPath(this string path) => path.NormalizePathToUnix().NormalizeUnixPathParts().EnsurePrefix("/");
67
+
66
68
  public static string GetFullPathFromRelative(string rootPath, string relativePath)
67
69
  => Path.GetFullPath(JoinPath(rootPath, relativePath.NormalizePathToUnix()));
68
70
 
@@ -85,6 +87,55 @@ internal static class PathHelper
85
87
  return candidatePaths.ToArray();
86
88
  }
87
89
 
90
+ /// <summary>
91
+ /// Resolves the case of the file path in a case-insensitive manner. Returns null if the file path is not found. file path must be a full path inside the repoRootPath.
92
+ /// </summary>
93
+ /// <param name="filePath">The file path to resolve.</param>
94
+ /// <param name="repoRootPath">The root path of the repository.</param>
95
+ public static string? ResolveCaseInsensitivePathInsideRepoRoot(string filePath, string repoRootPath)
96
+ {
97
+ if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(repoRootPath))
98
+ {
99
+ return null; // Invalid input
100
+ }
101
+
102
+ // Normalize paths
103
+ var normalizedFilePath = filePath.FullyNormalizedRootedPath();
104
+ var normalizedRepoRoot = repoRootPath.FullyNormalizedRootedPath();
105
+
106
+ // Ensure the file path starts with the repo root path
107
+ if (!normalizedFilePath.StartsWith(normalizedRepoRoot + "/", StringComparison.OrdinalIgnoreCase))
108
+ {
109
+ return null; // filePath is outside of repoRootPath
110
+ }
111
+
112
+ // Start resolving from the root path
113
+ var currentPath = normalizedRepoRoot;
114
+ var relativePath = normalizedFilePath.Substring(normalizedRepoRoot.Length).TrimStart('/');
115
+
116
+ foreach (var part in relativePath.Split('/'))
117
+ {
118
+ if (string.IsNullOrEmpty(part))
119
+ {
120
+ continue;
121
+ }
122
+
123
+ // Enumerate the current directory to find a case-insensitive match
124
+ var nextPath = Directory
125
+ .EnumerateFileSystemEntries(currentPath)
126
+ .FirstOrDefault(entry => string.Equals(Path.GetFileName(entry), part, StringComparison.OrdinalIgnoreCase));
127
+
128
+ if (nextPath == null)
129
+ {
130
+ return null; // Part of the path does not exist
131
+ }
132
+
133
+ currentPath = nextPath;
134
+ }
135
+
136
+ return currentPath; // Fully resolved path with correct casing
137
+ }
138
+
88
139
  /// <summary>
89
140
  /// Check in every directory from <paramref name="initialPath"/> up to <paramref name="rootPath"/> for the file specified in <paramref name="fileName"/>.
90
141
  /// </summary>