├── .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 | [![Build status](https://ci.appveyor.com/api/projects/status/sq3dnh0uyl7sf9uv/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/PackageUpdate) 4 | [![NuGet Status](https://img.shields.io/nuget/v/PackageUpdate.svg)](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 | [![Build status](https://ci.appveyor.com/api/projects/status/sq3dnh0uyl7sf9uv/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/PackageUpdate) 11 | [![NuGet Status](https://img.shields.io/nuget/v/PackageUpdate.svg)](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>&lt;profile version="1.0"&gt; 130 | &lt;option name="myName" value="c# Cleanup" /&gt; 131 | &lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; 132 | &lt;Language id="EditorConfig"&gt; 133 | &lt;Reformat&gt;false&lt;/Reformat&gt; 134 | &lt;/Language&gt; 135 | &lt;Language id="HTML"&gt; 136 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 137 | &lt;Reformat&gt;false&lt;/Reformat&gt; 138 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 139 | &lt;/Language&gt; 140 | &lt;Language id="JSON"&gt; 141 | &lt;Reformat&gt;false&lt;/Reformat&gt; 142 | &lt;/Language&gt; 143 | &lt;Language id="RELAX-NG"&gt; 144 | &lt;Reformat&gt;false&lt;/Reformat&gt; 145 | &lt;/Language&gt; 146 | &lt;Language id="XML"&gt; 147 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 148 | &lt;Reformat&gt;false&lt;/Reformat&gt; 149 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 150 | &lt;/Language&gt; 151 | &lt;/profile&gt;</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 | } --------------------------------------------------------------------------------