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
2 changes: 1 addition & 1 deletion src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ root = true
[*]
indent_style = space
end_of_line = lf
insert_final_newline = false
insert_final_newline = true

[*.cs]
indent_size = 4
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.0.4</Version>
<Version>4.0.5</Version>
<LangVersion>preview</LangVersion>
<NoWarn>NU1608</NoWarn>
<AssemblyVersion>1.0.0</AssemblyVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageVersion Include="MarkdownSnippets.MsBuild" Version="28.0.0-beta.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="NuGet.Protocol" Version="7.0.1" />
<PackageVersion Include="ProjectDefaults" Version="1.0.166" />
<PackageVersion Include="ProjectDefaults" Version="1.0.167" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
Expand Down
2 changes: 1 addition & 1 deletion src/PackageUpdate/DotnetStarter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static async Task<List<string>> StartDotNet(string arguments, string directory)
process.Start();
Log.Information(" dotnet {Arguments}", arguments);

if (!process.WaitForExit(300000))
if (!process.WaitForExit(60000))
{
process.Kill(true);
throw new(
Expand Down
56 changes: 37 additions & 19 deletions src/PackageUpdate/Updater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ public static async Task Update(
{
var directory = Path.GetDirectoryName(directoryPackagesPropsPath)!;

// Detect the original newline style
var newLine = DetectNewLine(directoryPackagesPropsPath);
// Detect the original newline style and trailing newline
var (newLine, hasTrailingNewline) = DetectNewLineInfo(directoryPackagesPropsPath);

// Load the XML document
var xml = XDocument.Load(directoryPackagesPropsPath);
Expand Down Expand Up @@ -84,36 +84,54 @@ public static async Task Update(
Async = true
};

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

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

static string DetectNewLine(string filePath)
static (string newLine, bool hasTrailingNewline) DetectNewLineInfo(string filePath)
{
// Read a portion of the file to detect newline style
using var reader = new StreamReader(filePath);
var buffer = new char[4096];
var charsRead = reader.Read(buffer, 0, buffer.Length);
var bytes = File.ReadAllBytes(filePath);
var newLine = Environment.NewLine;
var hasTrailingNewline = false;

for (var i = 0; i < charsRead; i++)
// Detect newline style from first occurrence
for (var i = 0; i < bytes.Length; i++)
{
if (buffer[i] == '\r')
if (bytes[i] == '\r')
{
// Check if it's CRLF or just CR
if (i + 1 < charsRead && buffer[i + 1] == '\n')
if (i + 1 < bytes.Length && bytes[i + 1] == '\n')
{
return "\r\n"; // Windows-style
newLine = "\r\n";
}
return "\r"; // Old Mac-style
else
{
newLine = "\r";
}
break;
}
if (buffer[i] == '\n')
if (bytes[i] == '\n')
{
return "\n"; // Unix-style
newLine = "\n";
break;
}
}

// Default to environment newline if no newlines found
return Environment.NewLine;
// Detect trailing newline
if (bytes.Length > 0)
{
var lastByte = bytes[^1];
hasTrailingNewline = lastByte == '\n' || lastByte == '\r';
}

return (newLine, hasTrailingNewline);
}

public static async Task<IPackageSearchMetadata?> GetLatestVersion(
Expand Down
86 changes: 86 additions & 0 deletions src/Tests/UpdaterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -674,4 +674,90 @@ public async Task UpdatePreservesWindowsNewlineStyle()
// Verify newline style is preserved (should still be Windows \r\n)
Assert.Contains("\r\n", resultText);
}

[Fact]
public async Task UpdatePreservesTrailingNewline()
{
using var cache = new SourceCacheContext { RefreshMemoryCache = true };
// Content WITH trailing newline
var content = "<Project>\n <ItemGroup>\n <PackageVersion Include=\"System.ValueTuple\" Version=\"4.5.0\" Pinned=\"true\" />\n </ItemGroup>\n</Project>\n";

using var tempFile = await TempFile.CreateText(content);

// Verify original ends with newline
var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.Equal((byte)'\n', originalBytes[^1]);

await Updater.Update(cache, tempFile.Path, null);

// Verify result still ends with newline
var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.Equal((byte)'\n', resultBytes[^1]);
}

[Fact]
public async Task UpdatePreservesNoTrailingNewline()
{
using var cache = new SourceCacheContext { RefreshMemoryCache = true };
// Content WITHOUT trailing newline
var content = "<Project>\n <ItemGroup>\n <PackageVersion Include=\"System.ValueTuple\" Version=\"4.5.0\" Pinned=\"true\" />\n </ItemGroup>\n</Project>";

using var tempFile = await TempFile.CreateText(content);

// Verify original does NOT end with newline
var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.NotEqual((byte)'\n', originalBytes[^1]);
Assert.NotEqual((byte)'\r', originalBytes[^1]);

await Updater.Update(cache, tempFile.Path, null);

// Verify result still does NOT end with newline
var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.NotEqual((byte)'\n', resultBytes[^1]);
Assert.NotEqual((byte)'\r', resultBytes[^1]);
}

[Fact]
public async Task UpdatePreservesTrailingCRLF()
{
using var cache = new SourceCacheContext { RefreshMemoryCache = true };
// Content WITH trailing CRLF
var content = "<Project>\r\n <ItemGroup>\r\n <PackageVersion Include=\"System.ValueTuple\" Version=\"4.5.0\" Pinned=\"true\" />\r\n </ItemGroup>\r\n</Project>\r\n";

using var tempFile = await TempFile.CreateText(content);

// Verify original ends with \r\n
var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.Equal((byte)'\n', originalBytes[^1]);
Assert.Equal((byte)'\r', originalBytes[^2]);

await Updater.Update(cache, tempFile.Path, null);

// Verify result still ends with \r\n
var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.Equal((byte)'\n', resultBytes[^1]);
Assert.Equal((byte)'\r', resultBytes[^2]);
}

[Fact]
public async Task UpdatePreservesNoTrailingNewlineWithCRLF()
{
using var cache = new SourceCacheContext { RefreshMemoryCache = true };
// Content with CRLF style but WITHOUT trailing newline
var content = "<Project>\r\n <ItemGroup>\r\n <PackageVersion Include=\"System.ValueTuple\" Version=\"4.5.0\" Pinned=\"true\" />\r\n </ItemGroup>\r\n</Project>";

using var tempFile = await TempFile.CreateText(content);

// Verify original does NOT end with newline
var originalBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.NotEqual((byte)'\n', originalBytes[^1]);
Assert.NotEqual((byte)'\r', originalBytes[^1]);

await Updater.Update(cache, tempFile.Path, null);

// Verify result still does NOT end with newline
var resultBytes = await File.ReadAllBytesAsync(tempFile.Path);
Assert.NotEqual((byte)'\n', resultBytes[^1]);
Assert.NotEqual((byte)'\r', resultBytes[^1]);
}
}