dependabot-nuget 0.398.0 → 0.400.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 (22) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Packages.props +2 -2
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +48 -11
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +31 -2
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/PackageVersionsCache.cs +91 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +8 -25
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Condition.cs +17 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyGroup.cs +37 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +66 -2
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/PullRequestTextGenerator.cs +44 -7
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/GroupUpdateAllVersionsHandler.cs +177 -36
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandler.cs +35 -7
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +107 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs +406 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs +269 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/PullRequestTextTests.cs +32 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +1 -1
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +33 -2
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandlerTests.cs +11 -3
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/GroupUpdateAllVersionsHandlerTests.cs +404 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandlerTests.cs +242 -0
  22. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3df370cfa9828e959bf529b8a391e336d368cc0c87aab8393665a554fa5e4b48
4
- data.tar.gz: d3e16c0df803153b916f442af358e42573b89cefff989e5da52524dad83affe4
3
+ metadata.gz: a29d38c1ed416c5fc9c810f3815f241789054754ff92ac4982bf65e9ef221488
4
+ data.tar.gz: 75c1ce1d121fc582d1ad2edbd1e3a35bb79e6955501984585643e455a85e4934
5
5
  SHA512:
6
- metadata.gz: 0464c48c69bf48bbc512a314c8600b50822defbe997f387ab8f29f54bd9308b8598e0c31d84995c98c266740bb90be3b2e612f0ed69a5d52132f7b57c4c481fd
7
- data.tar.gz: c7efa1a36e6b822254fa4caf97e4c9b891042df0c866c7e34c9e566be1a64e0f4b3635cf6169d0b0d0b7b3e66ad9331fd0076d8c8296bea347cd57c3569a27f1
6
+ metadata.gz: 525d56946331f53dabd8705aa5a903b2489b89e2ebf40a5cc3b514dbf4c28d93633e09a46330d8d0b0509e7feb2439a988a408dd4f01e5ce71cf4b4c6a7cbb41
7
+ data.tar.gz: 2bdbf0b8a2c5302646e50fa666a14b91567dfecc96713472a419f15407c1c7a7628470adec534c50edf342ac4f3356f90840bf1d8c65526f24660d6b005fd9c1
@@ -17,10 +17,10 @@
17
17
  <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
18
18
  <PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
19
19
  <PackageVersion Include="Microsoft.Extensions.FileProviders.Abstractions" Version="10.0.12" />
20
- <PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.11" />
20
+ <PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.12" />
21
21
  <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.12" />
22
22
  <PackageVersion Include="Microsoft.NET.StringTools" Version="$(MSBuildPackageVersion)" />
23
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
23
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
24
24
  <PackageVersion Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="3.14.2075" />
25
25
  <PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
26
26
  <PackageVersion Include="Microsoft.Web.Xdt" Version="3.2.11" />
@@ -20,6 +20,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
20
20
  private readonly string _jobId;
21
21
  private readonly ExperimentsManager _experimentsManager;
22
22
  private readonly ILogger _logger;
23
+ private readonly PackageVersionsCache _packageVersionsCache = new();
23
24
 
24
25
  internal static readonly JsonSerializerOptions SerializerOptions = new()
25
26
  {
@@ -96,7 +97,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
96
97
 
97
98
  bool isProjectUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency);
98
99
  var isUpdateNecessary = isProjectUpdateNecessary || dotnetToolsHasDependency || globalJsonHasDependency;
99
- using var nugetContext = new NuGetContext(startingDirectory);
100
+ using var nugetContext = new NuGetContext(startingDirectory, packageVersionsCache: _packageVersionsCache);
100
101
  AnalysisResult analysisResult;
101
102
  if (isUpdateNecessary)
