dependabot-nuget 0.283.0 → 0.285.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/CloneCommand.cs +40 -0
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +1 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Run.cs +0 -1
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/CloneWorker.cs +144 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/IGitCommandHandler.cs +6 -0
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/ShellGitCommandHandler.cs +37 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotFound.cs +5 -1
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +1 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +9 -2
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobRepoNotFound.cs +13 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobSource.cs +2 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/MarkAsProcessed.cs +6 -1
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PrivateSourceAuthenticationFailure.cs +5 -1
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UnknownError.cs +5 -1
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UpdateNotPossible.cs +5 -1
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/HttpApiHandler.cs +8 -2
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +73 -31
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +30 -9
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +3 -3
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +2 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Clone/CloneWorkerTests.cs +183 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Clone/TestGitCommandHandler.cs +16 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +640 -17
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +10 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdatedDependencyListTests.cs +53 -6
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/BindingRedirectsTests.cs +225 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +106 -0
  29. metadata +13 -5
@@ -28,7 +28,9 @@ internal static class BindingRedirectManager
28
28
  /// https://learn.microsoft.com/en-us/nuget/resources/check-project-format
29
29
  /// </remarks>
30
30
  /// <param name="projectBuildFile">The project build file (*.xproj) to be updated</param>
31
- public static async ValueTask UpdateBindingRedirectsAsync(ProjectBuildFile projectBuildFile)
31
+ /// <param name="updatedPackageName"/>The name of the package that was updated</param>
32
+ /// <param name="updatedPackageVersion">The version of the package that was updated</param>
33
+ public static async ValueTask UpdateBindingRedirectsAsync(ProjectBuildFile projectBuildFile, string updatedPackageName, string updatedPackageVersion)
32
34
  {
33
35
  var configFile = await TryGetRuntimeConfigurationFile(projectBuildFile);
34
36
  if (configFile is null)
@@ -47,7 +49,20 @@ internal static class BindingRedirectManager
47
49
  return;
48
50
  }
49
51
 
50
- var fileContent = AddBindingRedirects(configFile, bindings);
52
+ // we need to detect what assembly references come from the newly updated package; the `HintPath` will look like
53
+ // ..\packages\Some.Package.1.2.3\lib\net45\Some.Package.dll
54
+ // so we first pull out the packages sub-path, e.g., `..\packages`
55
+ // then we add the updated package name, version, and a trailing directory separator and ensure it's a unix-style path
56
+ // e.g., ../packages/Some.Package/1.2.3/
57
+ // at this point any assembly in that directory is from the updated package and will need a binding redirect
58
+ // finally we pull out the assembly `HintPath` values for _all_ references relative to the project file in a unix-style value
59
+ // e.g., ../packages/Some.Other.Package/4.5.6/lib/net45/Some.Other.Package.dll
60
+ // all of that is passed to `AddBindingRedirects()` so we can ensure binding redirects for the relevant assemblies
61
+ var packagesDirectory = PackagesConfigUpdater.GetPathToPackagesDirectory(projectBuildFile, updatedPackageName, updatedPackageVersion, packagesConfigPath: null)!;
62
+ var assemblyPathPrefix = Path.Combine(packagesDirectory, $"{updatedPackageName}.{updatedPackageVersion}").NormalizePathToUnix().EnsureSuffix("/");
63
+ var assemblyPaths = references.Select(static x => x.HintPath).Select(x => Path.GetRelativePath(Path.GetDirectoryName(projectBuildFile.Path)!, x).NormalizePathToUnix()).ToList();
64
+ var bindingsAndAssemblyPaths = bindings.Zip(assemblyPaths);
65
+ var fileContent = AddBindingRedirects(configFile, bindingsAndAssemblyPaths, assemblyPathPrefix);
51
66
  configFile = configFile with { Content = fileContent };
52
67
 
53
68
  await File.WriteAllTextAsync(configFile.Path, configFile.Content);
@@ -161,10 +176,10 @@ internal static class BindingRedirectManager
161
176
  }
162
177
  }
163
178
 
