dependabot-nuget 0.351.0 → 0.353.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +17 -43
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +102 -46
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +0 -3
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyGroup.cs +19 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +23 -2
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +4 -2
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/OutOfDisk.cs +9 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/IApiHandler.cs +11 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +25 -4
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandler.cs +2 -2
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/GroupUpdateAllVersionsHandler.cs +48 -35
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandler.cs +3 -3
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshSecurityUpdatePullRequestHandler.cs +2 -2
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshVersionUpdatePullRequestHandler.cs +2 -2
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +20 -23
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +41 -1
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +93 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +2 -5
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +21 -9
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +51 -96
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/SdkProjectDiscoveryTests.cs +1 -66
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/ApiModel/JobTests.cs +39 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/EndToEndTests.cs +142 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/HttpApiHandlerTests.cs +1 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/JobErrorBaseTests.cs +7 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MessageReportTests.cs +11 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs +76 -7
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +8 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/GroupUpdateAllVersionsHandlerTests.cs +242 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackageReferenceUpdaterTests.cs +30 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +25 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/PathHelperTests.cs +250 -0
  34. metadata +5 -4
@@ -51,19 +51,12 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
51
51
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
52
52
  foreach (var group in job.DependencyGroups)
53
53
  {
54
- var existingGroupPr = job.ExistingGroupPullRequests.FirstOrDefault(pr => pr.DependencyGroupName == group.Name);
55
- if (existingGroupPr is not null)
56
- {
57
- logger.Info($"Existing pull request found for group {group.Name}. Skipping pull request creation.");
58
- continue;
59
- }
60
-
61
54
  logger.Info($"Starting update for group {group.Name}");
62
55
  var groupMatcher = group.GetGroupMatcher();
63
56
  var updateOperationsPerformed = new List<UpdateOperationBase>();
64
57
  var updatedDependencies = new List<ReportedDependency>();
65
58
  var allUpdatedDependencyFiles = ImmutableArray.Create<DependencyFile>();
66
- foreach (var directory in job.GetAllDirectories())
59
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
67
60
  {
68
61
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
69
62
  logger.ReportDiscovery(discoveryResult);
@@ -98,7 +91,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
98
91
  continue;
99
92
  }
100
93
 
101
- var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, allowCooldown: true);
94
+ var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, groupMatchers: [groupMatcher], allowCooldown: true);
102
95
  var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
103
96
  if (analysisResult.Error is not null)
104
97
  {
@@ -146,19 +139,29 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
146
139
 
147
140
  if (updateOperationsPerformed.Count > 0)
148
141
  {
149
- var commitMessage = PullRequestTextGenerator.GetPullRequestCommitMessage(job, [.. updateOperationsPerformed], group.Name);
150
- var prTitle = PullRequestTextGenerator.GetPullRequestTitle(job, [.. updateOperationsPerformed], group.Name);
151
- var prBody = await PullRequestTextGenerator.GetPullRequestBodyAsync(job, [.. updateOperationsPerformed], [.. updatedDependencies], experimentsManager);
152
- await apiHandler.CreatePullRequest(new CreatePullRequest()
142
+ var existingPullRequest = job.GetExistingPullRequestForDependencies(
143
+ dependencies: updatedDependencies.Select(d => new Dependency(d.Name, d.Version, DependencyType.Unknown)),
144
+ considerVersions: true);
145
+ if (existingPullRequest is not null)
153
146
  {
154
- Dependencies = [.. updatedDependencies],
155
- UpdatedDependencyFiles = [.. allUpdatedDependencyFiles],
156
- BaseCommitSha = baseCommitSha,
157
- CommitMessage = commitMessage,
158
- PrTitle = prTitle,
159
- PrBody = prBody,
160
- DependencyGroup = group.Name,
161
- });
147
+ logger.Info($"Pull request already exists for {string.Join(", ", existingPullRequest!.Item2.Select(d => $"{d.DependencyName}/{d.DependencyVersion}"))}");
148
+ }
149
+ else
150
+ {
151
+ var commitMessage = PullRequestTextGenerator.GetPullRequestCommitMessage(job, [.. updateOperationsPerformed], group.Name);
152
+ var prTitle = PullRequestTextGenerator.GetPullRequestTitle(job, [.. updateOperationsPerformed], group.Name);
153
+ var prBody = await PullRequestTextGenerator.GetPullRequestBodyAsync(job, [.. updateOperationsPerformed], [.. updatedDependencies], experimentsManager);
154
+ await apiHandler.CreatePullRequest(new CreatePullRequest()
155
+ {
156
+ Dependencies = [.. updatedDependencies],
157
+ UpdatedDependencyFiles = [.. allUpdatedDependencyFiles],
158
+ BaseCommitSha = baseCommitSha,
159
+ CommitMessage = commitMessage,
160
+ PrTitle = prTitle,
161
+ PrBody = prBody,
162
+ DependencyGroup = group.Name,
163
+ });
164
+ }
162
165
  }