102
103
  {
@@ -120,6 +121,34 @@ public partial class AnalyzeWorker : IAnalyzeWorker
120
121
  .ToImmutableArray()
121
122
  : projectFrameworks;
122
123
 
124
+ ImmutableArray<Dependency> candidateUpdatedDependencies = [];
125
+ Func<NuGetVersion, Task<bool>>? candidateValidator = null;
126
+ if (isProjectUpdateNecessary)
127
+ {
128
+ candidateValidator = async candidateVersion =>
129
+ {
130
+ try
131
+ {
132
+ candidateUpdatedDependencies = await FindUpdatedDependenciesAsync(
133
+ repoRoot,
134
+ discovery,
135
+ dependenciesToUpdate,
136
+ candidateVersion,
137
+ nugetContext,
138
+ _logger,
139
+ CancellationToken.None);
140
+ return true;
141
+ }
142
+ catch (DependencyNotFoundException ex)
143
+ {
144
+ _logger.Info(
145
+ $" Rejecting version {candidateVersion.ToNormalizedString()} because dependencies could not be found: " +
146
+ $"{string.Join(", ", ex.Dependencies)}");
147
+ return false;
148
+ }
149
+ };
150
+ }
151
+
123
152
  _logger.Info($" Finding updated version.");
124
153
  updatedVersion = await FindUpdatedVersionAsync(
125
154
  startingDirectory,
@@ -128,6 +157,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
128
157
  applicableTargetFrameworks,
129
158
  nugetContext,
130
159
  _logger,
160
+ candidateValidator,
131
161
  CancellationToken.None);
132
162
 
133
163
  _logger.Info($" Finding updated peer dependencies.");
@@ -137,14 +167,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
137
167
  }
138
168
  else if (isProjectUpdateNecessary)
139
169
  {
140
- updatedDependencies = await FindUpdatedDependenciesAsync(
141
- repoRoot,
142
- discovery,
143
- dependenciesToUpdate,
144
- updatedVersion,
145
- nugetContext,
146
- _logger,
147
- CancellationToken.None);
170
+ updatedDependencies = candidateUpdatedDependencies;
148
171
  }
149
172
  else if (dotnetToolsHasDependency)
150
173
  {
@@ -232,6 +255,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
232
255
  ImmutableArray<NuGetFramework> projectFrameworks,
233
256
  NuGetContext nugetContext,
234
257
  ILogger logger,
258
+ Func<NuGetVersion, Task<bool>>? candidateValidator,
235
259
  CancellationToken cancellationToken)
236
260
  {
237
261
  var versionResult = await VersionFinder.GetVersionsAsync(
@@ -250,6 +274,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
250
274
  findLowestVersion: dependencyInfo.IsVulnerable,
251
275
  nugetContext,
252
276
  logger,
277
+ candidateValidator,
253
278
  cancellationToken);
254
279
  }
255
280
 
@@ -261,6 +286,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
261
286
  bool findLowestVersion,
262
287
  NuGetContext nugetContext,
263
288
  ILogger logger,
289
+ Func<NuGetVersion, Task<bool>>? candidateValidator,
264
290
  CancellationToken cancellationToken)
265
291
  {
266
292
  var versions = versionResult.GetVersions();
@@ -282,6 +308,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
282
308
  projectFrameworks,
283
309
  nugetContext,
284
310
  logger,
311
+ candidateValidator,
285
312
  cancellationToken);
286
313
  }
287
314
 
@@ -293,6 +320,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
293
320
  ImmutableArray<NuGetFramework> projectFrameworks,
294
321
  NuGetContext nugetContext,
295
322
  ILogger logger,
323
+ Func<NuGetVersion, Task<bool>>? candidateValidator,
296
324
  CancellationToken cancellationToken)
297
325
  {
298
326
  if (NuGetVersion.TryParse(versionString, out var currentVersion))
@@ -308,7 +336,15 @@ public partial class AnalyzeWorker : IAnalyzeWorker
308
336
  if (!isCompatible)
309
337
  {
310
338
  // If the current package is incompatible, then don't check for compatibility.
311
- return orderedVersions.First();
339
+ foreach (var version in orderedVersions)
340
+ {
341
+ if (candidateValidator is null || await candidateValidator(version))
342
+ {
343
+ return version;
344
+ }
345
+ }
346
+
347
+ return null;
312
348
  }
313
349
  }
314
350
 
@@ -328,7 +364,8 @@ public partial class AnalyzeWorker : IAnalyzeWorker
328
364
  logger,
329
365
  cancellationToken);
330
366
 
331
- if (isCompatible)
367
+ if (isCompatible &&
368
+ (candidateValidator is null || await candidateValidator(version)))
332
369
  {
333
370
  return version;
334
371
  }
@@ -21,11 +21,13 @@ internal record NuGetContext : IDisposable
21
21
  public ImmutableArray<PackageSource> PackageSources { get; }
22
22
  public NuGet.Common.ILogger Logger { get; }
23
23
  private readonly Func<PackageSource, SourceRepository> _sourceRepositoryFactory;