164
- private static string AddBindingRedirects(ConfigurationFile configFile, IEnumerable<Runtime_AssemblyBinding> bindingRedirects)
179
+ private static string AddBindingRedirects(ConfigurationFile configFile, IEnumerable<(Runtime_AssemblyBinding Binding, string AssemblyPath)> bindingRedirectsAndAssemblyPaths, string assemblyPathPrefix)
165
180
  {
166
181
  // Do nothing if there are no binding redirects to add, bail out
167
- if (!bindingRedirects.Any())
182
+ if (!bindingRedirectsAndAssemblyPaths.Any())
168
183
  {
169
184
  return configFile.Content;
170
185
  }
@@ -185,7 +200,7 @@ internal static class BindingRedirectManager
185
200
  // Get all of the current bindings in config
186
201
  var currentBindings = GetAssemblyBindings(runtime);
187
202
 
188
- foreach (var bindingRedirect in bindingRedirects)
203
+ foreach (var (bindingRedirect, assemblyPath) in bindingRedirectsAndAssemblyPaths)
189
204
  {
190
205
  // If the binding redirect already exists in config, update it. Otherwise, add it.
191
206
  var bindingAssemblyIdentity = new AssemblyIdentity(bindingRedirect.Name, bindingRedirect.PublicKeyToken);
@@ -208,11 +223,17 @@ internal static class BindingRedirectManager
208
223
  }
209
224
  else
210
225
  {
211
- // Get an assembly binding element to use
212
- var assemblyBindingElement = GetAssemblyBindingElement(runtime);
226
+ // only add a previously missing binding redirect if it's related to the package that caused the whole update
227
+ // this isn't strictly necessary, but can be helpful to the end user and it's easy for them to revert if they
228
+ // don't like this particular change
229
+ if (assemblyPath.StartsWith(assemblyPathPrefix, StringComparison.OrdinalIgnoreCase))
230
+ {
231
+ // Get an assembly binding element to use
232
+ var assemblyBindingElement = GetAssemblyBindingElement(runtime);
213
233
 
214
- // Add the binding to that element
215
- assemblyBindingElement.AddIndented(bindingRedirect.ToXElement());
234
+ // Add the binding to that element
235
+ assemblyBindingElement.AddIndented(bindingRedirect.ToXElement());
236
+ }
216
237
  }
217
238
  }
218
239
 
@@ -93,7 +93,7 @@ internal static class PackagesConfigUpdater
93
93
  projectBuildFile.NormalizeDirectorySeparatorsInProject();
94
94
 
95
95
  // Update binding redirects
96
- await BindingRedirectManager.UpdateBindingRedirectsAsync(projectBuildFile);
96
+ await BindingRedirectManager.UpdateBindingRedirectsAsync(projectBuildFile, dependencyName, newDependencyVersion);
97
97
 
98
98
  logger.Log(" Writing project file back to disk");
99
99
  await projectBuildFile.SaveAsync();
@@ -196,7 +196,7 @@ internal static class PackagesConfigUpdater
196
196
  return processes;
197
197
  }
198
198
 
199
- internal static string? GetPathToPackagesDirectory(ProjectBuildFile projectBuildFile, string dependencyName, string dependencyVersion, string packagesConfigPath)
199
+ internal static string? GetPathToPackagesDirectory(ProjectBuildFile projectBuildFile, string dependencyName, string dependencyVersion, string? packagesConfigPath)
200
200
  {
201
201
  // the packages directory can be found from the hint path of the matching dependency, e.g., when given "Newtonsoft.Json", "7.0.1", and a project like this:
202
202
  // <Project>
@@ -242,7 +242,7 @@ internal static class PackagesConfigUpdater
242
242
  }
243
243
  }
244
244
 
