dependabot-nuget 0.285.0 → 0.287.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 (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>