163
166
  }
164
167
  }
@@ -166,7 +169,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
166
169
  private async Task RunUngroupedDependencyUpdates(Job job, DirectoryInfo originalRepoContentsPath, DirectoryInfo? caseInsensitiveRepoContentsPath, string baseCommitSha, IDiscoveryWorker discoveryWorker, IAnalyzeWorker analyzeWorker, IUpdaterWorker updaterWorker, IApiHandler apiHandler, ExperimentsManager experimentsManager, ILogger logger)
167
170
  {
168
171
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
169
- foreach (var directory in job.GetAllDirectories())
172
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
170
173
  {
171
174
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
172
175
  logger.ReportDiscovery(discoveryResult);
@@ -210,7 +213,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
210
213
  continue;
211
214
  }
212
215
 
213
- var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, allowCooldown: true);
216
+ var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, groupMatchers: [], allowCooldown: true);
214
217
  var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
215
218
  if (analysisResult.Error is not null)
216
219
  {
@@ -254,19 +257,29 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
254
257
  var updatedDependencyFiles = await tracker.StopTrackingAsync(restoreOriginalContents: true);
255
258
  if (updateOperationsPerformed.Count > 0)
256
259
  {
257
- var commitMessage = PullRequestTextGenerator.GetPullRequestCommitMessage(job, [.. updateOperationsPerformed], null);
258
- var prTitle = PullRequestTextGenerator.GetPullRequestTitle(job, [.. updateOperationsPerformed], null);
259
- var prBody = await PullRequestTextGenerator.GetPullRequestBodyAsync(job, [.. updateOperationsPerformed], [.. updatedDependencies], experimentsManager);
260
- await apiHandler.CreatePullRequest(new CreatePullRequest()
260
+ var existingPullRequest = job.GetExistingPullRequestForDependencies(
261
+ dependencies: updatedDependencies.Select(d => new Dependency(d.Name, d.Version, DependencyType.Unknown)),
262
+ considerVersions: true);
263
+ if (existingPullRequest is not null)
261
264
  {
262
- Dependencies = [.. updatedDependencies],
263
- UpdatedDependencyFiles = [.. updatedDependencyFiles],
264
- BaseCommitSha = baseCommitSha,
265
- CommitMessage = commitMessage,
266
- PrTitle = prTitle,
267
- PrBody = prBody,
268
- DependencyGroup = null,
269
- });
265
+ logger.Info($"Pull request already exists for {string.Join(", ", existingPullRequest!.Item2.Select(d => $"{d.DependencyName}/{d.DependencyVersion}"))}");
266
+ }
267
+ else
268
+ {
269
+ var commitMessage = PullRequestTextGenerator.GetPullRequestCommitMessage(job, [.. updateOperationsPerformed], null);
270
+ var prTitle = PullRequestTextGenerator.GetPullRequestTitle(job, [.. updateOperationsPerformed], null);
271
+ var prBody = await PullRequestTextGenerator.GetPullRequestBodyAsync(job, [.. updateOperationsPerformed], [.. updatedDependencies], experimentsManager);
272
+ await apiHandler.CreatePullRequest(new CreatePullRequest()
273
+ {
274
+ Dependencies = [.. updatedDependencies],
275
+ UpdatedDependencyFiles = [.. updatedDependencyFiles],
276
+ BaseCommitSha = baseCommitSha,
277
+ CommitMessage = commitMessage,
278
+ PrTitle = prTitle,
279
+ PrBody = prBody,
280
+ DependencyGroup = null,
281
+ });
282
+ }
270
283
  }