24
+ private readonly PackageVersionsCache _packageVersionsCache;
24
25
 
25
26
  public NuGetContext(
26
27
  string? currentDirectory = null,
27
28
  NuGet.Common.ILogger? logger = null,
28
- Func<PackageSource, SourceRepository>? sourceRepositoryFactory = null)
29
+ Func<PackageSource, SourceRepository>? sourceRepositoryFactory = null,
30
+ PackageVersionsCache? packageVersionsCache = null)
29
31
  {
30
32
  SourceCacheContext = new SourceCacheContext();
31
33
  PackageDownloadContext = new PackageDownloadContext(SourceCacheContext);
@@ -41,6 +43,7 @@ internal record NuGetContext : IDisposable
41
43
  .ToImmutableArray();
42
44
  Logger = logger ?? NullLogger.Instance;
43
45
  _sourceRepositoryFactory = sourceRepositoryFactory ?? Repository.Factory.GetCoreV3;
46
+ _packageVersionsCache = packageVersionsCache ?? new PackageVersionsCache();
44
47
  }
45
48
 
46
49
  public void Dispose()
@@ -71,6 +74,32 @@ internal record NuGetContext : IDisposable
71
74
  return infoUrl;
72
75
  }
73
76
 
77
+ internal SourceRepository GetSourceRepository(PackageSource source) => _sourceRepositoryFactory(source);
78
+
79
+ internal Task<PackageVersions> GetPackageVersionsAsync(
80
+ PackageSource source,
81
+ string packageId,
82
+ bool includePrerelease,
83
+ bool includeUnlisted,
84
+ MetadataResource metadataResource,
85
+ CancellationToken cancellationToken)
86
+ {
87
+ return _packageVersionsCache.GetVersionsAsync(
88
+ CurrentDirectory,
89
+ source,
90
+ packageId,
91
+ includePrerelease,
92
+ includeUnlisted,
93
+ token => metadataResource.GetVersions(
94
+ packageId,
95
+ includePrerelease,
96
+ includeUnlisted,
97
+ SourceCacheContext,
98
+ NullLogger.Instance,
99
+ token),
100
+ cancellationToken);
101
+ }
102
+
74
103
  private async Task<string?> FindPackageInfoUrlAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken)