245
- if (partialPathMatch is null)
245
+ if (partialPathMatch is null && packagesConfigPath is not null)
246
246
  {
247
247
  // if we got this far, we couldn't find the packages directory for the specified dependency and there are 2 possibilities:
248
248
  // 1. the dependency doesn't actually exist in this project
@@ -27,6 +27,8 @@ internal static class PathHelper
27
27
 
28
28
  public static string EnsurePrefix(this string s, string prefix) => s.StartsWith(prefix) ? s : prefix + s;
29
29
 
30
+ public static string EnsureSuffix(this string s, string suffix) => s.EndsWith(suffix) ? s : s + suffix;
31
+
30
32
  public static string NormalizePathToUnix(this string path) => path.Replace("\\", "/");
31
33
 
32
34
  public static string NormalizeUnixPathParts(this string path)
@@ -0,0 +1,183 @@
1
+ using NuGetUpdater.Core.Clone;
2
+ using NuGetUpdater.Core.Run.ApiModel;
3
+ using NuGetUpdater.Core.Test.Run;
4
+
5
+ using Xunit;
6
+
7
+ namespace NuGetUpdater.Core.Test.Clone;
8
+
9
+ public class CloneWorkerTests
10
+ {
11
+ private const string TestRepoPath = "TEST/REPO/PATH";
12
+
13
+ [Fact]
14
+ public void CloneCommandsAreGenerated()
15
+ {
16
+ TestCommands(
17
+ provider: "github",
18
+ repoMoniker: "test/repo",
19
+ expectedCommands:
20
+ [
21
+ (["clone", "--no-tags", "--depth", "1", "--recurse-submodules", "--shallow-submodules", "https://github.com/test/repo", TestRepoPath], null)
22
+ ]
23
+ );
24
+ }
25
+
26
+ [Fact]
27
+ public void CloneCommandsAreGeneratedWhenBranchIsSpecified()
28
+ {
29
+ TestCommands(
30
+ provider: "github",
31
+ repoMoniker: "test/repo",
32
+ branch: "some-branch",
33
+ expectedCommands:
34
+ [
35
+ (["clone", "--no-tags", "--depth", "1", "--recurse-submodules", "--shallow-submodules", "--branch", "some-branch", "--single-branch", "https://github.com/test/repo", TestRepoPath], null)
36
+ ]
37
+ );
38
+ }
39
+
40
+ [Fact]
41
+ public void CloneCommandsAreGeneratedWhenCommitIsSpecified()
42
+ {
43
+ TestCommands(
44
+ provider: "github",
45
+ repoMoniker: "test/repo",
46
+ commit: "abc123",
47
+ expectedCommands:
48
+ [
49
+ (["clone", "--no-tags", "--depth", "1", "--recurse-submodules", "--shallow-submodules", "https://github.com/test/repo", TestRepoPath], null),
50
+ (["fetch", "--depth", "1", "--recurse-submodules=on-demand", "origin", "abc123"], TestRepoPath),
51
+ (["reset", "--hard", "--recurse-submodules", "abc123"], TestRepoPath)
52
+ ]
53
+ );
54
+ }
55
+
56
+ [Fact]
57
+ public async Task SuccessfulCloneGeneratesNoApiMessages()
58
+ {
59
+ await TestCloneAsync(
60
+ provider: "github",
61
+ repoMoniker: "test/repo",
62
+ expectedApiMessages: []
63
+ );
64
+ }
65
+
66
+ [Fact]
67
+ public async Task UnauthorizedCloneGeneratesTheExpectedApiMessagesFromGenericOutput()
68
+ {
69
+ await TestCloneAsync(
70
+ provider: "github",
71
+ repoMoniker: "test/repo",
72
+ testGitCommandHandler: new TestGitCommandHandlerWithOutputs("Authentication failed for repo", ""),
73
+ expectedApiMessages:
74
+ [
75
+ new JobRepoNotFound("Authentication failed for repo"),
76
+ new MarkAsProcessed("unknown"),
77
+ ],
78
+ expectedExitCode: 1
79
+ );
80
+ }
81
+
82
+ [Fact]
83
+ public async Task UnauthorizedCloneGeneratesTheExpectedApiMessagesFromGitCommandOutput()
84
+ {
85
+ await TestCloneAsync(
86
+ provider: "github",
87
+ repoMoniker: "test/repo",
88
+ testGitCommandHandler: new TestGitCommandHandlerWithOutputs("", "fatal: could not read Username for 'https://github.com': No such device or address"),
89
+ expectedApiMessages:
90
+ [
91
+ new JobRepoNotFound("fatal: could not read Username for 'https://github.com': No such device or address"),
92
+ new MarkAsProcessed("unknown"),
93
+ ],
94
+ expectedExitCode: 1
95
+ );
96
+ }
97
+
98
+ private class TestGitCommandHandlerWithOutputs : TestGitCommandHandler
99
+ {
100
+ private readonly string _stdout;
101
+ private readonly string _stderr;
102
+
103
+ public TestGitCommandHandlerWithOutputs(string stdout, string stderr)
104
+ {
105
+ _stdout = stdout;
106
+ _stderr = stderr;
107
+ }
108
+
109
+ public override async Task RunGitCommandAsync(IReadOnlyCollection<string> args, string? workingDirectory = null)
110
+ {
111
+ await base.RunGitCommandAsync(args, workingDirectory);
112
+ ShellGitCommandHandler.HandleErrorsFromOutput(_stdout, _stderr);
113
+ }
114
+ }
115
+
116
+ private static void TestCommands(string provider, string repoMoniker, (string[] Args, string? WorkingDirectory)[] expectedCommands, string? branch = null, string? commit = null)
117
+ {
118
+ var job = new Job()
119
+ {
120
+ Source = new()
121
+ {
122
+ Provider = provider,
123
+ Repo = repoMoniker,
124
+ Branch = branch,
125
+ Commit = commit,
126
+ }
127
+ };
128
+ var actualCommands = CloneWorker.GetAllCommandArgs(job, TestRepoPath);
129
+ VerifyCommands(expectedCommands, actualCommands);
130
+ }
131
+
132
+ private static async Task TestCloneAsync(string provider, string repoMoniker, object[] expectedApiMessages, string? branch = null, string? commit = null, TestGitCommandHandler? testGitCommandHandler = null, int expectedExitCode = 0)
133
+ {
134
+ // arrange
135
+ var testApiHandler = new TestApiHandler();
136
+ testGitCommandHandler ??= new TestGitCommandHandler();
137
+ var testLogger = new TestLogger();
138
+ var worker = new CloneWorker(testApiHandler, testGitCommandHandler, testLogger);
139
+
140
+ // act
141
+ var job = new Job()
142
+ {
143
+ Source = new()
144
+ {
145
+ Provider = provider,
146
+ Repo = repoMoniker,
147
+ Branch = branch,
148
+ Commit = commit,
149
+ }
150
+ };
151
+ var exitCode = await worker.RunAsync(job, TestRepoPath);
152
+
153
+ // assert
154
+ Assert.Equal(expectedExitCode, exitCode);
155
+
156
+ var actualApiMessages = testApiHandler.ReceivedMessages.ToArray();
157
+ if (actualApiMessages.Length > expectedApiMessages.Length)
158
+ {
159
+ var extraApiMessages = actualApiMessages.Skip(expectedApiMessages.Length).Select(m => RunWorkerTests.SerializeObjectAndType(m.Object)).ToArray();
160
+ Assert.Fail($"Expected {expectedApiMessages.Length} API messages, but got {extraApiMessages.Length} extra:\n\t{string.Join("\n\t", extraApiMessages)}");
161
+ }
162
+ if (expectedApiMessages.Length > actualApiMessages.Length)
163
+ {
164
+ var missingApiMessages = expectedApiMessages.Skip(actualApiMessages.Length).Select(m => RunWorkerTests.SerializeObjectAndType(m)).ToArray();
165
+ Assert.Fail($"Expected {expectedApiMessages.Length} API messages, but only got {actualApiMessages.Length}; missing:\n\t{string.Join("\n\t", missingApiMessages)}");
166
+ }
167
+ }
168
+
169
+ private static void VerifyCommands((string[] Args, string? WorkingDirectory)[] expected, (string[] Args, string? WorkingDirectory)[] actual)
170
+ {
171
+ var expectedCommands = StringifyCommands(expected);
172
+ var actualCommands = StringifyCommands(actual);
173
+ Assert.True(expectedCommands.Length == actualCommands.Length, $"Expected {expectedCommands.Length} messages:\n\t{string.Join("\n\t", expectedCommands)}\ngot {actualCommands.Length}:\n\t{string.Join("\n\t", actualCommands)}");
174
+ foreach (var (expectedCommand, actualCommand) in expectedCommands.Zip(actualCommands))
175
+ {
176
+ Assert.Equal(expectedCommand, actualCommand);
177
+ }
178
+ }
179
+
180
+ private static string[] StringifyCommands((string[] Args, string? WorkingDirectory)[] commandArgs) => commandArgs.Select(a => StringifyCommand(a.Args, a.WorkingDirectory)).ToArray();
181
+ private static string StringifyCommand(string[] args, string? workingDirectory) => $"args=[{string.Join(", ", args)}], workingDirectory={ReplaceWorkingDirectory(workingDirectory)}";
182
+ private static string ReplaceWorkingDirectory(string? arg) => arg ?? "NULL";
183
+ }
@@ -0,0 +1,16 @@
1
+ using NuGetUpdater.Core.Clone;
2
+
3
+ namespace NuGetUpdater.Core.Test.Clone;
4
+
5
+ internal class TestGitCommandHandler : IGitCommandHandler
6
+ {
7
+ private readonly List<(string[] Args, string? WorkingDirectory)> _seenCommands = new();
8
+
9
+ public IReadOnlyCollection<(string[] Args, string? WorkingDirectory)> SeenCommands => _seenCommands;
10
+
11
+ public virtual Task RunGitCommandAsync(IReadOnlyCollection<string> args, string? workingDirectory = null)
12
+ {
13
+ _seenCommands.Add((args.ToArray(), workingDirectory));
14
+ return Task.CompletedTask;
15
+ }
16
+ }