271
284
  }
272
285
  }
@@ -21,7 +21,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
21
21
  return false;
22
22
  }
23
23
 
24
- if (job.GetAllDirectories().Length > 1)
24
+ if (job.GetRawDirectories().Length > 1)
25
25
  {
26
26
  return true;
27
27
  }
@@ -62,7 +62,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
62
62
 
63
63
  var groupMatcher = group.GetGroupMatcher();
64
64
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
65
- foreach (var directory in job.GetAllDirectories())
65
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
66
66
  {
67
67
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
68
68
  logger.ReportDiscovery(discoveryResult);
@@ -93,7 +93,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
93
93
  var dependencyName = dependencyGroupToUpdate.Key;
94
94
  var relevantDependenciesToUpdate = dependencyGroupToUpdate.Value
95
95
  .Where(o => !job.IsDependencyIgnoredByNameOnly(o.Dependency.Name))
96
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: true)))
96
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [groupMatcher], allowCooldown: true)))
97
97
  .ToArray();
98
98
 
99
99
  foreach (var (projectPath, dependency, dependencyInfo) in relevantDependenciesToUpdate)
@@ -30,7 +30,7 @@ internal class RefreshSecurityUpdatePullRequestHandler : IUpdateHandler
30
30
  {
31
31
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
32
32
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
33
- foreach (var directory in job.GetAllDirectories())
33
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
34
34
  {
35
35
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
36
36
  logger.ReportDiscovery(discoveryResult);
@@ -82,7 +82,7 @@ internal class RefreshSecurityUpdatePullRequestHandler : IUpdateHandler
82
82
  var dependencyName = dependencyGroupToUpdate.Key;
83
83
  var vulnerableDependenciesToUpdate = dependencyGroupToUpdate.Value
84
84
  .Where(o => !job.IsDependencyIgnoredByNameOnly(o.Dependency.Name))
85
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: false)))
85
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [], allowCooldown: false)))
86
86
  .Where(set => set.Item3.IsVulnerable)
87
87
  .ToArray();
88
88
 
@@ -30,7 +30,7 @@ internal class RefreshVersionUpdatePullRequestHandler : IUpdateHandler
30
30
  {
31
31
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
32
32
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
33
- foreach (var directory in job.GetAllDirectories())
33
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
34
34
  {
35
35
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
36
36
  logger.ReportDiscovery(discoveryResult);
@@ -81,7 +81,7 @@ internal class RefreshVersionUpdatePullRequestHandler : IUpdateHandler
81
81
  var dependencyName = dependencyUpdatesToPerform.Key;
82
82
  var dependencyInfosToUpdate = dependencyUpdatesToPerform.Value
83
83
  .Where(o => !job.IsDependencyIgnoredByNameOnly(o.Dependency.Name))
84
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: true)))
84
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [], allowCooldown: true)))
85
85
  .ToArray();
86
86
 
87
87
  foreach (var (projectPath, dependency, dependencyInfo) in dependencyInfosToUpdate)
@@ -108,7 +108,7 @@ internal static class PackageReferenceUpdater
108
108
  return [.. updateOperations];
109
109
  }
110
110
 
