├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
├── dependabot.yml
├── workflows
│ ├── merge-dependabot.yml
│ └── on-push-do-docs.yml
└── stale.yml
├── src
├── key.snk
├── icon.png
├── PackageUpdate
│ ├── Invoke.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Options.cs
│ ├── AssemblyInfo.cs
│ ├── GlobalUsings.cs
│ ├── ProcessOutputReader.cs
│ ├── AssemblyLocation.cs
│ ├── Excluder.cs
│ ├── PackageSourceReader.cs
│ ├── RepositoryReader.cs
│ ├── Logging.cs
│ ├── PackageUpdate.csproj
│ ├── CommandRunner.cs
│ ├── FileSystem.cs
│ ├── DotnetStarter.cs
│ ├── Program.cs
│ ├── SerilogNuGetLogger.cs
│ └── Updater.cs
├── global.json
├── Tests
│ ├── UpdaterTests.GetLatestVersion_ReturnsMetadata.verified.txt
│ ├── GlobalUsings.cs
│ ├── UpdaterTests.UpdateSinglePackage_CaseInsensitive.verified.txt
│ ├── Extensions.cs
│ ├── UpdaterTests.UpdateAllPackages.verified.txt
│ ├── UpdaterTests.UpdateSinglePackage.verified.txt
│ ├── ExcluderTests.cs
│ ├── Tests.csproj
│ ├── CommandRunnerTests.cs
│ └── UpdaterTests.cs
├── mdsnippets.json
├── PackageUpdate.slnx
├── context-menu.reg
├── context-menu-build.reg
├── Directory.Build.props
├── appveyor.yml
├── PackageUpdate.slnx.DotSettings
├── Directory.Packages.props
├── nuget.config
├── .editorconfig
└── Shared.sln.DotSettings
├── .gitignore
├── .gitattributes
├── license.txt
├── code_of_conduct.md
├── readme.source.md
└── readme.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: SimonCropp
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/src/key.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonCropp/PackageUpdate/HEAD/src/key.snk
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonCropp/PackageUpdate/HEAD/src/icon.png
--------------------------------------------------------------------------------
/src/PackageUpdate/Invoke.cs:
--------------------------------------------------------------------------------
1 | public delegate Task Invoke(string targetDirectory, string? package, bool build);
--------------------------------------------------------------------------------
/src/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "10.0.101",
4 | "allowPrerelease": true,
5 | "rollForward": "latestFeature"
6 | }
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.suo
2 | *.user
3 | bin/
4 | obj/
5 | .vs/
6 | *.DotSettings.user
7 | .idea/
8 | *.received.*
9 | nugets/
10 | .claude/settings.local.json
11 |
--------------------------------------------------------------------------------
/src/Tests/UpdaterTests.GetLatestVersion_ReturnsMetadata.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Id: Newtonsoft.Json,
3 | Version: 13.0.4,
4 | HasVersion: true,
5 | IsPrerelease: false
6 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/src"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/src/Tests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | // Global using directives
2 |
3 | global using System.Xml.Linq;
4 | global using NuGet.Configuration;
5 | global using NuGet.Protocol.Core.Types;
6 | global using NuGet.Versioning;
--------------------------------------------------------------------------------
/src/mdsnippets.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json",
3 | "TocExcludes": [ "NuGet package", "Release Notes", "Icon" ],
4 | "MaxWidth": 80
5 | }
--------------------------------------------------------------------------------
/src/PackageUpdate/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "PackageUpdate": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--target-directory C:\\Code\\VerifyTests\\Verify"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/Tests/UpdaterTests.UpdateSinglePackage_CaseInsensitive.verified.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Tests/Extensions.cs:
--------------------------------------------------------------------------------
1 | static class Extensions
2 | {
3 | public static IEnumerable Lines(this string target)
4 | {
5 | using var reader = new StringReader(target);
6 | while (reader.ReadLine() is { } line)
7 | {
8 | yield return line;
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Tests/UpdaterTests.UpdateAllPackages.verified.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/PackageUpdate/Options.cs:
--------------------------------------------------------------------------------
1 | public class Options
2 | {
3 | [Option('t', "target-directory", Required = false)]
4 | public string? TargetDirectory { get; set; }
5 | [Option('b', "build", Required = false)]
6 | public bool Build { get; set; }
7 |
8 | [Option('p', "package", Required = false)]
9 | public string? Package { get; set; }
10 | }
--------------------------------------------------------------------------------
/src/Tests/UpdaterTests.UpdateSinglePackage.verified.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/PackageUpdate/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | [assembly: InternalsVisibleTo("Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3")]
--------------------------------------------------------------------------------
/src/PackageUpdate/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Xml;
2 | global using System.Xml.Linq;
3 | global using CommandLine;
4 | global using NuGet.Common;
5 | global using NuGet.Configuration;
6 | global using NuGet.Protocol;
7 | global using NuGet.Protocol.Core.Types;
8 | global using NuGet.Versioning;
9 | global using Serilog;
10 | global using Serilog.Events;
11 | global using Serilog.Parsing;
--------------------------------------------------------------------------------
/src/PackageUpdate.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Tests/ExcluderTests.cs:
--------------------------------------------------------------------------------
1 | public class ExcluderTests
2 | {
3 | [Fact]
4 | public void Simple()
5 | {
6 | Environment.SetEnvironmentVariable("PackageUpdateIgnores", "ignore, otherIgnore");
7 | Assert.True(Excluder.ShouldExclude("SolutionToIgnore.sln"));
8 | Assert.True(Excluder.ShouldExclude("SolutionOtherIgnore.sln"));
9 | Assert.False(Excluder.ShouldExclude("Solution.sln"));
10 | }
11 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.txt -text
3 | *.png binary
4 | *.snk binary
5 |
6 | *.verified.txt text eol=lf working-tree-encoding=UTF-8
7 | *.verified.xml text eol=lf working-tree-encoding=UTF-8
8 | *.verified.json text eol=lf working-tree-encoding=UTF-8
9 |
10 | .editorconfig text eol=lf working-tree-encoding=UTF-8
11 | *.sln.DotSettings text eol=lf working-tree-encoding=UTF-8
12 | *.slnx.DotSettings text eol=lf working-tree-encoding=UTF-8
--------------------------------------------------------------------------------
/.github/workflows/merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: merge-dependabot
2 | on:
3 | pull_request:
4 | jobs:
5 | automerge:
6 | runs-on: ubuntu-latest
7 | if: github.actor == 'dependabot[bot]'
8 | steps:
9 | - name: Dependabot Auto Merge
10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6
11 | with:
12 | target: minor
13 | github-token: ${{ secrets.dependabot }}
14 | command: squash and merge
--------------------------------------------------------------------------------
/src/PackageUpdate/ProcessOutputReader.cs:
--------------------------------------------------------------------------------
1 | public static class ProcessOutputReader
2 | {
3 | public static async Task> ReadLines(this Process process)
4 | {
5 | var list = new List();
6 | while (await process.StandardOutput.ReadLineAsync() is { } line)
7 | {
8 | if (string.IsNullOrWhiteSpace(line))
9 | {
10 | continue;
11 | }
12 | list.Add(line);
13 | }
14 |
15 | return list;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/PackageUpdate/AssemblyLocation.cs:
--------------------------------------------------------------------------------
1 | static class AssemblyLocation
2 | {
3 | static AssemblyLocation()
4 | {
5 | var assembly = typeof(AssemblyLocation).Assembly;
6 |
7 | var path = assembly.Location
8 | .Replace("file:///", "")
9 | .Replace("file://", "")
10 | .Replace(@"file:\\\", "")
11 | .Replace(@"file:\\", "");
12 |
13 | CurrentDirectory = Path.GetDirectoryName(path)!;
14 | }
15 |
16 | public static readonly string CurrentDirectory;
17 | }
--------------------------------------------------------------------------------
/src/context-menu.reg:
--------------------------------------------------------------------------------
1 | Windows Registry Editor Version 5.00
2 | [HKEY_CLASSES_ROOT\Directory\Shell]
3 | @="none"
4 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdate]
5 | "MUIVerb"="run packageupdate"
6 | "Position"="bottom"
7 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdate]
8 | "MUIVerb"="run packageupdate"
9 | "Position"="bottom"
10 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdate\command]
11 | @="cmd.exe /c packageupdate \"%V\""
12 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdate\command]
13 | @="cmd.exe /c packageupdate \"%V\""
--------------------------------------------------------------------------------
/src/context-menu-build.reg:
--------------------------------------------------------------------------------
1 | Windows Registry Editor Version 5.00
2 | [HKEY_CLASSES_ROOT\Directory\Shell]
3 | @="none"
4 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdatebuild]
5 | "MUIVerb"="run packageupdate -b"
6 | "Position"="bottom"
7 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdatebuild]
8 | "MUIVerb"="run packageupdate -b"
9 | "Position"="bottom"
10 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdatebuild\command]
11 | @="cmd.exe /c packageupdate \"%V\" -b"
12 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdatebuild\command]
13 | @="cmd.exe /c packageupdate \"%V\" -b"
--------------------------------------------------------------------------------
/src/PackageUpdate/Excluder.cs:
--------------------------------------------------------------------------------
1 | static class Excluder
2 | {
3 | static List ignores;
4 |
5 | static Excluder()
6 | {
7 | var variable = Environment.GetEnvironmentVariable("PackageUpdateIgnores");
8 | if (variable == null)
9 | {
10 | ignores = [];
11 | return;
12 | }
13 |
14 | ignores = variable.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
15 | }
16 |
17 | public static bool ShouldExclude(string solution) =>
18 | ignores.Any(_ => solution.Contains(_, StringComparison.OrdinalIgnoreCase));
19 | }
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.5
5 | preview
6 | NU1608
7 | 1.0.0
8 | Updates NuGet packages for all solutions in a directory.
9 | true
10 | true
11 | true
12 | true
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/PackageUpdate/PackageSourceReader.cs:
--------------------------------------------------------------------------------
1 | public static class PackageSourceReader
2 | {
3 | static XPlatMachineWideSetting machineSettings = new();
4 |
5 | public static List Read(string directory)
6 | {
7 | // Set up NuGet sources
8 | var settings = Settings.LoadDefaultSettings(
9 | root: directory,
10 | configFileName: null,
11 | machineWideSettings: machineSettings);
12 |
13 | var provider = new PackageSourceProvider(settings);
14 | return provider.LoadPackageSources()
15 | .Where(_ => _.IsEnabled)
16 | .ToList();
17 | }
18 | }
--------------------------------------------------------------------------------
/src/appveyor.yml:
--------------------------------------------------------------------------------
1 | image: Visual Studio 2022
2 | environment:
3 | DOTNET_NOLOGO: true
4 | DOTNET_CLI_TELEMETRY_OPTOUT: true
5 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
6 | skip_commits:
7 | message: /doco|Merge pull request.*/
8 | build_script:
9 | - pwsh: |
10 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1"
11 | ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet'
12 | - dotnet build src --configuration Release
13 | - dotnet test src --configuration Release --no-build --no-restore
14 | test: off
15 | artifacts:
16 | - path: nugets\**\*.nupkg
--------------------------------------------------------------------------------
/src/PackageUpdate/RepositoryReader.cs:
--------------------------------------------------------------------------------
1 | public static class RepositoryReader
2 | {
3 | static Dictionary cache = [];
4 | static Repository.RepositoryFactory factory = Repository.Factory;
5 |
6 | public static async Task<(SourceRepository repository, PackageMetadataResource metadataResource)> Read(PackageSource source)
7 | {
8 | if (!cache.TryGetValue(source, out var value))
9 | {
10 | var repository = factory.GetCoreV3(source);
11 |
12 | var metadataResource = await repository.GetResourceAsync();
13 | cache[source] = value = (repository, metadataResource);
14 | }
15 |
16 | return value;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/PackageUpdate/Logging.cs:
--------------------------------------------------------------------------------
1 | static class Logging
2 | {
3 | public static string LogsDirectory { get; } = Path.Combine(AssemblyLocation.CurrentDirectory, "logs");
4 |
5 | public static void Init()
6 | {
7 | Console.Write($"Logs Directory: {LogsDirectory}");
8 | Directory.CreateDirectory(LogsDirectory);
9 | var configuration = new LoggerConfiguration();
10 | configuration.MinimumLevel.Debug();
11 | configuration.WriteTo.Console();
12 | configuration.WriteTo.File(
13 | Path.Combine(LogsDirectory, "log.txt"),
14 | rollOnFileSizeLimit: true,
15 | fileSizeLimitBytes: 1000000, //1mb
16 | retainedFileCountLimit: 10);
17 | Log.Logger = configuration.CreateLogger();
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | Exe
6 | testing
7 | $(NoWarn);xUnit1051
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.github/workflows/on-push-do-docs.yml:
--------------------------------------------------------------------------------
1 | name: on-push-do-docs
2 | on:
3 | push:
4 | jobs:
5 | docs:
6 | runs-on: windows-latest
7 | steps:
8 | - uses: actions/checkout@v4
9 | - name: Run MarkdownSnippets
10 | run: |
11 | dotnet tool install --global MarkdownSnippets.Tool
12 | mdsnippets ${GITHUB_WORKSPACE}
13 | shell: bash
14 | - name: Push changes
15 | run: |
16 | git config --local user.email "action@github.com"
17 | git config --local user.name "GitHub Action"
18 | git commit -m "Docs changes" -a || echo "nothing to commit"
19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git"
20 | branch="${GITHUB_REF:11}"
21 | git push "${remote}" ${branch} || echo "nothing to push"
22 | shell: bash
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 7
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Set to true to ignore issues in a milestone (defaults to false)
6 | exemptMilestones: true
7 | # Comment to post when marking an issue as stale. Set to `false` to disable
8 | markComment: >
9 | This issue has been automatically marked as stale because it has not had
10 | recent activity. It will be closed if no further activity occurs. Thank you
11 | for your contributions.
12 | # Comment to post when closing a stale issue. Set to `false` to disable
13 | closeComment: false
14 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
15 | pulls:
16 | daysUntilStale: 30
17 | exemptLabels:
18 | - Question
19 | - Bug
20 | - Feature
21 | - Improvement
--------------------------------------------------------------------------------
/src/PackageUpdate.slnx.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | ..\Shared.sln.DotSettings
3 | True
4 | True
5 | 1
6 |
--------------------------------------------------------------------------------
/src/PackageUpdate/PackageUpdate.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net10.0
5 | packageupdate
6 | PackageUpdate
7 | PackageUpdate
8 | True
9 | .NET Core Global Tool that updates packages for all solutions in a directory
10 | true
11 | false
12 | LatestMajor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Simon Cropp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
20 |
21 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/PackageUpdate/CommandRunner.cs:
--------------------------------------------------------------------------------
1 | static class CommandRunner
2 | {
3 | public static Task RunCommand(Invoke invoke, params string[] args)
4 | {
5 | if (args.Length == 1)
6 | {
7 | var firstArg = args[0];
8 | if (!firstArg.StartsWith('-'))
9 | {
10 | return invoke(firstArg, null, false);
11 | }
12 | }
13 |
14 | return Parser.Default.ParseArguments(args)
15 | .WithParsedAsync(
16 | options =>
17 | {
18 | var targetDirectory = FindTargetDirectory(options.TargetDirectory);
19 | return invoke(targetDirectory, options.Package, options.Build);
20 | });
21 | }
22 |
23 | static async Task> WithParsedAsync(
24 | this ParserResult result,
25 | Func action)
26 | {
27 | if (result is Parsed parsed)
28 | {
29 | await action(parsed.Value);
30 | }
31 |
32 | return result;
33 | }
34 |
35 | static string FindTargetDirectory(string? targetDirectory)
36 | {
37 | if (targetDirectory == null)
38 | {
39 | return Environment.CurrentDirectory;
40 | }
41 |
42 | return Path.GetFullPath(targetDirectory);
43 | }
44 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: How to raise feature requests
4 | ---
5 |
6 |
7 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.
8 |
9 | If you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/).
10 |
11 | If you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature.
12 |
13 | Note that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers.
14 |
15 |
16 | #### Is the feature request related to a problem
17 |
18 | A clear and concise description of what the problem is.
19 |
20 |
21 | #### Describe the solution
22 |
23 | A clear and concise proposal of how you intend to implement the feature.
24 |
25 |
26 | #### Describe alternatives considered
27 |
28 | A clear and concise description of any alternative solutions or features you've considered.
29 |
30 |
31 | #### Additional context
32 |
33 | Add any other context about the feature request here.
34 |
--------------------------------------------------------------------------------
/src/PackageUpdate/FileSystem.cs:
--------------------------------------------------------------------------------
1 | static class FileSystem
2 | {
3 | public static IEnumerable FindSolutions(string directory)
4 | {
5 | foreach (var solution in EnumerateFiles(directory, "*.sln"))
6 | {
7 | yield return solution;
8 | }
9 |
10 | foreach (var solution in EnumerateFiles(directory, "*.slnx"))
11 | {
12 | yield return solution;
13 | }
14 | }
15 |
16 | static List EnumerateFiles(string directory, string pattern)
17 | {
18 | var allFiles = new List();
19 |
20 | var stack = new Stack();
21 | stack.Push(directory);
22 |
23 | while (stack.TryPop(out var current))
24 | {
25 | var files = GetFiles(current, pattern);
26 | allFiles.AddRange(files);
27 |
28 | foreach (var subdirectory in GetDirectories(current))
29 | {
30 | stack.Push(subdirectory);
31 | }
32 | }
33 |
34 | return allFiles;
35 | }
36 |
37 | static IEnumerable GetFiles(string directory, string pattern)
38 | {
39 | try
40 | {
41 | return Directory.EnumerateFiles(directory, pattern, SearchOption.TopDirectoryOnly);
42 | }
43 | catch (UnauthorizedAccessException)
44 | {
45 | return [];
46 | }
47 | }
48 |
49 | static IEnumerable GetDirectories(string directory)
50 | {
51 | try
52 | {
53 | return Directory.EnumerateDirectories(directory, "*", SearchOption.TopDirectoryOnly);
54 | }
55 | catch (UnauthorizedAccessException)
56 | {
57 | return [];
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug fix
3 | about: Create a bug fix to help us improve
4 | ---
5 |
6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.
7 |
8 |
9 | #### Preamble
10 |
11 | General questions may be better placed [StackOveflow](https://stackoverflow.com/).
12 |
13 | Where relevant, ensure you are using the current stable versions on your development stack. For example:
14 |
15 | * Visual Studio
16 | * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download)
17 | * Any related NuGet packages
18 |
19 | Any code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/).
20 |
21 |
22 | #### Describe the bug
23 |
24 | A clear and concise description of what the bug is. Include any relevant version information.
25 |
26 | A clear and concise description of what you expected to happen.
27 |
28 | Add any other context about the problem here.
29 |
30 |
31 | #### Minimal Repro
32 |
33 | Ensure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix.
34 |
35 | #### Submit a PR that fixes the bug
36 |
37 | Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice.
--------------------------------------------------------------------------------
/src/PackageUpdate/DotnetStarter.cs:
--------------------------------------------------------------------------------
1 | static class DotnetStarter
2 | {
3 | public static Task Build(string solution)
4 | {
5 | Log.Information(" Build {Solution}", solution);
6 | return StartDotNet(
7 | arguments: $"build {solution} --nologo",
8 | directory: Directory.GetParent(solution)!.FullName);
9 | }
10 |
11 | public static Task Shutdown()
12 | {
13 | Log.Information("Shutdown dotnet build");
14 | return StartDotNet(
15 | arguments: "build-server shutdown",
16 | directory: Environment.CurrentDirectory);
17 | }
18 |
19 | static async Task> StartDotNet(string arguments, string directory)
20 | {
21 | using var process = new Process();
22 | process.StartInfo = new()
23 | {
24 | FileName = "dotnet",
25 | Arguments = arguments,
26 | WorkingDirectory = directory,
27 | UseShellExecute = false,
28 | RedirectStandardOutput = true,
29 | RedirectStandardError = true,
30 | CreateNoWindow = true
31 | };
32 | process.Start();
33 | Log.Information(" dotnet {Arguments}", arguments);
34 |
35 | if (!process.WaitForExit(60000))
36 | {
37 | process.Kill(true);
38 | throw new(
39 | $"""
40 | Command: dotnet {arguments}
41 | Timed out
42 | WorkingDirectory: {directory}
43 | """);
44 | }
45 |
46 | if (process.ExitCode == 0)
47 | {
48 | return await process.ReadLines();
49 | }
50 |
51 | var error = await process.StandardError.ReadToEndAsync();
52 | var output = await process.StandardOutput.ReadToEndAsync();
53 | throw new(
54 | $"""
55 | Command: dotnet {arguments}
56 | WorkingDirectory: {directory}
57 | ExitCode: {process.ExitCode}
58 | Error: {error}
59 | Output: {output}
60 | """);
61 | }
62 | }
--------------------------------------------------------------------------------
/src/PackageUpdate/Program.cs:
--------------------------------------------------------------------------------
1 | Logging.Init();
2 | await CommandRunner.RunCommand(Inner, args);
3 |
4 | static async Task Inner(string directory, string? package, bool build)
5 | {
6 | Log.Information("TargetDirectory: {TargetDirectory}", directory);
7 | if (package != null)
8 | {
9 | Log.Information("Package: {Package}", package);
10 | }
11 |
12 | if (!Directory.Exists(directory))
13 | {
14 | Log.Information("Target directory does not exist: {TargetDirectory}", directory);
15 | Environment.Exit(1);
16 | }
17 |
18 | using var cache = new SourceCacheContext
19 | {
20 | RefreshMemoryCache = true
21 | };
22 | foreach (var solution in FileSystem.FindSolutions(directory))
23 | {
24 | await TryProcessSolution(cache, solution, package, build);
25 | }
26 |
27 | if (build)
28 | {
29 | await DotnetStarter.Shutdown();
30 | }
31 | }
32 |
33 | static async Task TryProcessSolution(SourceCacheContext cache, string solution, string? package, bool build)
34 | {
35 | try
36 | {
37 | await ProcessSolution(cache, solution, package, build);
38 | }
39 | catch (Exception e)
40 | {
41 | Log.Error(
42 | """
43 | Failed to process solution: {Solution}.
44 | Error: {Message}
45 | """,
46 | solution,
47 | e.Message);
48 | }
49 | }
50 |
51 | static async Task ProcessSolution(SourceCacheContext cache, string solution, string? package, bool build)
52 | {
53 | if (Excluder.ShouldExclude(solution))
54 | {
55 | Log.Information(" Exclude: {Solution}", solution);
56 | return;
57 | }
58 |
59 | Log.Information(" {Solution}", solution);
60 |
61 | var solutionDirectory = Directory.GetParent(solution)!.FullName;
62 |
63 | var props = Path.Combine(solutionDirectory, "Directory.Packages.props");
64 | if (!File.Exists(props))
65 | {
66 | Log.Error(" Only central packages supported. Skipping: {Solution}", solution);
67 | return;
68 | }
69 |
70 | await Updater.Update(cache, props, package);
71 |
72 | if (build)
73 | {
74 | await DotnetStarter.Build(solution);
75 | }
76 | }
--------------------------------------------------------------------------------
/src/PackageUpdate/SerilogNuGetLogger.cs:
--------------------------------------------------------------------------------
1 | using ILogger = NuGet.Common.ILogger;
2 | #pragma warning disable CA2254
3 |
4 | public class SerilogNuGetLogger : ILogger
5 | {
6 | public static SerilogNuGetLogger Instance { get; } = new();
7 |
8 | public void LogDebug(string data) =>
9 | Serilog.Log.Debug(data);
10 |
11 | public void LogVerbose(string data) =>
12 | Serilog.Log.Verbose(data);
13 |
14 | public void LogInformation(string data) =>
15 | Serilog.Log.Information(data);
16 |
17 | public void LogMinimal(string data) =>
18 | Serilog.Log.Information(data);
19 |
20 | public void LogWarning(string data) =>
21 | Serilog.Log.Warning(data);
22 |
23 | public void LogError(string data) =>
24 | Serilog.Log.Error(data);
25 |
26 | public void LogInformationSummary(string data) =>
27 | Serilog.Log.Information(data);
28 |
29 | public void Log(LogLevel level, string data) =>
30 | Serilog.Log.Write(MapLogLevel(level), data);
31 |
32 | public Task LogAsync(LogLevel level, string data)
33 | {
34 | Log(level, data);
35 | return Task.CompletedTask;
36 | }
37 |
38 | public void Log(ILogMessage message)
39 | {
40 | var parser = new MessageTemplateParser();
41 | var messageTemplate = parser.Parse(message.Message);
42 |
43 | var properties = new List();
44 |
45 | if (message.Code != NuGetLogCode.Undefined)
46 | properties.Add(new("NuGetCode", new ScalarValue(message.Code)));
47 |
48 | if (!string.IsNullOrEmpty(message.ProjectPath))
49 | properties.Add(new("ProjectPath", new ScalarValue(message.ProjectPath)));
50 |
51 | if (message.WarningLevel > WarningLevel.Minimal)
52 | properties.Add(new("WarningLevel", new ScalarValue((int) message.WarningLevel)));
53 |
54 | var logEvent = new LogEvent(
55 | timestamp: message.Time,
56 | level: MapLogLevel(message.Level),
57 | exception: null,
58 | messageTemplate: messageTemplate,
59 | properties: properties);
60 |
61 | Serilog.Log.Write(logEvent);
62 | }
63 |
64 | public Task LogAsync(ILogMessage message)
65 | {
66 | Log(message);
67 | return Task.CompletedTask;
68 | }
69 |
70 | static LogEventLevel MapLogLevel(LogLevel level) =>
71 | level switch
72 | {
73 | LogLevel.Debug => LogEventLevel.Debug,
74 | LogLevel.Verbose => LogEventLevel.Verbose,
75 | LogLevel.Warning => LogEventLevel.Warning,
76 | LogLevel.Error => LogEventLevel.Error,
77 | _ => LogEventLevel.Information
78 | };
79 | }
--------------------------------------------------------------------------------
/src/Tests/CommandRunnerTests.cs:
--------------------------------------------------------------------------------
1 | public class CommandRunnerTests
2 | {
3 | string? targetDirectory;
4 | string? package;
5 | bool build;
6 |
7 | [Fact]
8 | public async Task Empty()
9 | {
10 | await CommandRunner.RunCommand(Capture);
11 | Assert.Equal(Environment.CurrentDirectory, targetDirectory);
12 | Assert.Null(package);
13 | Assert.False(build);
14 | }
15 |
16 | [Fact]
17 | public async Task SingleUnNamedArg()
18 | {
19 | await CommandRunner.RunCommand(Capture, "dir");
20 | Assert.Equal("dir", targetDirectory);
21 | Assert.Null(package);
22 | }
23 |
24 | [Fact]
25 | public async Task TargetDirectoryShort()
26 | {
27 | await CommandRunner.RunCommand(Capture, "-t", "dir");
28 | Assert.Equal(Path.GetFullPath("dir"), targetDirectory);
29 | Assert.Null(package);
30 | }
31 |
32 | [Fact]
33 | public async Task TargetDirectoryLong()
34 | {
35 | await CommandRunner.RunCommand(Capture, "--target-directory", "dir");
36 | Assert.Equal(Path.GetFullPath("dir"), targetDirectory);
37 | Assert.Null(package);
38 | }
39 |
40 | [Fact]
41 | public async Task BuildShort()
42 | {
43 | await CommandRunner.RunCommand(Capture, "-b");
44 | Assert.Equal(Environment.CurrentDirectory, targetDirectory);
45 | Assert.True(build);
46 | }
47 |
48 | [Fact]
49 | public async Task BuildLong()
50 | {
51 | await CommandRunner.RunCommand(Capture, "--build");
52 | Assert.Equal(Environment.CurrentDirectory, targetDirectory);
53 | Assert.True(build);
54 | }
55 |
56 | [Fact]
57 | public async Task PackageShort()
58 | {
59 | await CommandRunner.RunCommand(Capture, "-p", "packageName");
60 | Assert.Equal(Environment.CurrentDirectory, targetDirectory);
61 | Assert.Equal("packageName", package);
62 | }
63 |
64 | [Fact]
65 | public async Task PackageLong()
66 | {
67 | await CommandRunner.RunCommand(Capture, "--package", "packageName");
68 | Assert.Equal(Environment.CurrentDirectory, targetDirectory);
69 | Assert.Equal("packageName", package);
70 | }
71 |
72 | [Fact]
73 | public async Task All()
74 | {
75 | await CommandRunner.RunCommand(Capture, "--target-directory", "dir", "--package", "packageName", "--build");
76 | Assert.Equal(Path.GetFullPath("dir"), targetDirectory);
77 | Assert.Equal("packageName", package);
78 | Assert.True(build);
79 | }
80 |
81 | Task Capture(string targetDirectory, string? package, bool build)
82 | {
83 | this.targetDirectory = targetDirectory;
84 | this.package = package;
85 | this.build = build;
86 | return Task.CompletedTask;
87 | }
88 | }
--------------------------------------------------------------------------------
/code_of_conduct.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at simon.cropp@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/PackageUpdate/Updater.cs:
--------------------------------------------------------------------------------
1 | public static class Updater
2 | {
3 | public static async Task Update(
4 | SourceCacheContext cache,
5 | string directoryPackagesPropsPath,
6 | string? packageName)
7 | {
8 | var directory = Path.GetDirectoryName(directoryPackagesPropsPath)!;
9 |
10 | // Detect the original newline style and trailing newline
11 | var (newLine, hasTrailingNewline) = DetectNewLineInfo(directoryPackagesPropsPath);
12 |
13 | // Load the XML document
14 | var xml = XDocument.Load(directoryPackagesPropsPath);
15 |
16 | // Read current package versions
17 | var packageVersions = xml.Descendants("PackageVersion")
18 | .Select(element => new
19 | {
20 | Element = element,
21 | Package = element.Attribute("Include")?.Value,
22 | CurrentVersion = element.Attribute("Version")?.Value,
23 | Pinned = element.Attribute("Pinned")?.Value == "true"
24 | })
25 | .Where(_ => _.Package != null &&
26 | _.CurrentVersion != null &&
27 | !_.Pinned)
28 | .ToList();
29 |
30 | // Filter to specific package if requested
31 | if (!string.IsNullOrEmpty(packageName))
32 | {
33 | packageVersions = packageVersions
34 | .Where(_ => string.Equals(_.Package, packageName, StringComparison.OrdinalIgnoreCase))
35 | .ToList();
36 |
37 | if (packageVersions.Count == 0)
38 | {
39 | Log.Warning("Package {Package} not found in {FilePath}", packageName, directoryPackagesPropsPath);
40 | return;
41 | }
42 | }
43 |
44 | var sources = PackageSourceReader.Read(directory);
45 |
46 | // Update each package
47 | foreach (var package in packageVersions)
48 | {
49 | if (!NuGetVersion.TryParse(package.CurrentVersion, out var currentVersion))
50 | {
51 | continue;
52 | }
53 |
54 | var latestMetadata = await GetLatestVersion(
55 | package.Package!,
56 | currentVersion,
57 | sources,
58 | cache);
59 |
60 | if (latestMetadata == null)
61 | {
62 | continue;
63 | }
64 |
65 | var latestVersion = latestMetadata.Identity.Version;
66 |
67 | if (latestVersion <= currentVersion)
68 | {
69 | continue;
70 | }
71 |
72 | // Update the Version attribute
73 | package.Element.SetAttributeValue("Version", latestVersion.ToString());
74 | Log.Information("Updated {Package}: {NuGetVersion} -> {LatestVersion}", package.Package, currentVersion, latestVersion);
75 | }
76 |
77 | var xmlSettings = new XmlWriterSettings
78 | {
79 | OmitXmlDeclaration = true,
80 | Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
81 | Indent = true,
82 | IndentChars = " ",
83 | NewLineChars = newLine,
84 | Async = true
85 | };
86 |
87 | await using (var writer = XmlWriter.Create(directoryPackagesPropsPath, xmlSettings))
88 | {
89 | await xml.SaveAsync(writer, Cancel.None);
90 | }
91 |
92 | // Match the original trailing newline convention
93 | if (hasTrailingNewline)
94 | {
95 | await File.AppendAllTextAsync(directoryPackagesPropsPath, newLine);
96 | }
97 | }
98 |
99 | static (string newLine, bool hasTrailingNewline) DetectNewLineInfo(string filePath)
100 | {
101 | var bytes = File.ReadAllBytes(filePath);
102 | var newLine = Environment.NewLine;
103 | var hasTrailingNewline = false;
104 |
105 | // Detect newline style from first occurrence
106 | for (var i = 0; i < bytes.Length; i++)
107 | {
108 | if (bytes[i] == '\r')
109 | {
110 | if (i + 1 < bytes.Length && bytes[i + 1] == '\n')
111 | {
112 | newLine = "\r\n";
113 | }
114 | else
115 | {
116 | newLine = "\r";
117 | }
118 | break;
119 | }
120 | if (bytes[i] == '\n')
121 | {
122 | newLine = "\n";
123 | break;
124 | }
125 | }
126 |
127 | // Detect trailing newline
128 | if (bytes.Length > 0)
129 | {
130 | var lastByte = bytes[^1];
131 | hasTrailingNewline = lastByte == '\n' || lastByte == '\r';
132 | }
133 |
134 | return (newLine, hasTrailingNewline);
135 | }
136 |
137 | public static async Task GetLatestVersion(
138 | string package,
139 | NuGetVersion currentVersion,
140 | List sources,
141 | SourceCacheContext cache)
142 | {
143 | IPackageSearchMetadata? latestMetadata = null;
144 |
145 | foreach (var source in sources)
146 | {
147 | var (repository, metadataResource) = await RepositoryReader.Read(source);
148 |
149 | var condidates = await GetCondidates(package, currentVersion, cache, repository);
150 |
151 | foreach (var candidate in condidates)
152 | {
153 | var metadata = await metadataResource.GetMetadataAsync(
154 | new(package, candidate),
155 | cache,
156 | SerilogNuGetLogger.Instance,
157 | Cancel.None);
158 |
159 | // Skip unlisted packages
160 | if (metadata is not {IsListed: true})
161 | {
162 | continue;
163 | }
164 |
165 | // Found a listed version - check if it's better than what we have
166 | if (latestMetadata == null ||
167 | candidate > latestMetadata.Identity.Version)
168 | {
169 | // Found the best version from this source
170 | latestMetadata = metadata;
171 | break;
172 | }
173 | }
174 | }
175 |
176 | return latestMetadata;
177 | }
178 |
179 | static async Task> GetCondidates(string package, NuGetVersion currentVersion, SourceCacheContext cache, SourceRepository repository)
180 | {
181 | // Use FindPackageByIdResource to efficiently get version list
182 | var findResource = await repository.GetResourceAsync();
183 |
184 | var versions = await findResource.GetAllVersionsAsync(
185 | package,
186 | cache,
187 | SerilogNuGetLogger.Instance,
188 | Cancel.None);
189 |
190 | return versions
191 | .Where(v => ShouldConsiderVersion(v, currentVersion))
192 | .OrderByDescending(_ => _)
193 | .ToList();
194 | }
195 |
196 | static bool ShouldConsiderVersion(NuGetVersion candidate, NuGetVersion current)
197 | {
198 | // If current is stable, only consider stable or newer versions
199 | // If current is pre-release, consider any newer version
200 | if (!current.IsPrerelease && candidate.IsPrerelease)
201 | {
202 | return false;
203 | }
204 |
205 | return candidate > current;
206 | }
207 | }
--------------------------------------------------------------------------------
/readme.source.md:
--------------------------------------------------------------------------------
1 | #
PackageUpdate
2 |
3 | [](https://ci.appveyor.com/project/SimonCropp/PackageUpdate)
4 | [](https://www.nuget.org/packages/PackageUpdate/)
5 |
6 | A [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) that updates packages for all solutions in a directory.
7 |
8 | **See [Milestones](../../milestones?state=closed) for release notes.**
9 |
10 |
11 | ## Requirements/Caveats
12 |
13 | * .net SDK 10 is required. https://dotnet.microsoft.com/en-us/download
14 | * Only solutions using [Central Package Management (CPM)](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) are supported.
15 |
16 |
17 | ## NuGet package
18 |
19 | https://nuget.org/packages/PackageUpdate/
20 |
21 |
22 | ## Installation
23 |
24 | Ensure [dotnet CLI is installed](https://docs.microsoft.com/en-us/dotnet/core/tools/).
25 |
26 | Install [PackageUpdate](https://nuget.org/packages/PackageUpdate/)
27 |
28 | ```ps
29 | dotnet tool install -g PackageUpdate
30 | ```
31 |
32 | ## Performance characteristics
33 |
34 | 73 seconds for the following scenario
35 |
36 | * 50 solutions
37 | * 2 NuGet sources
38 | * 384 nuget packages
39 | * Network (Mbps): 94 up / 35 down. 17ms ping
40 |
41 |
42 |
43 | ## Usage
44 |
45 | ```ps
46 | packageupdate C:\Code\TargetDirectory
47 | ```
48 |
49 | If no directory is passed the current directory will be used.
50 |
51 |
52 | ### Arguments
53 |
54 |
55 | #### Target Directory
56 |
57 | ```ps
58 | packageupdate C:\Code\TargetDirectory
59 | ```
60 |
61 | ```ps
62 | packageupdate -t C:\Code\TargetDirectory
63 | ```
64 |
65 | ```ps
66 | packageupdate --target-directory C:\Code\TargetDirectory
67 | ```
68 |
69 |
70 | #### Package
71 |
72 | The package name to update. If not specified, all packages will be updated.
73 |
74 | ```ps
75 | packageupdate -p packageName
76 | ```
77 |
78 | ```ps
79 | packageupdate --package packageName
80 | ```
81 |
82 |
83 | #### Build
84 |
85 | Build the solution after the update
86 |
87 | ```ps
88 | packageupdate -b
89 | ```
90 |
91 | ```ps
92 | packageupdate --build
93 | ```
94 |
95 |
96 | ### Behavior
97 |
98 | * Recursively scan the target directory for all directories containing a `.sln` file.
99 | * Perform a [dotnet restore](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore) on the directory.
100 | * Recursively scan the directory for `*.csproj` files.
101 | * Call [dotnet list package](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-list-package) to get the list of pending packages.
102 | * Call [dotnet add package](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-add-package) with the package and version.
103 |
104 |
105 | ## PackageUpdateIgnores
106 |
107 | When processing multiple directories, it is sometimes desirable to "always ignore" certain directories. This can be done by adding a `PackageUpdateIgnores` environment variable:
108 |
109 | ```
110 | setx PackageUpdateIgnores "AspNetCore,EntityFrameworkCore"
111 | ```
112 |
113 | The value is comma separated.
114 |
115 |
116 | ## Add to Windows Explorer
117 |
118 | Use [context-menu.reg](/src/context-menu.reg) to add PackageUpdate to the Windows Explorer context menu.
119 |
120 | snippet: context-menu.reg
121 |
122 |
123 | ## Authenticated feed
124 |
125 | To use authenticated feed, add the [packageSourceCredentials](https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file#packagesourcecredentials) to the global nuget config:
126 |
127 | ```xml
128 |
129 |
130 |
131 |
132 |
133 |
134 | ```
135 |
136 |
137 | ## Package Version Pinning
138 |
139 |
140 | ### Overview
141 |
142 | prevent specific packages from being automatically updated by adding the `Pinned="true"` attribute to package entries in the `Directory.Packages.props` file.
143 |
144 |
145 | ### Usage
146 |
147 |
148 | #### Pin a Single Package
149 |
150 | ```xml
151 |
152 |
153 |
154 |
155 |
156 |
157 | ```
158 |
159 | In this example:
160 |
161 | - `System.ValueTuple` will remain at version `4.5.0` and will **not** be updated
162 | - `Newtonsoft.Json` will be updated to the latest version when running the updater
163 |
164 |
165 | #### Pin Multiple Packages
166 |
167 | ```xml
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | ```
176 |
177 |
178 | #### Document Why a Package is Pinned
179 |
180 | It's good practice to add comments explaining why a package is pinned:
181 |
182 | ```xml
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | ```
195 |
196 |
197 | ### Behavior
198 |
199 |
200 | #### When Running Update All Packages
201 |
202 | ```bash
203 | dotnet run -- update
204 | ```
205 |
206 | - All packages **without** `Pinned="true"` will be checked for updates
207 | - Pinned packages are skipped entirely
208 |
209 |
210 | #### When Running Update Specific Package
211 |
212 | ```bash
213 | dotnet run -- update --package System.ValueTuple
214 | ```
215 |
216 | - Even when explicitly targeting a pinned package, it will **not** be updated
217 | - The pin is always respected, regardless of how the updater is invoked
218 |
219 |
220 | ### Common Use Cases
221 |
222 |
223 | #### 1. Breaking Changes
224 |
225 | Pin packages when newer versions introduce breaking changes you're not ready to handle:
226 |
227 | ```xml
228 |
229 | ```
230 |
231 |
232 | #### 2. Framework Constraints
233 |
234 | Pin packages that have specific framework version requirements:
235 |
236 | ```xml
237 |
238 |
239 | ```
240 |
241 |
242 | #### 3. Security Fixes
243 |
244 | Pin to a specific patched version while waiting for a proper migration:
245 |
246 | ```xml
247 |
248 |
249 | ```
250 |
251 |
252 | #### 4. Performance Regressions
253 |
254 | Pin when a newer version causes performance issues:
255 |
256 | ```xml
257 |
258 |
259 | ```
260 |
261 |
262 | #### 5. Vendor Dependencies
263 |
264 | Pin packages that must match versions used by third-party SDKs:
265 |
266 | ```xml
267 |
268 |
269 | ```
270 |
271 |
272 | ### Unpinning a Package
273 |
274 | To allow a package to be updated again, remove the `Pinned="true"` attribute:
275 |
276 | ```xml
277 |
278 |
279 |
280 |
281 |
282 | ```
283 |
284 | The next time you run the updater, it will update to the latest version.
285 |
286 |
287 | ### Technical Details
288 |
289 | - The `Pinned` attribute is a custom attribute used by this updater tool
290 | - It has no effect on NuGet's normal package resolution
291 | - The attribute follows MSBuild conventions (similar to how `Pinned` works in project files)
292 | - Comments and formatting around pinned packages are preserved during updates
293 |
294 |
295 | ## Icon
296 |
297 | [Update](https://thenounproject.com/search/?q=update&i=2060555) by [Andy Miranda](https://thenounproject.com/andylontuan88) from [The Noun Project](https://thenounproject.com/).
298 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | #
PackageUpdate
9 |
10 | [](https://ci.appveyor.com/project/SimonCropp/PackageUpdate)
11 | [](https://www.nuget.org/packages/PackageUpdate/)
12 |
13 | A [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) that updates packages for all solutions in a directory.
14 |
15 | **See [Milestones](../../milestones?state=closed) for release notes.**
16 |
17 |
18 | ## Requirements/Caveats
19 |
20 | * .net SDK 10 is required. https://dotnet.microsoft.com/en-us/download
21 | * Only solutions using [Central Package Management (CPM)](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) are supported.
22 |
23 |
24 | ## NuGet package
25 |
26 | https://nuget.org/packages/PackageUpdate/
27 |
28 |
29 | ## Installation
30 |
31 | Ensure [dotnet CLI is installed](https://docs.microsoft.com/en-us/dotnet/core/tools/).
32 |
33 | Install [PackageUpdate](https://nuget.org/packages/PackageUpdate/)
34 |
35 | ```ps
36 | dotnet tool install -g PackageUpdate
37 | ```
38 |
39 | ## Performance characteristics
40 |
41 | 73 seconds for the following scenario
42 |
43 | * 50 solutions
44 | * 2 NuGet sources
45 | * 384 nuget packages
46 | * Network (Mbps): 94 up / 35 down. 17ms ping
47 |
48 |
49 |
50 | ## Usage
51 |
52 | ```ps
53 | packageupdate C:\Code\TargetDirectory
54 | ```
55 |
56 | If no directory is passed the current directory will be used.
57 |
58 |
59 | ### Arguments
60 |
61 |
62 | #### Target Directory
63 |
64 | ```ps
65 | packageupdate C:\Code\TargetDirectory
66 | ```
67 |
68 | ```ps
69 | packageupdate -t C:\Code\TargetDirectory
70 | ```
71 |
72 | ```ps
73 | packageupdate --target-directory C:\Code\TargetDirectory
74 | ```
75 |
76 |
77 | #### Package
78 |
79 | The package name to update. If not specified, all packages will be updated.
80 |
81 | ```ps
82 | packageupdate -p packageName
83 | ```
84 |
85 | ```ps
86 | packageupdate --package packageName
87 | ```
88 |
89 |
90 | #### Build
91 |
92 | Build the solution after the update
93 |
94 | ```ps
95 | packageupdate -b
96 | ```
97 |
98 | ```ps
99 | packageupdate --build
100 | ```
101 |
102 |
103 | ### Behavior
104 |
105 | * Recursively scan the target directory for all directories containing a `.sln` file.
106 | * Perform a [dotnet restore](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore) on the directory.
107 | * Recursively scan the directory for `*.csproj` files.
108 | * Call [dotnet list package](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-list-package) to get the list of pending packages.
109 | * Call [dotnet add package](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-add-package) with the package and version.
110 |
111 |
112 | ## PackageUpdateIgnores
113 |
114 | When processing multiple directories, it is sometimes desirable to "always ignore" certain directories. This can be done by adding a `PackageUpdateIgnores` environment variable:
115 |
116 | ```
117 | setx PackageUpdateIgnores "AspNetCore,EntityFrameworkCore"
118 | ```
119 |
120 | The value is comma separated.
121 |
122 |
123 | ## Add to Windows Explorer
124 |
125 | Use [context-menu.reg](/src/context-menu.reg) to add PackageUpdate to the Windows Explorer context menu.
126 |
127 |
128 |
129 | ```reg
130 | Windows Registry Editor Version 5.00
131 | [HKEY_CLASSES_ROOT\Directory\Shell]
132 | @="none"
133 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdate]
134 | "MUIVerb"="run packageupdate"
135 | "Position"="bottom"
136 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdate]
137 | "MUIVerb"="run packageupdate"
138 | "Position"="bottom"
139 | [HKEY_CLASSES_ROOT\Directory\shell\packageupdate\command]
140 | @="cmd.exe /c packageupdate \"%V\""
141 | [HKEY_CLASSES_ROOT\Directory\Background\shell\packageupdate\command]
142 | @="cmd.exe /c packageupdate \"%V\""
143 | ```
144 | snippet source | anchor
145 |
146 |
147 |
148 | ## Authenticated feed
149 |
150 | To use authenticated feed, add the [packageSourceCredentials](https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file#packagesourcecredentials) to the global nuget config:
151 |
152 | ```xml
153 |
154 |
155 |
156 |
157 |
158 |
159 | ```
160 |
161 |
162 | ## Package Version Pinning
163 |
164 |
165 | ### Overview
166 |
167 | prevent specific packages from being automatically updated by adding the `Pinned="true"` attribute to package entries in the `Directory.Packages.props` file.
168 |
169 |
170 | ### Usage
171 |
172 |
173 | #### Pin a Single Package
174 |
175 | ```xml
176 |
177 |
178 |
179 |
180 |
181 |
182 | ```
183 |
184 | In this example:
185 |
186 | - `System.ValueTuple` will remain at version `4.5.0` and will **not** be updated
187 | - `Newtonsoft.Json` will be updated to the latest version when running the updater
188 |
189 |
190 | #### Pin Multiple Packages
191 |
192 | ```xml
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | ```
201 |
202 |
203 | #### Document Why a Package is Pinned
204 |
205 | It's good practice to add comments explaining why a package is pinned:
206 |
207 | ```xml
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | ```
220 |
221 |
222 | ### Behavior
223 |
224 |
225 | #### When Running Update All Packages
226 |
227 | ```bash
228 | dotnet run -- update
229 | ```
230 |
231 | - All packages **without** `Pinned="true"` will be checked for updates
232 | - Pinned packages are skipped entirely
233 |
234 |
235 | #### When Running Update Specific Package
236 |
237 | ```bash
238 | dotnet run -- update --package System.ValueTuple
239 | ```
240 |
241 | - Even when explicitly targeting a pinned package, it will **not** be updated
242 | - The pin is always respected, regardless of how the updater is invoked
243 |
244 |
245 | ### Common Use Cases
246 |
247 |
248 | #### 1. Breaking Changes
249 |
250 | Pin packages when newer versions introduce breaking changes you're not ready to handle:
251 |
252 | ```xml
253 |
254 | ```
255 |
256 |
257 | #### 2. Framework Constraints
258 |
259 | Pin packages that have specific framework version requirements:
260 |
261 | ```xml
262 |
263 |
264 | ```
265 |
266 |
267 | #### 3. Security Fixes
268 |
269 | Pin to a specific patched version while waiting for a proper migration:
270 |
271 | ```xml
272 |
273 |
274 | ```
275 |
276 |
277 | #### 4. Performance Regressions
278 |
279 | Pin when a newer version causes performance issues:
280 |
281 | ```xml
282 |
283 |
284 | ```
285 |
286 |
287 | #### 5. Vendor Dependencies
288 |
289 | Pin packages that must match versions used by third-party SDKs:
290 |
291 | ```xml
292 |
293 |
294 | ```
295 |
296 |
297 | ### Unpinning a Package
298 |
299 | To allow a package to be updated again, remove the `Pinned="true"` attribute:
300 |
301 | ```xml
302 |
303 |
304 |
305 |
306 |
307 | ```
308 |
309 | The next time you run the updater, it will update to the latest version.
310 |
311 |
312 | ### Technical Details
313 |
314 | - The `Pinned` attribute is a custom attribute used by this updater tool
315 | - It has no effect on NuGet's normal package resolution
316 | - The attribute follows MSBuild conventions (similar to how `Pinned` works in project files)
317 | - Comments and formatting around pinned packages are preserved during updates
318 |
319 |
320 | ## Icon
321 |
322 | [Update](https://thenounproject.com/search/?q=update&i=2060555) by [Andy Miranda](https://thenounproject.com/andylontuan88) from [The Noun Project](https://thenounproject.com/).
323 |
--------------------------------------------------------------------------------
/src/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | [*.cs]
9 | indent_size = 4
10 | charset = utf-8
11 |
12 | # Redundant accessor body
13 | resharper_redundant_accessor_body_highlighting = error
14 |
15 | # Replace with field keyword
16 | resharper_replace_with_field_keyword_highlighting = error
17 |
18 | # Replace with single call to Single(..)
19 | resharper_replace_with_single_call_to_single_highlighting = error
20 |
21 | # Replace with single call to SingleOrDefault(..)
22 | resharper_replace_with_single_call_to_single_or_default_highlighting = error
23 |
24 | # Replace with single call to LastOrDefault(..)
25 | resharper_replace_with_single_call_to_last_or_default_highlighting = error
26 |
27 | # Element is localizable
28 | resharper_localizable_element_highlighting = none
29 |
30 | # Replace with single call to Last(..)
31 | resharper_replace_with_single_call_to_last_highlighting = error
32 |
33 | # Replace with single call to First(..)
34 | resharper_replace_with_single_call_to_first_highlighting = error
35 |
36 | # Replace with single call to FirstOrDefault(..)
37 | resharper_replace_with_single_call_to_first_or_default_highlighting = error
38 |
39 | # Replace with single call to Any(..)
40 | resharper_replace_with_single_call_to_any_highlighting = error
41 |
42 | # Simplify negative equality expression
43 | resharper_negative_equality_expression_highlighting = error
44 |
45 | # Replace with single call to Count(..)
46 | resharper_replace_with_single_call_to_count_highlighting = error
47 |
48 | # Declare types in namespaces
49 | dotnet_diagnostic.CA1050.severity = none
50 |
51 | # Use Literals Where Appropriate
52 | dotnet_diagnostic.CA1802.severity = error
53 |
54 | # Template should be a static expression
55 | dotnet_diagnostic.CA2254.severity = error
56 |
57 | # Potentially misleading parameter name in lambda or local function
58 | resharper_all_underscore_local_parameter_name_highlighting = none
59 |
60 | # Redundant explicit collection creation in argument of 'params' parameter
61 | resharper_redundant_explicit_params_array_creation_highlighting = error
62 |
63 | # Do not initialize unnecessarily
64 | dotnet_diagnostic.CA1805.severity = error
65 |
66 | # Avoid unsealed attributes
67 | dotnet_diagnostic.CA1813.severity = error
68 |
69 | # Test for empty strings using string length
70 | dotnet_diagnostic.CA1820.severity = none
71 |
72 | # Remove empty finalizers
73 | dotnet_diagnostic.CA1821.severity = error
74 |
75 | # Mark members as static
76 | dotnet_diagnostic.CA1822.severity = error
77 |
78 | # Avoid unused private fields
79 | dotnet_diagnostic.CA1823.severity = error
80 |
81 | # Avoid zero-length array allocations
82 | dotnet_diagnostic.CA1825.severity = error
83 |
84 | # Use property instead of Linq Enumerable method
85 | dotnet_diagnostic.CA1826.severity = error
86 |
87 | # Do not use Count()/LongCount() when Any() can be used
88 | dotnet_diagnostic.CA1827.severity = error
89 | dotnet_diagnostic.CA1828.severity = error
90 |
91 | # Use Length/Count property instead of Enumerable.Count method
92 | dotnet_diagnostic.CA1829.severity = error
93 |
94 | # Prefer strongly-typed Append and Insert method overloads on StringBuilder
95 | dotnet_diagnostic.CA1830.severity = error
96 |
97 | # Use AsSpan instead of Range-based indexers for string when appropriate
98 | dotnet_diagnostic.CA1831.severity = error
99 |
100 | # Use AsSpan instead of Range-based indexers for string when appropriate
101 | dotnet_diagnostic.CA1831.severity = error
102 | dotnet_diagnostic.CA1832.severity = error
103 | dotnet_diagnostic.CA1833.severity = error
104 |
105 | # Use StringBuilder.Append(char) for single character strings
106 | dotnet_diagnostic.CA1834.severity = error
107 |
108 | # Prefer IsEmpty over Count when available
109 | dotnet_diagnostic.CA1836.severity = error
110 |
111 | # Prefer IsEmpty over Count when available
112 | dotnet_diagnostic.CA1836.severity = error
113 |
114 | # Use Environment.ProcessId instead of Process.GetCurrentProcess().Id
115 | dotnet_diagnostic.CA1837.severity = error
116 |
117 | # Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName
118 | dotnet_diagnostic.CA1839.severity = error
119 |
120 | # Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId
121 | dotnet_diagnostic.CA1840.severity = error
122 |
123 | # Prefer Dictionary Contains methods
124 | dotnet_diagnostic.CA1841.severity = error
125 |
126 | # Do not use WhenAll with a single task
127 | dotnet_diagnostic.CA1842.severity = error
128 |
129 | # Do not use WhenAll/WaitAll with a single task
130 | dotnet_diagnostic.CA1842.severity = error
131 | dotnet_diagnostic.CA1843.severity = error
132 |
133 | # Use span-based 'string.Concat'
134 | dotnet_diagnostic.CA1845.severity = error
135 |
136 | # Prefer AsSpan over Substring
137 | dotnet_diagnostic.CA1846.severity = error
138 |
139 | # Use string.Contains(char) instead of string.Contains(string) with single characters
140 | dotnet_diagnostic.CA1847.severity = error
141 |
142 | # Prefer static HashData method over ComputeHash
143 | dotnet_diagnostic.CA1850.severity = error
144 |
145 | # Possible multiple enumerations of IEnumerable collection
146 | dotnet_diagnostic.CA1851.severity = error
147 |
148 | # Unnecessary call to Dictionary.ContainsKey(key)
149 | dotnet_diagnostic.CA1853.severity = error
150 |
151 | # Prefer the IDictionary.TryGetValue(TKey, out TValue) method
152 | dotnet_diagnostic.CA1854.severity = error
153 |
154 | # Use Span.Clear() instead of Span.Fill()
155 | dotnet_diagnostic.CA1855.severity = error
156 |
157 | # Incorrect usage of ConstantExpected attribute
158 | dotnet_diagnostic.CA1856.severity = error
159 |
160 | # The parameter expects a constant for optimal performance
161 | dotnet_diagnostic.CA1857.severity = error
162 |
163 | # Use StartsWith instead of IndexOf
164 | dotnet_diagnostic.CA1858.severity = error
165 |
166 | # Avoid using Enumerable.Any() extension method
167 | dotnet_diagnostic.CA1860.severity = error
168 |
169 | # Avoid constant arrays as arguments
170 | dotnet_diagnostic.CA1861.severity = error
171 |
172 | # Use the StringComparison method overloads to perform case-insensitive string comparisons
173 | dotnet_diagnostic.CA1862.severity = error
174 |
175 | # Prefer the IDictionary.TryAdd(TKey, TValue) method
176 | dotnet_diagnostic.CA1864.severity = error
177 |
178 | # Use string.Method(char) instead of string.Method(string) for string with single char
179 | dotnet_diagnostic.CA1865.severity = error
180 | dotnet_diagnostic.CA1866.severity = error
181 | dotnet_diagnostic.CA1867.severity = error
182 |
183 | # Unnecessary call to 'Contains' for sets
184 | dotnet_diagnostic.CA1868.severity = error
185 |
186 | # Cache and reuse 'JsonSerializerOptions' instances
187 | dotnet_diagnostic.CA1869.severity = error
188 |
189 | # Use a cached 'SearchValues' instance
190 | dotnet_diagnostic.CA1870.severity = error
191 |
192 | # Microsoft .NET properties
193 | trim_trailing_whitespace = true
194 | csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion
195 | resharper_namespace_body = file_scoped
196 | dotnet_naming_rule.private_constants_rule.severity = warning
197 | dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style
198 | dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
199 | dotnet_naming_rule.private_instance_fields_rule.severity = warning
200 | dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
201 | dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
202 | dotnet_naming_rule.private_static_fields_rule.severity = warning
203 | dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
204 | dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
205 | dotnet_naming_rule.private_static_readonly_rule.severity = warning
206 | dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style
207 | dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
208 | dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
209 | dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
210 | dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
211 | dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
212 | dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
213 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
214 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
215 | dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
216 | dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
217 | dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
218 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
219 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
220 | dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
221 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
222 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
223 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
224 |
225 | # ReSharper properties
226 | resharper_object_creation_when_type_not_evident = target_typed
227 |
228 | # ReSharper inspection severities
229 | resharper_arrange_object_creation_when_type_evident_highlighting = error
230 | resharper_arrange_object_creation_when_type_not_evident_highlighting = error
231 | resharper_arrange_redundant_parentheses_highlighting = error
232 | resharper_arrange_static_member_qualifier_highlighting = error
233 | resharper_arrange_this_qualifier_highlighting = error
234 | resharper_arrange_type_member_modifiers_highlighting = none
235 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint
236 | resharper_built_in_type_reference_style_highlighting = hint
237 | resharper_check_namespace_highlighting = none
238 | resharper_convert_to_using_declaration_highlighting = error
239 | resharper_field_can_be_made_read_only_local_highlighting = none
240 | resharper_merge_into_logical_pattern_highlighting = warning
241 | resharper_merge_into_pattern_highlighting = error
242 | resharper_method_has_async_overload_highlighting = warning
243 | # because stop rider giving errors before source generators have run
244 | resharper_partial_type_with_single_part_highlighting = warning
245 | resharper_redundant_base_qualifier_highlighting = warning
246 | resharper_redundant_cast_highlighting = error
247 | resharper_redundant_empty_object_creation_argument_list_highlighting = error
248 | resharper_redundant_empty_object_or_collection_initializer_highlighting = error
249 | resharper_redundant_name_qualifier_highlighting = error
250 | resharper_redundant_suppress_nullable_warning_expression_highlighting = error
251 | resharper_redundant_using_directive_highlighting = error
252 | resharper_redundant_verbatim_string_prefix_highlighting = error
253 | resharper_redundant_lambda_signature_parentheses_highlighting = error
254 | resharper_replace_substring_with_range_indexer_highlighting = warning
255 | resharper_suggest_var_or_type_built_in_types_highlighting = error
256 | resharper_suggest_var_or_type_elsewhere_highlighting = error
257 | resharper_suggest_var_or_type_simple_types_highlighting = error
258 | resharper_unnecessary_whitespace_highlighting = error
259 | resharper_use_await_using_highlighting = warning
260 | resharper_use_deconstruction_highlighting = warning
261 |
262 | # Sort using and Import directives with System.* appearing first
263 | dotnet_sort_system_directives_first = true
264 |
265 | # Avoid "this." and "Me." if not necessary
266 | dotnet_style_qualification_for_field = false:error
267 | dotnet_style_qualification_for_property = false:error
268 | dotnet_style_qualification_for_method = false:error
269 | dotnet_style_qualification_for_event = false:error
270 |
271 | # Use language keywords instead of framework type names for type references
272 | dotnet_style_predefined_type_for_locals_parameters_members = true:error
273 | dotnet_style_predefined_type_for_member_access = true:error
274 |
275 | # Suggest more modern language features when available
276 | dotnet_style_object_initializer = true:error
277 | dotnet_style_collection_initializer = true:error
278 | dotnet_style_coalesce_expression = false:error
279 | dotnet_style_null_propagation = true:error
280 | dotnet_style_explicit_tuple_names = true:error
281 |
282 | # Use collection expression syntax
283 | resharper_use_collection_expression_highlighting = error
284 |
285 | # Prefer "var" everywhere
286 | csharp_style_var_for_built_in_types = true:error
287 | csharp_style_var_when_type_is_apparent = true:error
288 | csharp_style_var_elsewhere = true:error
289 |
290 | # Prefer method-like constructs to have a block body
291 | csharp_style_expression_bodied_methods = true:error
292 | csharp_style_expression_bodied_local_functions = true:error
293 | csharp_style_expression_bodied_constructors = true:error
294 | csharp_style_expression_bodied_operators = true:error
295 | resharper_place_expr_method_on_single_line = false
296 |
297 | # Prefer property-like constructs to have an expression-body
298 | csharp_style_expression_bodied_properties = true:error
299 | csharp_style_expression_bodied_indexers = true:error
300 | csharp_style_expression_bodied_accessors = true:error
301 |
302 | # Suggest more modern language features when available
303 | csharp_style_pattern_matching_over_is_with_cast_check = true:error
304 | csharp_style_pattern_matching_over_as_with_null_check = true:error
305 | csharp_style_inlined_variable_declaration = true:suggestion
306 | csharp_style_throw_expression = true:suggestion
307 | csharp_style_conditional_delegate_call = true:suggestion
308 |
309 | # Newline settings
310 | #csharp_new_line_before_open_brace = all:error
311 | resharper_max_array_initializer_elements_on_line = 1
312 | csharp_new_line_before_else = true
313 | csharp_new_line_before_catch = true
314 | csharp_new_line_before_finally = true
315 | csharp_new_line_before_members_in_object_initializers = true
316 | csharp_new_line_before_members_in_anonymous_types = true
317 | resharper_wrap_before_first_type_parameter_constraint = true
318 | resharper_wrap_extends_list_style = chop_always
319 | resharper_wrap_after_dot_in_method_calls = false
320 | resharper_wrap_before_binary_pattern_op = false
321 | resharper_wrap_object_and_collection_initializer_style = chop_always
322 | resharper_place_simple_initializer_on_single_line = false
323 |
324 | # space
325 | resharper_space_around_lambda_arrow = true
326 |
327 | dotnet_style_require_accessibility_modifiers = never:error
328 | resharper_place_type_constraints_on_same_line = false
329 | resharper_blank_lines_inside_namespace = 0
330 | resharper_blank_lines_after_file_scoped_namespace_directive = 1
331 | resharper_blank_lines_inside_type = 0
332 |
333 | insert_final_newline = false
334 | resharper_place_attribute_on_same_line = false
335 |
336 | #braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces
337 | resharper_braces_for_ifelse = required
338 | resharper_braces_for_foreach = required
339 | resharper_braces_for_while = required
340 | resharper_braces_for_dowhile = required
341 | resharper_braces_for_lock = required
342 | resharper_braces_for_fixed = required
343 | resharper_braces_for_for = required
344 |
345 | resharper_return_value_of_pure_method_is_not_used_highlighting = error
346 |
347 |
348 | resharper_misleading_body_like_statement_highlighting = error
349 |
350 | resharper_redundant_record_class_keyword_highlighting = error
351 |
352 | resharper_redundant_extends_list_entry_highlighting = error
353 |
354 | resharper_redundant_type_arguments_inside_nameof_highlighting = error
355 |
356 | # Xml files
357 | [*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}]
358 | indent_size = 2
359 | # https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi
360 | resharper_blank_line_after_pi = false
361 | resharper_space_before_self_closing = true
362 | ij_xml_space_inside_empty_tag = true
363 |
364 | [*.json]
365 | indent_size = 2
366 |
367 | # Verify settings
368 | [*.{received,verified}.{txt,xml,json,md,sql,csv,html,htm,nuspec,rels}]
369 | charset = utf-8-bom
370 | end_of_line = lf
371 | indent_size = unset
372 | indent_style = unset
373 | insert_final_newline = false
374 | tab_width = unset
375 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/src/Shared.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | False
3 | Quiet
4 | True
5 | True
6 | True
7 | DO_NOT_SHOW
8 | ERROR
9 | ERROR
10 | ERROR
11 | WARNING
12 | ERROR
13 | ERROR
14 | ERROR
15 | ERROR
16 | ERROR
17 | ERROR
18 | ERROR
19 | ERROR
20 | ERROR
21 | ERROR
22 | ERROR
23 | ERROR
24 | ERROR
25 | ERROR
26 | ERROR
27 | ERROR
28 | ERROR
29 | ERROR
30 | ERROR
31 | ERROR
32 | ERROR
33 | ERROR
34 | ERROR
35 | ERROR
36 | ERROR
37 | ERROR
38 | ERROR
39 | ERROR
40 | ERROR
41 | ERROR
42 | ERROR
43 | DO_NOT_SHOW
44 | DO_NOT_SHOW
45 | ERROR
46 | ERROR
47 | ERROR
48 | ERROR
49 | ERROR
50 | ERROR
51 | ERROR
52 | ERROR
53 | ERROR
54 | ERROR
55 | ERROR
56 | ERROR
57 | C90+,E79+,S14+
58 | ERROR
59 | ERROR
60 | ERROR
61 | ERROR
62 | ERROR
63 | ERROR
64 | ERROR
65 | ERROR
66 | ERROR
67 | ERROR
68 | ERROR
69 | ERROR
70 | ERROR
71 | ERROR
72 | ERROR
73 | ERROR
74 | ERROR
75 | ERROR
76 | ERROR
77 | ERROR
78 | ERROR
79 | ERROR
80 | ERROR
81 | ERROR
82 | ERROR
83 | ERROR
84 | ERROR
85 | ERROR
86 | ERROR
87 | ERROR
88 | ERROR
89 | ERROR
90 | ERROR
91 | ERROR
92 | ERROR
93 | ERROR
94 | ERROR
95 | ERROR
96 | ERROR
97 | ERROR
98 | ERROR
99 | ERROR
100 | ERROR
101 | ERROR
102 | ERROR
103 | ERROR
104 | ERROR
105 | ERROR
106 | ERROR
107 | ERROR
108 | ERROR
109 | ERROR
110 | ERROR
111 | ERROR
112 | ERROR
113 | ERROR
114 | ERROR
115 | ERROR
116 | ERROR
117 | ERROR
118 | ERROR
119 | ERROR
120 | ERROR
121 | ERROR
122 | DO_NOT_SHOW
123 | *.received.*
124 | *.verified.*
125 | ERROR
126 | ERROR
127 | DO_NOT_SHOW
128 | ECMAScript 2016
129 | <?xml version="1.0" encoding="utf-16"?><Profile name="c# Cleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JSStringLiteralQuotesDescriptor>True</JSStringLiteralQuotesDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><HtmlReformatCode>True</HtmlReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS><profile version="1.0">
130 | <option name="myName" value="c# Cleanup" />
131 | </profile></IDEA_SETTINGS><RIDER_SETTINGS><profile>
132 | <Language id="EditorConfig">
133 | <Reformat>false</Reformat>
134 | </Language>
135 | <Language id="HTML">
136 | <OptimizeImports>false</OptimizeImports>
137 | <Reformat>false</Reformat>
138 | <Rearrange>false</Rearrange>
139 | </Language>
140 | <Language id="JSON">
141 | <Reformat>false</Reformat>
142 | </Language>
143 | <Language id="RELAX-NG">
144 | <Reformat>false</Reformat>
145 | </Language>
146 | <Language id="XML">
147 | <OptimizeImports>false</OptimizeImports>
148 | <Reformat>false</Reformat>
149 | <Rearrange>false</Rearrange>
150 | </Language>
151 | </profile></RIDER_SETTINGS></Profile>
152 | ExpressionBody
153 | ExpressionBody
154 | ExpressionBody
155 | False
156 | NEVER
157 | NEVER
158 | False
159 | False
160 | False
161 | True
162 | False
163 | CHOP_ALWAYS
164 | False
165 | False
166 | RemoveIndent
167 | RemoveIndent
168 | False
169 | True
170 | True
171 | True
172 | True
173 | True
174 | ERROR
175 | DoNothing
176 |
--------------------------------------------------------------------------------
/src/Tests/UpdaterTests.cs:
--------------------------------------------------------------------------------
1 | public class UpdaterTests
2 | {
3 | [Fact]
4 | public async Task UpdateAllPackages()
5 | {
6 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
7 | var content =
8 | """
9 |
10 |
11 | true
12 |
13 |
14 |
15 |
16 |
17 |
18 | """;
19 |
20 | using var tempFile = await TempFile.CreateText(content);
21 |
22 | await Updater.Update(cache, tempFile.Path, null);
23 |
24 | var result = await File.ReadAllTextAsync(tempFile.Path);
25 | await Verify(result);
26 | }
27 |
28 | [Fact]
29 | public async Task UpdateSinglePackage()
30 | {
31 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
32 | var content =
33 | """
34 |
35 |
36 | true
37 |
38 |
39 |
40 |
41 |
42 |
43 | """;
44 |
45 | using var tempFile = await TempFile.CreateText(content);
46 |
47 | await Updater.Update(cache, tempFile.Path, "Newtonsoft.Json");
48 |
49 | var result = await File.ReadAllTextAsync(tempFile.Path);
50 | await Verify(result);
51 | }
52 |
53 | [Fact]
54 | public async Task UpdateSinglePackage_CaseInsensitive()
55 | {
56 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
57 | var content =
58 | """
59 |
60 |
61 |
62 |
63 |
64 |
65 | """;
66 |
67 | using var tempFile = await TempFile.CreateText(content);
68 |
69 | await Updater.Update(cache, tempFile.Path, "newtonsoft.json");
70 |
71 | var result = await File.ReadAllTextAsync(tempFile.Path);
72 | await Verify(result);
73 | }
74 |
75 | [Fact]
76 | public async Task UpdatePackageNotFound()
77 | {
78 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
79 | var content =
80 | """
81 |
82 |
83 |
84 |
85 |
86 | """;
87 |
88 | using var tempFile = await TempFile.CreateText(content);
89 |
90 | await Updater.Update(cache, tempFile.Path, "NonExistentPackage");
91 |
92 | var result = await File.ReadAllTextAsync(tempFile.Path);
93 |
94 | // File should be unchanged
95 | Assert.Equal(content, result);
96 | }
97 |
98 | [Fact]
99 | public async Task PreservesFormatting()
100 | {
101 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
102 | var content =
103 | """
104 |
105 |
106 |
107 | true
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | """;
116 |
117 | using var tempFile = await TempFile.CreateText(content);
118 |
119 | await Updater.Update(cache, tempFile.Path, null);
120 |
121 | var result = await File.ReadAllTextAsync(tempFile.Path);
122 |
123 | // Verify comments are preserved
124 | Assert.Contains("", result);
125 | Assert.Contains("", result);
126 | }
127 |
128 | [Fact]
129 | public async Task SkipsInvalidVersions()
130 | {
131 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
132 | var content =
133 | """
134 |
135 |
136 |
137 |
138 |
139 |
140 | """;
141 |
142 | using var tempFile = await TempFile.CreateText(content);
143 |
144 | await Updater.Update(cache, tempFile.Path, null);
145 |
146 | var result = await File.ReadAllTextAsync(tempFile.Path);
147 |
148 | // Invalid version should remain unchanged
149 | Assert.Contains("not-a-version", result);
150 | }
151 |
152 | static List sources = [new("https://api.nuget.org/v3/index.json")];
153 |
154 | [Fact]
155 | public async Task GetLatestVersion_StableToStable()
156 | {
157 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
158 | var currentVersion = NuGetVersion.Parse("12.0.1");
159 |
160 | var result = await Updater.GetLatestVersion(
161 | "Newtonsoft.Json",
162 | currentVersion,
163 | sources,
164 | cache);
165 |
166 | Assert.NotNull(result);
167 | Assert.True(result.Identity.Version > currentVersion);
168 | Assert.False(result.Identity.Version.IsPrerelease);
169 | }
170 |
171 | [Fact]
172 | public async Task GetLatestVersion_PreReleaseToPreRelease()
173 | {
174 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
175 | var currentVersion = NuGetVersion.Parse("1.0.0-beta.1");
176 |
177 | var result = await Updater.GetLatestVersion(
178 | "Verify",
179 | currentVersion,
180 | sources,
181 | cache);
182 |
183 | Assert.NotNull(result);
184 | Assert.True(result.Identity.Version > currentVersion);
185 | }
186 |
187 | [Fact]
188 | public async Task GetLatestVersion_DoesNotDowngradeStableToPreRelease()
189 | {
190 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
191 | var currentVersion = NuGetVersion.Parse("13.0.1");
192 |
193 | var result = await Updater.GetLatestVersion(
194 | "Newtonsoft.Json",
195 | currentVersion,
196 | sources,
197 | cache);
198 |
199 | if (result != null)
200 | {
201 | Assert.False(result.Identity.Version.IsPrerelease);
202 | }
203 | }
204 |
205 | [Fact]
206 | public async Task GetLatestVersion_PackageNotFound()
207 | {
208 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
209 | var currentVersion = NuGetVersion.Parse("1.0.0");
210 |
211 | var result = await Updater.GetLatestVersion(
212 | "ThisPackageDefinitelyDoesNotExist12345",
213 | currentVersion,
214 | sources,
215 | cache);
216 |
217 | Assert.Null(result);
218 | }
219 |
220 | [Fact]
221 | public async Task GetLatestVersion_AlreadyLatest()
222 | {
223 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
224 |
225 | // Use a very high version number
226 | var currentVersion = NuGetVersion.Parse("999.999.999");
227 |
228 | var result = await Updater.GetLatestVersion(
229 | "Newtonsoft.Json",
230 | currentVersion,
231 | sources,
232 | cache);
233 |
234 | Assert.Null(result);
235 | }
236 |
237 | [Fact]
238 | public async Task GetLatestVersion_ReturnsMetadata()
239 | {
240 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
241 | var currentVersion = NuGetVersion.Parse("12.0.1");
242 |
243 | var result = await Updater.GetLatestVersion(
244 | "Newtonsoft.Json",
245 | currentVersion,
246 | sources,
247 | cache);
248 |
249 | Assert.NotNull(result);
250 |
251 | var metadata = new
252 | {
253 | result.Identity.Id,
254 | Version = result.Identity.Version.ToString(),
255 | HasVersion = result.Identity.Version != null,
256 | result.Identity.Version?.IsPrerelease
257 | };
258 |
259 | await Verify(metadata);
260 | }
261 |
262 | [Fact]
263 | public async Task UsesLocalNuGetConfig()
264 | {
265 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
266 | var nugetConfig =
267 | """
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 | """;
276 |
277 | var packages =
278 | """
279 |
280 |
281 |
282 |
283 |
284 | """;
285 |
286 | using var directory = new TempDirectory();
287 | var nugetConfigPath = Path.Combine(directory, "nuget.config");
288 | var packagesPath = Path.Combine(directory, "Directory.Packages.props");
289 |
290 | await File.WriteAllTextAsync(nugetConfigPath, nugetConfig);
291 | await File.WriteAllTextAsync(packagesPath, packages);
292 |
293 | await Updater.Update(cache, packagesPath, null);
294 |
295 | var result = await File.ReadAllTextAsync(packagesPath);
296 |
297 | // Verify the package was updated (should have a newer version than 12.0.1)
298 | Assert.DoesNotContain("Version=\"12.0.1\"", result);
299 | }
300 |
301 | [Fact]
302 | public async Task WarnsAndReturnsWhenNoNuGetConfig()
303 | {
304 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
305 | var packages =
306 | """
307 |
308 |
309 |
310 |
311 |
312 | """;
313 |
314 | using var directory = new TempDirectory();
315 | var packagesPath = Path.Combine(directory, "Directory.Packages.props");
316 |
317 | await File.WriteAllTextAsync(packagesPath, packages);
318 |
319 | // Should not throw, just log warning and return
320 | await Updater.Update(cache, packagesPath, null);
321 |
322 | var result = await File.ReadAllTextAsync(packagesPath);
323 |
324 | // Verify the package was updated (should have a newer version than 12.0.1)
325 | Assert.DoesNotContain("Version=\"12.0.1\"", result);
326 | }
327 |
328 | [Fact]
329 | public async Task UsesLocalNuGetConfigInHierarchy()
330 | {
331 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
332 | var nugetConfig =
333 | """
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 | """;
342 |
343 | var packages =
344 | """
345 |
346 |
347 |
348 |
349 |
350 | """;
351 |
352 | using var directory = new TempDirectory();
353 | var nugetConfigPath = Path.Combine(directory, "nuget.config");
354 | var directoryPath = Path.Combine(directory, "Directory.Packages.props");
355 |
356 | await File.WriteAllTextAsync(nugetConfigPath, nugetConfig);
357 | await File.WriteAllTextAsync(directoryPath, packages);
358 |
359 | await Updater.Update(cache, directoryPath, null);
360 |
361 | var result = await File.ReadAllTextAsync(directoryPath);
362 |
363 | // Verify the package was updated using the local config merged with hierarchy
364 | Assert.DoesNotContain("Version=\"12.0.1\"", result);
365 | }
366 |
367 | [Fact]
368 | public async Task GetLatestVersion_IgnoresUnlistedPackages()
369 | {
370 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
371 | // YoloDev.Expecto.TestSdk v1.0.0 is unlisted
372 | // Start from a version before 1.0.0 to see if it skips the unlisted version
373 | var currentVersion = NuGetVersion.Parse("0.1.0");
374 |
375 | var result = await Updater.GetLatestVersion(
376 | "YoloDev.Expecto.TestSdk",
377 | currentVersion,
378 | sources,
379 | cache);
380 |
381 | // Should find a listed version, skipping 1.0.0 (unlisted)
382 | Assert.NotNull(result);
383 | Assert.True(result.IsListed, "Returned package version should be listed");
384 | Assert.NotEqual("1.0.0", result.Identity.Version.ToString());
385 | Assert.True(result.Identity.Version > currentVersion);
386 | }
387 |
388 | [Fact]
389 | public async Task UpdateSkipsUnlistedVersions()
390 | {
391 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
392 | var nugetConfig =
393 | """
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 | """;
402 |
403 | var packages =
404 | """
405 |
406 |
407 |
408 |
409 |
410 | """;
411 |
412 | using var directory = new TempDirectory();
413 | var nugetConfigPath = Path.Combine(directory, "nuget.config");
414 | var packagesPath = Path.Combine(directory, "Directory.Packages.props");
415 |
416 | await File.WriteAllTextAsync(nugetConfigPath, nugetConfig);
417 | await File.WriteAllTextAsync(packagesPath, packages);
418 |
419 | await Updater.Update(cache, packagesPath, null);
420 |
421 | var result = await File.ReadAllTextAsync(packagesPath);
422 | var doc = XDocument.Parse(result);
423 |
424 | var packageVersion = doc.Descendants("PackageVersion")
425 | .FirstOrDefault(e => e.Attribute("Include")?.Value == "YoloDev.Expecto.TestSdk");
426 |
427 | Assert.NotNull(packageVersion);
428 |
429 | var versionAttr = packageVersion.Attribute("Version")?.Value;
430 |
431 | // Should have updated, but NOT to the unlisted 1.0.0
432 | Assert.NotEqual("0.1.0", versionAttr);
433 | Assert.NotEqual("1.0.0", versionAttr);
434 |
435 | Assert.True(NuGetVersion.TryParse(versionAttr, out var updatedVersion));
436 | Assert.True(updatedVersion > NuGetVersion.Parse("0.1.0"));
437 | }
438 |
439 | [Fact]
440 | public async Task UpdateRespectsPinnedPackages()
441 | {
442 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
443 | var nugetConfig =
444 | """
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 | """;
453 |
454 | var packages =
455 | """
456 |
457 |
458 |
459 |
460 |
461 |
462 | """;
463 |
464 | using var directory = new TempDirectory();
465 | var nugetConfigPath = Path.Combine(directory, "nuget.config");
466 | var packagesPath = Path.Combine(directory, "Directory.Packages.props");
467 |
468 | await File.WriteAllTextAsync(nugetConfigPath, nugetConfig);
469 | await File.WriteAllTextAsync(packagesPath, packages);
470 |
471 | await Updater.Update(cache, packagesPath, null);
472 |
473 | var result = await File.ReadAllTextAsync(packagesPath);
474 |
475 | // Pinned package should not be updated
476 | Assert.Contains("Newtonsoft.Json\" Version=\"12.0.1\"", result);
477 | Assert.Contains("Pinned=\"true\"", result);
478 |
479 | // Non-pinned package should be updated
480 | Assert.DoesNotContain("NUnit\" Version=\"3.13.0\"", result);
481 | }
482 |
483 | [Fact]
484 | public async Task UpdateAllPackagesArePinned()
485 | {
486 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
487 | var packages =
488 | """
489 |
490 |
491 |
492 |
493 |
494 |
495 | """;
496 |
497 | using var tempFile = await TempFile.CreateText(packages);
498 |
499 | await Updater.Update(cache, tempFile.Path, null);
500 |
501 | var result = await File.ReadAllTextAsync(tempFile.Path);
502 |
503 | // All packages should remain unchanged
504 | Assert.Contains("Newtonsoft.Json\" Version=\"12.0.1\"", result);
505 | Assert.Contains("NUnit\" Version=\"3.13.0\"", result);
506 | Assert.Contains("Pinned=\"true\"", result);
507 | }
508 |
509 | [Fact]
510 | public async Task UpdateSinglePinnedPackage()
511 | {
512 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
513 | var packages =
514 | """
515 |
516 |
517 |
518 |
519 |
520 |
521 | """;
522 |
523 | using var tempFile = await TempFile.CreateText(packages);
524 |
525 | // Try to update the pinned package specifically
526 | await Updater.Update(cache, tempFile.Path, "Newtonsoft.Json");
527 |
528 | var result = await File.ReadAllTextAsync(tempFile.Path);
529 |
530 | // Should still be pinned and not updated
531 | Assert.Contains("Newtonsoft.Json\" Version=\"12.0.1\"", result);
532 | Assert.Contains("Pinned=\"true\"", result);
533 | }
534 |
535 | [Fact]
536 | public async Task UpdatePreservesPinAttributeFormat()
537 | {
538 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
539 | var packages =
540 | """
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 | """;
549 |
550 | using var tempFile = await TempFile.CreateText(packages);
551 |
552 | await Updater.Update(cache, tempFile.Path, null);
553 |
554 | var result = await File.ReadAllTextAsync(tempFile.Path);
555 |
556 | // Verify comment is preserved
557 | Assert.Contains("", result);
558 |
559 | // Verify pinned package wasn't updated
560 | Assert.Contains("System.ValueTuple\" Version=\"4.5.0\"", result);
561 | Assert.Contains("Pinned=\"true\"", result);
562 | }
563 |
564 | [Fact]
565 | public async Task UpdateOnlyUnpinnedPackagesUpdated()
566 | {
567 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
568 | var nugetConfig =
569 | """
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 | """;
578 |
579 | var directoryPackages =
580 | """
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 | """;
589 |
590 | using var directory = new TempDirectory();
591 | var nugetConfigPath = Path.Combine(directory, "nuget.config");
592 | var packagesPath = Path.Combine(directory, "Directory.Packages.props");
593 |
594 | await File.WriteAllTextAsync(nugetConfigPath, nugetConfig);
595 | await File.WriteAllTextAsync(packagesPath, directoryPackages);
596 |
597 | await Updater.Update(cache, packagesPath, null);
598 |
599 | var result = await File.ReadAllTextAsync(packagesPath);
600 | var doc = XDocument.Parse(result);
601 |
602 | var packages = doc.Descendants("PackageVersion")
603 | .Select(element => new
604 | {
605 | Id = element.Attribute("Include")?.Value,
606 | Version = element.Attribute("Version")?.Value,
607 | Pinned = element.Attribute("Pinned")?.Value
608 | })
609 | .ToList();
610 |
611 | // Pinned packages unchanged
612 | var newtonsoft = packages.First(_ => _.Id == "Newtonsoft.Json");
613 | Assert.Equal("12.0.1", newtonsoft.Version);
614 | Assert.Equal("true", newtonsoft.Pinned);
615 |
616 | var nunit = packages.First(_ => _.Id == "NUnit");
617 | Assert.Equal("3.13.0", nunit.Version);
618 | Assert.Equal("true", nunit.Pinned);
619 |
620 | // Unpinned package updated
621 | var xunit = packages.First(_ => _.Id == "xunit");
622 | Assert.NotEqual("2.4.0", xunit.Version);
623 | Assert.Null(xunit.Pinned);
624 | }
625 |
626 | [Fact]
627 | public async Task UpdatePreservesOriginalNewlineStyle()
628 | {
629 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
630 | var content = "\n \n \n \n\n";
631 |
632 | using var tempFile = await TempFile.CreateText(content);
633 |
634 | // Read the original file to verify it has \n newlines
635 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
636 | var originalText = Encoding.UTF8.GetString(originalBytes);
637 |
638 | // Verify original has Unix newlines (\n) and not Windows newlines (\r\n)
639 | Assert.Contains("\n", originalText);
640 | Assert.DoesNotContain("\r\n", originalText);
641 |
642 | await Updater.Update(cache, tempFile.Path, null);
643 |
644 | // Read the result
645 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
646 | var resultText = Encoding.UTF8.GetString(resultBytes);
647 |
648 | // Verify newline style is preserved (should still be Unix \n, not Windows \r\n)
649 | Assert.Contains("\n", resultText);
650 | Assert.DoesNotContain("\r\n", resultText);
651 | }
652 |
653 | [Fact]
654 | public async Task UpdatePreservesWindowsNewlineStyle()
655 | {
656 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
657 | var content = "\r\n \r\n \r\n \r\n\r\n";
658 |
659 | using var tempFile = await TempFile.CreateText(content);
660 |
661 | // Read the original file to verify it has \r\n newlines
662 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
663 | var originalText = Encoding.UTF8.GetString(originalBytes);
664 |
665 | // Verify original has Windows newlines (\r\n)
666 | Assert.Contains("\r\n", originalText);
667 |
668 | await Updater.Update(cache, tempFile.Path, null);
669 |
670 | // Read the result
671 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
672 | var resultText = Encoding.UTF8.GetString(resultBytes);
673 |
674 | // Verify newline style is preserved (should still be Windows \r\n)
675 | Assert.Contains("\r\n", resultText);
676 | }
677 |
678 | [Fact]
679 | public async Task UpdatePreservesTrailingNewline()
680 | {
681 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
682 | // Content WITH trailing newline
683 | var content = "\n \n \n \n\n";
684 |
685 | using var tempFile = await TempFile.CreateText(content);
686 |
687 | // Verify original ends with newline
688 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
689 | Assert.Equal((byte)'\n', originalBytes[^1]);
690 |
691 | await Updater.Update(cache, tempFile.Path, null);
692 |
693 | // Verify result still ends with newline
694 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
695 | Assert.Equal((byte)'\n', resultBytes[^1]);
696 | }
697 |
698 | [Fact]
699 | public async Task UpdatePreservesNoTrailingNewline()
700 | {
701 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
702 | // Content WITHOUT trailing newline
703 | var content = "\n \n \n \n";
704 |
705 | using var tempFile = await TempFile.CreateText(content);
706 |
707 | // Verify original does NOT end with newline
708 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
709 | Assert.NotEqual((byte)'\n', originalBytes[^1]);
710 | Assert.NotEqual((byte)'\r', originalBytes[^1]);
711 |
712 | await Updater.Update(cache, tempFile.Path, null);
713 |
714 | // Verify result still does NOT end with newline
715 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
716 | Assert.NotEqual((byte)'\n', resultBytes[^1]);
717 | Assert.NotEqual((byte)'\r', resultBytes[^1]);
718 | }
719 |
720 | [Fact]
721 | public async Task UpdatePreservesTrailingCRLF()
722 | {
723 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
724 | // Content WITH trailing CRLF
725 | var content = "\r\n \r\n \r\n \r\n\r\n";
726 |
727 | using var tempFile = await TempFile.CreateText(content);
728 |
729 | // Verify original ends with \r\n
730 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
731 | Assert.Equal((byte)'\n', originalBytes[^1]);
732 | Assert.Equal((byte)'\r', originalBytes[^2]);
733 |
734 | await Updater.Update(cache, tempFile.Path, null);
735 |
736 | // Verify result still ends with \r\n
737 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
738 | Assert.Equal((byte)'\n', resultBytes[^1]);
739 | Assert.Equal((byte)'\r', resultBytes[^2]);
740 | }
741 |
742 | [Fact]
743 | public async Task UpdatePreservesNoTrailingNewlineWithCRLF()
744 | {
745 | using var cache = new SourceCacheContext { RefreshMemoryCache = true };
746 | // Content with CRLF style but WITHOUT trailing newline
747 | var content = "\r\n \r\n \r\n \r\n";
748 |
749 | using var tempFile = await TempFile.CreateText(content);
750 |
751 | // Verify original does NOT end with newline
752 | var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
753 | Assert.NotEqual((byte)'\n', originalBytes[^1]);
754 | Assert.NotEqual((byte)'\r', originalBytes[^1]);
755 |
756 | await Updater.Update(cache, tempFile.Path, null);
757 |
758 | // Verify result still does NOT end with newline
759 | var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
760 | Assert.NotEqual((byte)'\n', resultBytes[^1]);
761 | Assert.NotEqual((byte)'\r', resultBytes[^1]);
762 | }
763 | }
--------------------------------------------------------------------------------