Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,51 @@ The next time you run the updater, it will update to the latest version.
- Comments and formatting around pinned packages are preserved during updates


## Version Overrides


### Overview

Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`.

PackageUpdate keeps these overrides up to date, independently of the central version. Each override is updated using the same rule applied to central versions:

- A stable override is only moved to a newer stable version.
- A pre-release override also considers pre-release versions.


### Example

A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**.

`Directory.Packages.props` stays on the stable:

```xml
<PackageVersion Include="Verify.XunitV3" Version="31.25.0" />
```

`Tests/Tests.csproj` tracks the pre-release:

```xml
<PackageReference Include="Verify.XunitV3" VersionOverride="32.0.0-beta.1" />
```

When a newer pre-release is published, only the override in the test project is advanced; the central stable entry is left untouched.

Overrides that point at a stable version work the same way, and are moved to newer stable versions:

```xml
<PackageReference Include="Newtonsoft.Json" VersionOverride="13.0.1" />
```


### Behavior

- `VersionOverride` entries are updated in all `*.csproj` files under each solution directory.
- A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating.
- The `--package` filter also applies to override updates.


## Automatic Package Migration


Expand Down
45 changes: 45 additions & 0 deletions readme.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,51 @@ The next time you run the updater, it will update to the latest version.
- Comments and formatting around pinned packages are preserved during updates


## Version Overrides


### Overview

Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`.

PackageUpdate keeps these overrides up to date, independently of the central version. Each override is updated using the same rule applied to central versions:

- A stable override is only moved to a newer stable version.
- A pre-release override also considers pre-release versions.


### Example

A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**.

`Directory.Packages.props` stays on the stable:

```xml
<PackageVersion Include="Verify.XunitV3" Version="31.25.0" />
```

`Tests/Tests.csproj` tracks the pre-release:

```xml
<PackageReference Include="Verify.XunitV3" VersionOverride="32.0.0-beta.1" />
```

When a newer pre-release is published, only the override in the test project is advanced; the central stable entry is left untouched.

Overrides that point at a stable version work the same way, and are moved to newer stable versions:

```xml
<PackageReference Include="Newtonsoft.Json" VersionOverride="13.0.1" />
```


### Behavior

- `VersionOverride` entries are updated in all `*.csproj` files under each solution directory.
- A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating.
- The `--package` filter also applies to override updates.


## Automatic Package Migration


Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

<Project>
<PropertyGroup>
<Version>4.2.0</Version>
<Version>4.3.0</Version>
<LangVersion>preview</LangVersion>
<NoWarn>NU1608</NoWarn>
<AssemblyVersion>1.0.0</AssemblyVersion>
Expand Down
1 change: 1 addition & 0 deletions src/PackageUpdate/ProcessOutputReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static async Task<List<string>> ReadLines(this Process process)
{
continue;
}

list.Add(line);
}

Expand Down
109 changes: 88 additions & 21 deletions src/PackageUpdate/Updater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ public static async Task Update(
Log.Information("Updated {Package}: {NuGetVersion} -> {LatestVersion}", package.Package, currentVersion, latestVersion);
}

await SaveXml(directoryPackagesPropsPath, xml, newLine, hasTrailingNewline);

// Update PackageReference entries in csproj files for migrated packages
if (migrations.Count > 0)
{
await UpdateCsprojFiles(directory, migrations);
}

// Update VersionOverride entries in csproj files (e.g. test projects tracking a pre-release)
await UpdateVersionOverrides(directory, packageName, sources, cache);
}

static async Task SaveXml(string path, XDocument xml, string newLine, bool hasTrailingNewline)
{
var xmlSettings = new XmlWriterSettings
{
OmitXmlDeclaration = true,
Expand All @@ -120,21 +134,15 @@ public static async Task Update(
Async = true
};

await using (var writer = XmlWriter.Create(directoryPackagesPropsPath, xmlSettings))
await using (var writer = XmlWriter.Create(path, xmlSettings))
{
await xml.SaveAsync(writer, Cancel.None);
}

// Match the original trailing newline convention
if (hasTrailingNewline)
{
await File.AppendAllTextAsync(directoryPackagesPropsPath, newLine);
}

// Update PackageReference entries in csproj files for migrated packages
if (migrations.Count > 0)
{
await UpdateCsprojFiles(directory, migrations);
await File.AppendAllTextAsync(path, newLine);
}
}

Expand Down Expand Up @@ -362,25 +370,84 @@ static async Task UpdateCsprojFiles(string directory, List<(string OldPackage, s

if (updated)
{
var xmlSettings = new XmlWriterSettings
await SaveXml(csprojPath, csprojXml, newLine, hasTrailingNewline);
}
}
}

static async Task UpdateVersionOverrides(
string directory,
string? packageName,
List<PackageSource> sources,
SourceCacheContext cache)
{
foreach (var csprojPath in EnumerateCsprojFiles(directory))
{
var (newLine, hasTrailingNewline) = DetectNewLineInfo(csprojPath);
var csprojXml = XDocument.Load(csprojPath);

var overrides = csprojXml.Descendants("PackageReference")
.Select(element => new
{
Element = element,
Package = element.Attribute("Include")?.Value,
CurrentOverride = element.Attribute("VersionOverride")?.Value,
Pinned = element.Attribute("Pinned")?.Value == "true"
})
.Where(_ => _.Package != null &&
_.CurrentOverride != null &&
!_.Pinned)
.ToList();

var updated = false;

foreach (var packageOverride in overrides)
{
// Filter to specific package if requested
if (!string.IsNullOrEmpty(packageName) &&
!string.Equals(packageOverride.Package, packageName, StringComparison.OrdinalIgnoreCase))
{
continue;
}

if (!NuGetVersion.TryParse(packageOverride.CurrentOverride, out var currentVersion))
{
OmitXmlDeclaration = true,
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
Indent = true,
IndentChars = " ",
NewLineChars = newLine,
Async = true
};

await using (var writer = XmlWriter.Create(csprojPath, xmlSettings))
continue;
}

// An override follows the same rule as a central version: a stable override only moves
// to a newer stable, a pre-release override also considers pre-releases
var latestMetadata = await GetLatestVersion(
packageOverride.Package!,
currentVersion,
sources,
cache);

if (latestMetadata == null)
{
await csprojXml.SaveAsync(writer, Cancel.None);
continue;
}

if (hasTrailingNewline)
var latestVersion = latestMetadata.Identity.Version;

if (latestVersion <= currentVersion)
{
await File.AppendAllTextAsync(csprojPath, newLine);
continue;
}

packageOverride.Element.SetAttributeValue("VersionOverride", latestVersion.ToString());
updated = true;
Log.Information(
"Updated {Package} VersionOverride: {NuGetVersion} -> {LatestVersion} in {File}",
packageOverride.Package,
currentVersion,
latestVersion,
Path.GetFileName(csprojPath));
}

if (updated)
{
await SaveXml(csprojPath, csprojXml, newLine, hasTrailingNewline);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/PackageUpdate/nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ If no directory is passed the current directory will be used.

* Updates all packages across all solutions in a directory
* Respects `Pinned="true"` attribute to skip specific packages
* Updates `VersionOverride` entries in projects independently of the central version
* Automatically migrates deprecated packages to recommended alternatives
* Preserves file formatting (newlines, indentation)
* Queries all configured NuGet sources
Expand Down
Loading
Loading