111
- private static async Task<(Dictionary<string, HashSet<string>> PackageParents, Dictionary<string, NuGetVersion> PackageVersions)> GetPackageGraphForDependencies(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> topLevelDependencies, ILogger logger)
111
+ internal static async Task<(Dictionary<string, HashSet<string>> PackageParents, Dictionary<string, NuGetVersion> PackageVersions)> GetPackageGraphForDependencies(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> topLevelDependencies, ILogger logger)
112
112
  {
113
113
  var packageParents = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
114
114
  var packageVersions = new Dictionary<string, NuGetVersion>(StringComparer.OrdinalIgnoreCase);
@@ -117,37 +117,34 @@ internal static class PackageReferenceUpdater
117
117
  {
118
118
  // generate project.assets.json
119
119
  var parsedTargetFramework = NuGetFramework.Parse(targetFramework);
120
- var tempProject = await MSBuildHelper.CreateTempProjectAsync(tempDir, repoRoot, projectPath, targetFramework, topLevelDependencies, logger, importDependencyTargets: false);
121
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["build", tempProject, "/t:_ReportDependencies"], tempDir.FullName);
120
+ var tempProject = await MSBuildHelper.CreateTempProjectAsync(tempDir, repoRoot, projectPath, targetFramework, topLevelDependencies, logger, importDependencyTargets: true);
121
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["msbuild", tempProject, "/t:Restore,GenerateBuildDependencyFile"], tempDir.FullName);
122
122
  var assetsJsonPath = Path.Join(tempDir.FullName, "obj", "project.assets.json");
123
123
  var assetsJsonContent = await File.ReadAllTextAsync(assetsJsonPath);
124
124
 
125
125
  // build reverse dependency graph
126
126
  var assets = JsonDocument.Parse(assetsJsonContent).RootElement;
127
- foreach (var tfmObject in assets.GetProperty("targets").EnumerateObject())
127
+ var tfmObjects = assets.GetProperty("targets").EnumerateObject().ToImmutableArray();
128
+ if (tfmObjects.Length != 1)
128
129
  {
129
- var reportedTargetFramework = NuGetFramework.Parse(tfmObject.Name);
130
- if (reportedTargetFramework != parsedTargetFramework)
131
- {
132
- // not interested in this target framework
133
- continue;
134
- }
130
+ logger.Error($"Expected exactly one target framework group compatible with {targetFramework} but found {tfmObjects.Length}. Values: {tfmObjects.Select(t => t.Name)}");
131
+ return (packageParents, packageVersions);
132
+ }
135
133
 
136
- foreach (var parentObject in tfmObject.Value.EnumerateObject())
137
- {
138
- var parts = parentObject.Name.Split('/');
139
- var parentName = parts[0];
140
- var parentVersion = parts[1];
141
- packageVersions[parentName] = NuGetVersion.Parse(parentVersion);
134
+ foreach (var parentObject in tfmObjects[0].Value.EnumerateObject())
135
+ {
136
+ var parts = parentObject.Name.Split('/');
137
+ var parentName = parts[0];
138
+ var parentVersion = parts[1];
139
+ packageVersions[parentName] = NuGetVersion.Parse(parentVersion);
142
140
 
143
- if (parentObject.Value.TryGetProperty("dependencies", out var dependencies))
141
+ if (parentObject.Value.TryGetProperty("dependencies", out var dependencies))
142
+ {
143
+ foreach (var childObject in dependencies.EnumerateObject())
144
144
  {
145
- foreach (var childObject in dependencies.EnumerateObject())
146
- {
147
- var childName = childObject.Name;
148
- var parentSet = packageParents.GetOrAdd(childName, () => new(StringComparer.OrdinalIgnoreCase));
149
- parentSet.Add(parentName);
150
- }
145
+ var childName = childObject.Name;
146
+ var parentSet = packageParents.GetOrAdd(childName, () => new(StringComparer.OrdinalIgnoreCase));
147
+ parentSet.Add(parentName);
151
148
  }
152
149
  }
153
150
  }
@@ -493,7 +493,7 @@ internal static partial class MSBuildHelper
493
493
  var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
494
494
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger, importDependencyTargets: false);
495
495
 