75
104
  {
76
105
  var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(Settings);
@@ -88,7 +117,7 @@ internal record NuGetContext : IDisposable
88
117
  foreach (var source in sources)
89
118
  {
90
119
  message.AppendLine($" checking {source.Name}");
91
- var sourceRepository = _sourceRepositoryFactory(source);
120
+ var sourceRepository = GetSourceRepository(source);
92
121
  var feed = await sourceRepository.GetResourceAsync<MetadataResource>(cancellationToken);
93
122
  if (feed is null)
94
123
  {
@@ -0,0 +1,91 @@
1
+ using NuGet.Configuration;
2
+ using NuGet.Versioning;
3
+
4
+ namespace NuGetUpdater.Core.Analyze;
5
+
6
+ internal sealed class PackageVersionsCache
7
+ {
8
+ private readonly Dictionary<CacheKey, Task<PackageVersions>> _cache = [];
9
+ private readonly object _cacheLock = new();
10
+
11
+ public async Task<PackageVersions> GetVersionsAsync(
12
+ string currentDirectory,
13
+ PackageSource source,
14
+ string packageId,
15
+ bool includePrerelease,
16
+ bool includeUnlisted,
17
+ Func<CancellationToken, Task<IEnumerable<NuGetVersion>>> getVersionsAsync,
18
+ CancellationToken cancellationToken)
19
+ {
20
+ cancellationToken.ThrowIfCancellationRequested();
21
+
22
+ var key = new CacheKey(
23
+ Path.TrimEndingDirectorySeparator(Path.GetFullPath(currentDirectory)),
24
+ source.Name,
25
+ source.Source,
26
+ packageId.ToLowerInvariant(),
27
+ includePrerelease,
28
+ includeUnlisted);
29
+
30
+ Task<PackageVersions> versionsTask;
31
+ var createdLoad = false;
32
+ lock (_cacheLock)
33
+ {
34
+ if (_cache.TryGetValue(key, out var cachedTask))
35
+ {
36
+ versionsTask = cachedTask;
37
+ }
38
+ else
39
+ {
40
+ versionsTask = LoadVersionsAsync(getVersionsAsync, cancellationToken);
41
+ _cache.Add(key, versionsTask);
42
+ createdLoad = true;
43
+ }
44
+ }
45
+
46
+ try
47
+ {
48
+ return await versionsTask.WaitAsync(cancellationToken);
49
+ }
50
+ catch
51
+ {
52
+ if (!versionsTask.IsCompletedSuccessfully &&
53
+ (createdLoad || versionsTask.IsCanceled || versionsTask.IsFaulted))
54
+ {
55
+ lock (_cacheLock)
56
+ {
57
+ if (_cache.TryGetValue(key, out var cachedTask) && ReferenceEquals(cachedTask, versionsTask))
58
+ {
59
+ _cache.Remove(key);
60
+ }
61
+ }
62
+ }
63
+
64
+ throw;
65
+ }
66
+ }
67
+
68
+ private static async Task<PackageVersions> LoadVersionsAsync(
69
+ Func<CancellationToken, Task<IEnumerable<NuGetVersion>>> getVersionsAsync,
70
+ CancellationToken cancellationToken)
71
+ {
72
+ var versions = await getVersionsAsync(cancellationToken);
73
+ return new PackageVersions(versions.ToHashSet());
74
+ }
75
+
76
+ private sealed record CacheKey(
77
+ string CurrentDirectory,
78
+ string SourceName,
79
+ string SourceUrl,
80
+ string PackageId,
81
+ bool IncludePrerelease,
82
+ bool IncludeUnlisted);
83
+ }
84
+
85
+ internal sealed class PackageVersions(HashSet<NuGetVersion> items)
86
+ {
87
+ public int Count => items.Count;
88
+ public IEnumerable<NuGetVersion> Items => items;
89
+
90
+ public bool Contains(NuGetVersion version) => items.Contains(version);
91
+ }
@@ -6,7 +6,6 @@ using NuGet.Common;
6
6
  using NuGet.Configuration;
7
7
  using NuGet.Frameworks;
8
8
  using NuGet.Packaging.Core;
9
- using NuGet.Protocol;
10
9
  using NuGet.Protocol.Core.Types;
11
10
  using NuGet.Versioning;
12
11
 
@@ -94,25 +93,18 @@ internal static class VersionFinder
94
93
 
95
94
  foreach (var source in sources)
96
95
  {
97
- MetadataResource? feed = null;
98
96
  PackageMetadataResource? metadataResource = null;
97
+ PackageVersions feedVersions;
99
98
  try
100
99
  {
101
- var sourceRepository = Repository.Factory.GetCoreV3(source);
102
- feed = await sourceRepository.GetResourceAsync<MetadataResource>();
100
+ var sourceRepository = nugetContext.GetSourceRepository(source);
101
+ var feed = await sourceRepository.GetResourceAsync<MetadataResource>();
103
102
  if (feed is null)
104
103
  {
105
104
  logger.Warn($"Failed to get {nameof(MetadataResource)} for [{source.Source}]");
106
105
  continue;
107
106
  }
108
107
 
109
- var packageFinder = await sourceRepository.GetResourceAsync<FindPackageByIdResource>();
110
- if (packageFinder is null)
111
- {
112
- logger.Warn($"Failed to get {nameof(FindPackageByIdResource)} for [{source.Source}]");
113
- continue;
114
- }
115
-
116
108
  if (dependencyInfo.Cooldown is not null)
117
109
  {
118
110
  metadataResource = await sourceRepository.GetResourceAsync<PackageMetadataResource>();
@@ -123,15 +115,14 @@ internal static class VersionFinder
123
115
  }
124
116
  }
125
117
 
126
- // a non-compliant v2 API returning 404 can cause this to throw
127
- var existsInFeed = await feed.Exists(
118
+ feedVersions = await nugetContext.GetPackageVersionsAsync(
119
+ source,
128
120
  dependencyInfo.Name,
129
121
  includePrerelease,
130
122
  includeUnlisted: false,
131
- nugetContext.SourceCacheContext,
132
- NullLogger.Instance,
123
+ feed,
133
124
  cancellationToken);
134
- if (!existsInFeed)
125
+ if (feedVersions.Count == 0)
135
126
  {
136
127
  continue;
137
128
  }
@@ -152,20 +143,12 @@ internal static class VersionFinder
152
143
  throw new BadResponseException(ex.Message, source.Source);
153
144
  }
154
145
 
155
- var feedVersions = (await feed.GetVersions(
156
- dependencyInfo.Name,
157
- includePrerelease,
158
- includeUnlisted: false,
159
- nugetContext.SourceCacheContext,
160
- NullLogger.Instance,
161
- CancellationToken.None)).ToHashSet();
162
-
163
146
  if (feedVersions.Contains(currentVersion))
164
147
  {
165
148
  result.AddCurrentVersionSource(source);
166
149
  }
167
150
 
168
- var versions = feedVersions.Where(versionFilter).ToArray();
151
+ var versions = feedVersions.Items.Where(versionFilter).ToArray();
169
152
  foreach (var version in versions)
170
153
  {
171
154
  var packageIdentity = new PackageIdentity(dependencyInfo.Name, version);
@@ -1,11 +1,15 @@
1
1
  using System.Text.Json.Serialization;
2
2
 
3
+ using NuGet.Versioning;
4
+
3
5
  using NuGetUpdater.Core.Analyze;
4
6
 
5
7
  namespace NuGetUpdater.Core.Run.ApiModel;
6
8
 
7
9
  public sealed record Condition
8
10
  {
11
+ private static readonly NuGetVersion MinimumVersion = NuGetVersion.Parse("0");
12
+
9
13
  [JsonPropertyName("dependency-name")]
10
14
  public required string DependencyName { get; init; }
11
15
  [JsonPropertyName("source")]
@@ -16,6 +20,19 @@ public sealed record Condition
16
20
  public DateTime? UpdatedAt { get; init; } = null;
17
21
  [JsonPropertyName("version-requirement")]
18
22
  public Requirement? VersionRequirement { get; init; } = null;
23
+
24
+ internal bool IsUnconditionalIgnore()
25
+ {
26
+ if ((UpdateTypes ?? []).Length > 0)
27
+ {
28
+ return false;
29
+ }
30
+
31
+ // Name-only ignore conditions are serialized by the service with a >= 0 requirement.
32
+ return VersionRequirement is null ||
33
+ VersionRequirement is IndividualRequirement { Operator: ">=", Version: var version } &&
34
+ version == MinimumVersion;
35
+ }
19
36
  }
20
37
 
21
38
  public enum ConditionUpdateType
@@ -1,6 +1,7 @@
1
1
  using System.Collections.Immutable;
2
2
  using System.IO.Enumeration;
3
3
  using System.Text.Json;
4
+ using System.Text.Json.Serialization;
4
5
 
5
6
  using NuGet.Versioning;
6
7
 
@@ -19,6 +20,42 @@ public record DependencyGroup
19
20
  public Dictionary<string, object> Rules { get; init; } = new();
20
21
 
21
22
  public GroupMatcher GetGroupMatcher() => GroupMatcher.FromRules(Rules);
23
+
24
+ public bool IsGroupedByDependencyName =>
25
+ GetString(Rules, "group-by")?.Equals("dependency-name", StringComparison.Ordinal) == true;
26
+
27
+ [JsonIgnore]
28
+ public string? DependencyNameGroupTarget { get; init; }
29
+
30
+ public DependencyGroup CreateDependencyNameSubgroup(string dependencyName, string? subgroupName = null)
31
+ {
32
+ var matchesParent = GetGroupMatcher().IsMatch(dependencyName);
33
+ var subgroupRules = new Dictionary<string, object>(Rules)
34
+ {
35
+ ["patterns"] = matchesParent ? new[] { dependencyName } : Array.Empty<string>(),
36
+ };
37
+ return this with
38
+ {
39
+ Name = subgroupName ?? $"{Name}/{dependencyName}",
40
+ Rules = subgroupRules,
41
+ DependencyNameGroupTarget = dependencyName,
42
+ };
43
+ }
44
+
45
+ private static string? GetString(Dictionary<string, object> rules, string propertyName)
46
+ {
47
+ if (!rules.TryGetValue(propertyName, out var propertyObject))
48
+ {
49
+ return null;
50
+ }
51
+
52
+ return propertyObject switch
53
+ {
54
+ string value => value,
55
+ JsonElement { ValueKind: JsonValueKind.String } element => element.GetString(),
56
+ _ => null,
57
+ };
58
+ }
22
59
  }
23
60
 
24
61
  public enum GroupUpdateType
@@ -102,7 +102,65 @@ public sealed record Job
102
102
  return existingPullRequests;
103
103
  }
104
104
 
105
- public Tuple<string?, ImmutableArray<PullRequestDependency>>? GetExistingPullRequestForDependencies(IEnumerable<ReportedDependencyWithDirectory> dependencies, bool considerVersions)
105
+ public DependencyGroup? ResolveDependencyGroupToRefresh()
106
+ {
107
+ if (DependencyGroupToRefresh is null)
108
+ {
109
+ return null;
110
+ }
111
+
112
+ var exactGroup = FindConfiguredDependencyGroup(DependencyGroupToRefresh);
113
+ if (exactGroup is not null)
114
+ {
115
+ return exactGroup.IsGroupedByDependencyName ? null : exactGroup;
116
+ }
117
+
118
+ var dynamicParent = DependencyGroups
119
+ .Where(g => g.IsGroupedByDependencyName)
120
+ .Where(g => DependencyGroupToRefresh.StartsWith($"{g.Name}/", StringComparison.OrdinalIgnoreCase))
121
+ .OrderByDescending(g => g.Name.Length)
122
+ .FirstOrDefault();
123
+ if (dynamicParent is null)
124
+ {
125
+ return null;
126
+ }
127
+
128
+ var dependencyName = DependencyGroupToRefresh[(dynamicParent.Name.Length + 1)..];
129
+ if (dependencyName.Length == 0 ||
130
+ (Dependencies.Length > 0 && !Dependencies.Contains(dependencyName, StringComparer.OrdinalIgnoreCase)))
131
+ {
132
+ return null;
133
+ }
134
+
135
+ return dynamicParent.CreateDependencyNameSubgroup(dependencyName, DependencyGroupToRefresh);
136
+ }
137
+
138
+ public DependencyGroup? FindConfiguredDependencyGroup(string name)
139
+ {
140
+ return DependencyGroups.FirstOrDefault(g => g.Name == name) ??
141
+ DependencyGroups.FirstOrDefault(g => g.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
142
+ }
143
+
144
+ public Tuple<string?, ImmutableArray<PullRequestDependency>>? GetExistingPullRequestForDependencies(
145
+ IEnumerable<ReportedDependencyWithDirectory> dependencies,
146
+ bool considerVersions)
147
+ {
148
+ return GetExistingPullRequestForDependencies(dependencies, considerVersions, dependencyGroupName: null, matchGroupName: false);
149
+ }
150
+
151
+ public Tuple<string?, ImmutableArray<PullRequestDependency>>? GetExistingGroupPullRequestForDependencies(
152
+ IEnumerable<ReportedDependencyWithDirectory> dependencies,
153
+ bool considerVersions,
154
+ string dependencyGroupName)
155
+ {
156
+ return GetExistingPullRequestForDependencies(dependencies, considerVersions, dependencyGroupName, matchGroupName: true);
157
+ }
158
+
159
+ private Tuple<string?, ImmutableArray<PullRequestDependency>>? GetExistingPullRequestForDependencies(
160
+ IEnumerable<ReportedDependencyWithDirectory> dependencies,
161
+ bool considerVersions,
162
+ string? dependencyGroupName,
163
+ bool matchGroupName)
106
164
  {
107
165
  var desiredDependencies = dependencies.ToArray();
108
166
  if (desiredDependencies.Any(d => d.Version is null))
@@ -128,6 +186,12 @@ public sealed record Job
128
186
  var existingPullRequest = existingPullRequests
129
187
  .FirstOrDefault(pr =>
130
188
  {
189
+ if (matchGroupName &&
190
+ !string.Equals(pr.Item1, dependencyGroupName, StringComparison.OrdinalIgnoreCase))
191
+ {
192
+ return false;
193
+ }
194
+
131
195
  return desiredDependencies.All(desired => pr.Item2.Any(existing => DependenciesMatch(desired, existing))) &&
132
196
  pr.Item2.All(existing => desiredDependencies.Any(desired => DependenciesMatch(desired, existing)));
133
197
  });
@@ -137,7 +201,7 @@ public sealed record Job
137
201
  public bool IsDependencyIgnoredByNameOnly(string dependencyName)
138
202
  {
139
203
  var packageNamesToIgnore = IgnoreConditions
140
- .Where(c => (c.UpdateTypes ?? []).Length == 0 && c.VersionRequirement is null) // ignoring by name means there can't be any qualification
204
+ .Where(c => c.IsUnconditionalIgnore())
141
205
  .Select(c => c.DependencyName)
142
206
  .ToArray();
143
207
  var isIgnored = packageNamesToIgnore
@@ -14,9 +14,19 @@ public class PullRequestTextGenerator
14
14
  {
15
15
  private const int MaxTitleLength = 70;
16
16
 
17
- public static string GetPullRequestTitle(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
17
+ public static string GetPullRequestTitle(
18
+ Job job,
19
+ ImmutableArray<UpdateOperationBase> updateOperationsPerformed,
20
+ string? dependencyGroupName,
21
+ string? dependencyNameGroupTarget = null,
22
+ IEnumerable<string>? updatedDirectories = null)
18
23
  {
19
- var shortTitle = GetPullRequestShortTitle(job, updateOperationsPerformed, dependencyGroupName);
24
+ var shortTitle = GetPullRequestShortTitle(
25
+ job,
26
+ updateOperationsPerformed,
27
+ dependencyGroupName,
28
+ dependencyNameGroupTarget,
29
+ updatedDirectories);
20
30
  var titlePrefix = GetPullRequestTitlePrefix(job);
21
31
  var fullTitle = $"{titlePrefix}{shortTitle}";
22
32
  return fullTitle;
@@ -43,14 +53,31 @@ public class PullRequestTextGenerator
43
53
  return prefix;
44
54
  }
45
55
 
46
- private static string GetPullRequestShortTitle(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
56
+ private static string GetPullRequestShortTitle(
57
+ Job job,
58
+ ImmutableArray<UpdateOperationBase> updateOperationsPerformed,
59
+ string? dependencyGroupName,
60
+ string? dependencyNameGroupTarget,
61
+ IEnumerable<string>? updatedDirectories)
47
62
  {
48
63
  string title;
49
64
  var dependencySets = GetDependencySets(updateOperationsPerformed);
50
- if (dependencyGroupName is not null)
65
+ if (dependencyGroupName is not null && dependencyNameGroupTarget is null)
51
66
  {
52
67
  title = $"Bump the {dependencyGroupName} group with {dependencySets.Length} update{(dependencySets.Length > 1 ? "s" : "")}";
53
68
  }
69
+ else if (dependencyNameGroupTarget is not null)
70
+ {
71
+ var directories = (updatedDirectories ?? [])
72
+ .Distinct(StringComparer.OrdinalIgnoreCase)
73
+ .ToArray();
74
+ title = directories.Length switch
75
+ {
76
+ > 1 => $"Bump {dependencyNameGroupTarget} across {directories.Length} directories",
77
+ 1 when directories[0] != "/" => $"Bump {dependencyNameGroupTarget} in {directories[0]}",
78
+ _ => $"Bump {dependencyNameGroupTarget}",
79
+ };
80
+ }
54
81
  else
55
82
  {
56
83
  if (dependencySets.Length == 1)
@@ -73,13 +100,23 @@ public class PullRequestTextGenerator
73
100
  return title;
74
101
  }
75
102
 
76
- public static string GetPullRequestCommitMessage(Job job, ImmutableArray<UpdateOperationBase> updateOperationsPerformed, string? dependencyGroupName)
103
+ public static string GetPullRequestCommitMessage(
104
+ Job job,
105
+ ImmutableArray<UpdateOperationBase> updateOperationsPerformed,
106
+ string? dependencyGroupName,
107
+ string? dependencyNameGroupTarget = null,
108
+ IEnumerable<string>? updatedDirectories = null)
77
109
  {
78
110
  var sb = new StringBuilder();
79
- sb.AppendLine(GetPullRequestTitle(job, updateOperationsPerformed, dependencyGroupName));
111
+ sb.AppendLine(GetPullRequestTitle(
112
+ job,
113
+ updateOperationsPerformed,
114
+ dependencyGroupName,
115
+ dependencyNameGroupTarget,
116
+ updatedDirectories));
80
117
  var dependencySets = GetDependencySets(updateOperationsPerformed);
81
118
  if (dependencySets.Length > 1 ||
82
- dependencyGroupName is not null)
119
+ (dependencyGroupName is not null && dependencyNameGroupTarget is null))
83
120
  {
84
121
  // multiple updates performed, enumerate them
85
122
  sb.AppendLine();