From ef9c066dee5e0ca9e54f2cd2373ae7d18685f23b Mon Sep 17 00:00:00 2001 From: Gadfly Date: Fri, 13 Mar 2026 11:01:26 +0800 Subject: [PATCH] fix: correct patch generation when discarding partial changes of staged new files When a new file was already staged and the user tried to discard selected lines from the unstaged changes list, the generated patch was malformed due to incorrect index header format, wrong hunk line numbers, and unconditional "No newline at end of file" marker. --- src/Models/DiffResult.cs | 72 ++++++++++++++++----------------- src/Views/TextDiffView.axaml.cs | 20 +++------ 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 6c381df35..d00359888 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -95,53 +95,45 @@ public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombin return rs; } - public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) + public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, string output) { var isTracked = !string.IsNullOrEmpty(fileBlobGuid); var fileGuid = isTracked ? fileBlobGuid : "00000000"; + // Collect selected added lines first to check if there's anything to write + var selectedAddedLines = new List(); + for (int i = 0; i < Lines.Count; i++) + { + var line = Lines[i]; + if (line.Type != TextDiffLineType.Added) + continue; + + var lineIndex = i + 1; // 1-based line number + if (lineIndex >= selection.StartLine && lineIndex <= selection.EndLine) + selectedAddedLines.Add(line); + } + + if (selectedAddedLines.Count == 0) + return; + using var writer = new StreamWriter(output); writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); - if (!revert && !isTracked) + if (!isTracked) writer.WriteLine("new file mode 100644"); - writer.WriteLine($"index 00000000...{fileGuid}"); - writer.WriteLine($"--- {(revert || isTracked ? $"a/{change.Path}" : "/dev/null")}"); + writer.WriteLine($"index 0000000..{fileGuid}"); + writer.WriteLine($"--- {(isTracked ? $"a/{change.Path}" : "/dev/null")}"); writer.WriteLine($"+++ b/{change.Path}"); - var additions = selection.EndLine - selection.StartLine; - if (selection.StartLine != 1) - additions++; - - if (revert) + writer.WriteLine($"@@ -0,0 +1,{selectedAddedLines.Count} @@"); + foreach (var line in selectedAddedLines) { - var totalLines = Lines.Count - 1; - writer.WriteLine($"@@ -0,{totalLines - additions} +0,{totalLines} @@"); - for (int i = 1; i <= totalLines; i++) - { - var line = Lines[i]; - if (line.Type != TextDiffLineType.Added) - continue; - - if (i >= selection.StartLine - 1 && i < selection.EndLine) - writer.WriteLine($"+{line.Content}"); - else - writer.WriteLine($" {line.Content}"); - } - } - else - { - writer.WriteLine($"@@ -0,0 +0,{additions} @@"); - for (int i = selection.StartLine - 1; i < selection.EndLine; i++) - { - var line = Lines[i]; - if (line.Type != TextDiffLineType.Added) - continue; - writer.WriteLine($"+{line.Content}"); - } + writer.WriteLine($"+{line.Content}"); } - writer.WriteLine("\\ No newline at end of file"); + if (selectedAddedLines[^1].NoNewLineEndOfFile) + writer.WriteLine("\\ No newline at end of file"); + writer.Flush(); } @@ -152,7 +144,7 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD using var writer = new StreamWriter(output); writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); - writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); + writer.WriteLine($"index 0000000..{fileTreeGuid} 100644"); writer.WriteLine($"--- a/{orgFile}"); writer.WriteLine($"+++ b/{change.Path}"); @@ -255,7 +247,9 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD } } - writer.WriteLine($" {tail}"); + if (tail != null) + writer.WriteLine($" {tail}"); + writer.Flush(); } @@ -266,7 +260,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG using var writer = new StreamWriter(output); writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); - writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); + writer.WriteLine($"index 0000000..{fileTreeGuid} 100644"); writer.WriteLine($"--- a/{orgFile}"); writer.WriteLine($"+++ b/{change.Path}"); @@ -406,7 +400,9 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG } } - writer.WriteLine($" {tail}"); + if (tail != null) + writer.WriteLine($" {tail}"); + writer.Flush(); } diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 516b6e8e9..a6b3e140f 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1495,7 +1495,7 @@ private async void OnStageChunk(object _1, RoutedEventArgs _2) var tmpFile = Path.GetTempFileName(); if (change.WorkTree == Models.ChangeState.Untracked) { - diff.GenerateNewPatchFromSelection(change, null, selection, false, tmpFile); + diff.GenerateNewPatchFromSelection(change, null, selection, tmpFile); } else if (chunk.Combined) { @@ -1532,9 +1532,7 @@ private async void OnUnstageChunk(object _1, RoutedEventArgs _2) var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync(); var tmpFile = Path.GetTempFileName(); - if (change.Index == Models.ChangeState.Added) - diff.GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile); - else if (chunk.Combined) + if (chunk.Combined) diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile); else diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); @@ -1562,20 +1560,12 @@ private async void OnDiscardChunk(object _1, RoutedEventArgs _2) using var lockWatcher = repo.LockWatcher(); var tmpFile = Path.GetTempFileName(); - if (change.Index == Models.ChangeState.Added) - { - diff.GenerateNewPatchFromSelection(change, null, selection, true, tmpFile); - } - else if (chunk.Combined) - { - var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync(); + var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync(); + + if (chunk.Combined) diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile); - } else - { - var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync(); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); - } await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--reverse").ExecAsync(); File.Delete(tmpFile);