496
- var experimentsManager = new ExperimentsManager() { UseSingleRestore = false }; // single restore is meaningless here
496
+ var experimentsManager = new ExperimentsManager();
497
497
  var projectDiscovery = await SdkProjectDiscovery.DiscoverAsync(repoRoot, tempDirectory.FullName, tempProjectPath, experimentsManager, logger);
498
498
  var allDependencies = projectDiscovery
499
499
  .Where(p => p.FilePath == Path.GetFileName(tempProjectPath))
@@ -545,6 +545,37 @@ internal static partial class MSBuildHelper
545
545
  return targets;
546
546
  }
547
547
 
548
+ internal static async Task<ImmutableArray<string>> GetProjectTargetFrameworksAsync(string projectPath, ILogger logger)
549
+ {
550
+ var extension = Path.GetExtension(projectPath)?.ToLowerInvariant();
551
+ if (extension == ".sln" || extension == ".slnx")
552
+ {
553
+ // solution files don't specify target frameworks, so we can skip the process invocation
554
+ return [];
555
+ }
556
+
557
+ var projectDirectory = Path.GetDirectoryName(projectPath)!;
558
+ var args = new[]
559
+ {
560
+ "msbuild",
561
+ projectPath,
562
+ "-getProperty:TargetFrameworks"
563
+ };
564
+
565
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(args, projectDirectory);
566
+ if (exitCode != 0)
567
+ {
568
+ logger.Warn($"Unable to determine target frameworks for project [{projectPath}]:\nSTDOUT:\n{stdOut}\nSTDERR:\n{stdErr}\n");
569
+ return [];
570
+ }
571
+
572
+ var tfms = Regex.Replace(stdOut, "@[\r\n\t ]", "")
573
+ .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
574
+ .OrderBy(t => t)
575
+ .ToImmutableArray();
576
+ return tfms;
577
+ }
578
+
548
579
  internal static string? GetMissingFile(string output)
549
580
  {
550
581
  var missingFilePatterns = new[]
@@ -571,6 +602,7 @@ internal static partial class MSBuildHelper
571
602
  ThrowOnTimeout(output);
572
603
  ThrowOnBadResponse(output);
573
604
  ThrowOnUnparseableFile(output);
605
+ ThrowOnMultipleProjectsForPackagesConfig(output);
574
606
  }
575
607
 
576
608
  private static void ThrowOnUnauthenticatedFeed(string stdout)
@@ -704,6 +736,14 @@ internal static partial class MSBuildHelper
704
736
  }
705
737
  }
706
738
 
739
+ private static void ThrowOnMultipleProjectsForPackagesConfig(string output)
740
+ {
741
+ if (output.Contains("Found multiple project files for "))
742
+ {
743
+ throw new Exception("Multiple project files found for single packages.config");
744
+ }
745
+ }
746
+
707
747
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
708
748
  {
709
749
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -1,4 +1,6 @@
1
+ using System.Collections.Immutable;
1
2
  using System.Runtime.InteropServices;
3
+ using System.Text;
2
4
  using System.Text.RegularExpressions;
3
5
 
4
6
  namespace NuGetUpdater.Core;
@@ -248,4 +250,95 @@ internal static class PathHelper
248
250
 
249
251
  return candidateFilePath.StartsWith(directoryPath);
250
252
  }
253
+
254
+ public static ImmutableArray<string> GetMatchingDirectoriesUnder(string rootDirectory, string searchPattern, bool caseSensitive)
255
+ {
256
+ // translate pattern to regex
257
+ searchPattern = searchPattern.Replace("\\", "/"); // unix-style paths make things easier
258
+ searchPattern = searchPattern.TrimStart('/'); // pattern shouldn't be rooted
259
+ if (searchPattern == string.Empty)
260
+ {
261
+ searchPattern = "/"; // special case repo root
262
+ }
263
+
264
+ var pb = new StringBuilder();
265
+ pb.Append('^');
266
+ var appendAnchor = true;
267
+ for (int i = 0; i < searchPattern.Length; i++)
268
+ {
269
+ // special case recursive wildcard
270
+ if (searchPattern[i..] == "/**/*")
271
+ {
272
+ pb.Append("($|/.*$)"); // capture just this directory and every subdirectory
273
+ appendAnchor = false;
274
+ break;
275
+ }
276
+
277
+ var c = searchPattern[i];
278
+ switch (c)
279
+ {
280
+ case '*':
281
+ // could be single level or multi-level
282
+ var isRecursiveMatch = i < searchPattern.Length - 2
283
+ && searchPattern[i + 1] == '*'
284
+ && searchPattern[i + 2] == '/';
285
+ if (isRecursiveMatch)
286
+ {
287
+ // match anything
288
+ pb.Append(".*");
289
+ i += 2; // consume the extra characters
290
+ }
291
+ else
292
+ {
293
+ // only match up to a directory separator
294
+ pb.Append("[^/]*");
295
+ }
296
+ break;
297
+ case '?':
298
+ pb.Append(".");
299
+ break;
300
+ case '/':
301
+ pb.Append("/");
302
+ break;
303
+ default:
304
+ if ("+()^$.{}[]|\\".Contains(c))
305
+ {
306
+ pb.Append('\\');
307
+ }
308
+ pb.Append(c);
309
+ break;
310
+ }
311
+ }
312
+
313
+ if (appendAnchor)
314
+ {
315
+ pb.Append('$');
316
+ }
317
+
318
+ var pattern = new Regex(pb.ToString(), caseSensitive ? RegexOptions.IgnoreCase : RegexOptions.None);
319
+
320
+ // find all directories
321
+ var allDirectories = Directory.EnumerateDirectories(rootDirectory, "*", SearchOption.AllDirectories)
322
+ .Select(d => Path.GetRelativePath(rootDirectory, d).NormalizePathToUnix())
323
+ .ToImmutableArray();
324
+
325
+ // filter
326
+ var matchingDirectories = allDirectories.Where(pattern.IsMatch)
327
+ .OrderBy(d => d, caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)
328
+ .Select(d => d.EnsurePrefix("/")) // paths must appear rooted from here on out
329
+ .ToImmutableArray();
330
+
331
+ // special case some well-known directories that are hard to generate patterns for
332
+ switch (searchPattern)
333
+ {
334
+ case "/":
335
+ case ".":
336
+ case "/.":
337
+ case "**/*":
338
+ matchingDirectories = [.. matchingDirectories.Prepend("/")];
339
+ break;
340
+ }
341
+
342
+ return matchingDirectories;
343
+ }
251
344
  }
@@ -61,13 +61,10 @@ public partial class DiscoveryWorkerTests
61
61
  );
62
62
  }
63
63
 
64
- [Theory]
65
- [InlineData(true)]
66
- [InlineData(false)]
67
- public async Task DiscoveryIsMergedWithPackageReferences(bool useSingleRestore)
64
+ [Fact]
65
+ public async Task DiscoveryIsMergedWithPackageReferences()
68
66
  {
69
67
  await TestDiscoveryAsync(
70
- experimentsManager: new ExperimentsManager() { UseSingleRestore = useSingleRestore },
71
68
  packages:
72
69
  [
73
70
  MockNuGetPackage.CreateSimplePackage("Package.A", "1.0.0", "net46"),
@@ -626,7 +626,7 @@ public partial class DiscoveryWorkerTests
626
626
  ("src/project.csproj", """
627
627
  <Project Sdk="Microsoft.NET.Sdk">
628
628
  <PropertyGroup>
629
- <TargetFrameworks>net8.0-ios;net8.0-android;net8.0-macos;net8.0-maccatalyst;net8.0-windows</TargetFrameworks>
629
+ <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-macos;net8.0-windows</TargetFrameworks>
630
630
  </PropertyGroup>
631
631
  <ItemGroup>
632
632
  <PackageReference Include="Some.Package" Version="1.2.3" />
@@ -645,7 +645,7 @@ public partial class DiscoveryWorkerTests
645
645
  new("Some.Package", "1.2.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0-android", "net8.0-ios", "net8.0-maccatalyst", "net8.0-macos", "net8.0-windows"], IsDirect: true),
646
646
  ],
647
647
  Properties = [
648
- new("TargetFrameworks", "net8.0-ios;net8.0-android;net8.0-macos;net8.0-maccatalyst;net8.0-windows", @"src/project.csproj"),
648
+ new("TargetFrameworks", "net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-macos;net8.0-windows", @"src/project.csproj"),
649
649
  ],
650
650
  TargetFrameworks = ["net8.0-android", "net8.0-ios", "net8.0-maccatalyst", "net8.0-macos", "net8.0-windows"],
651
651
  ReferencedProjectPaths = [],
@@ -737,7 +737,7 @@ public partial class DiscoveryWorkerTests
737
737
 
738
738
  // The SDK package handling is detected in a very specific circumstance; an assembly being removed from the
739
739
  // `@(References)` item group in the `_HandlePackageFileConflicts` target. Since we don't want to involve
740
- // the real SDK, we fake some required targets.
740
+ // the real SDK, we fake some required targets in the same shape as the real SDK.
741
741
  await TestDiscoveryAsync(
742
742
  packages: [],
743
743
  workspacePath: "",
@@ -764,6 +764,14 @@ public partial class DiscoveryWorkerTests
764
764
  <Reference Include="@(RuntimeCopyLocalItems)" />
765
765
  </ItemGroup>
766
766
 
767
+ <Target Name="ResolveProjectReferences">
768
+ <!-- this target needs to exist for discovery to work -->
769
+ </Target>
770
+
771
+ <Target Name="Restore">
772
+ <!-- this target needs to exist for discovery to work -->
773
+ </Target>
774
+
767
775
  <Target Name="_HandlePackageFileConflicts">
768
776
  <!-- this target needs to exist for discovery to work -->
769
777
  <ItemGroup>
@@ -775,21 +783,25 @@ public partial class DiscoveryWorkerTests
775
783
  </ItemGroup>
776
784
  </Target>
777
785
 
778
- <Target Name="ResolveAssemblyReferences" DependsOnTargets="_HandlePackageFileConflicts">
786
+ <Target Name="ResolvePackageAssets">
779
787
  <!-- this target needs to exist for discovery to work -->
780
788
  </Target>
781
789
 
782
- <Target Name="GenerateBuildDependencyFile">
790
+ <Target Name="ResolveFrameworkReferences" DependsOnTargets="ResolvePackageAssets">
791
+ <!-- this target needs to exist for discovery to work -->
792
+ </Target>
793
+
794
+ <Target Name="ResolveRuntimePackAssets" DependsOnTargets="ResolveFrameworkReferences">
795
+ <!-- this target needs to exist for discovery to work -->
796
+ </Target>
797
+
798
+ <Target Name="GenerateBuildDependencyFile" DependsOnTargets="_HandlePackageFileConflicts;ResolveRuntimePackAssets">
783
799
  <!-- this target needs to exist for discovery to work -->
784
800
  <ItemGroup>
785
801
  <!-- this removal is what removes the regular package reference from the project -->
786
802
  <RuntimeCopyLocalItems Remove="TestOnlyAssembly.dll" />
787
803
  </ItemGroup>
788
804
  </Target>
789
-
790
- <Target Name="ResolvePackageAssets">
791
- <!-- this target needs to exist for discovery to work -->
792
- </Target>
793
805
  </Project>
794
806
  """)
795
807
  ],