From 935f4b5c5e7866538cbcced884ca581a4ab1955c Mon Sep 17 00:00:00 2001 From: Shun-ichi Goto Date: Thu, 2 Apr 2026 20:09:56 +0900 Subject: [PATCH] feature: allow partial stage/unstage/discard for non-UTF8 text in diff view Current implementation fails on partial stage/unstage/discard operations for non-UTF8 text because of applying a patch made with broken (replaced) text. This modification allows these operation by preserving the original raw bytes from the output of `git diff`, and use it to create patch file. --- src/Commands/Diff.cs | 46 +++++++++++++++++++--------------------- src/Models/DiffResult.cs | 31 ++++++++++++++++----------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 4d0cc72ac..d2ad23861 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -45,24 +45,23 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa using var proc = new Process(); proc.StartInfo = CreateGitStartInfo(true); proc.Start(); - - var text = await proc.StandardOutput.ReadToEndAsync().ConfigureAwait(false); - + using var ms = new System.IO.MemoryStream(); + await proc.StandardOutput.BaseStream.CopyToAsync(ms, CancellationToken).ConfigureAwait(false); + var bytes = ms.ToArray(); var start = 0; - var end = text.IndexOf('\n', start); - while (end > 0) + while (start < bytes.Length) { - var line = text[start..end]; - ParseLine(line); - - start = end + 1; - end = text.IndexOf('\n', start); + var end = Array.IndexOf(bytes, (byte)'\n', start); + if (end < 0) + end = bytes.Length; + var next = end + 1; + if (start <= end - 1 && bytes[end - 1] == '\r') + end--; + if (!_result.IsBinary) + ParseLine(bytes[start..end]); + start = next; } - - if (start < text.Length) - ParseLine(text[start..]); - - await proc.WaitForExitAsync().ConfigureAwait(false); + await proc.WaitForExitAsync(CancellationToken).ConfigureAwait(false); } catch { @@ -82,10 +81,9 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa return _result; } - private void ParseLine(string line) + private void ParseLine(byte[] lineBytes) { - if (_result.IsBinary) - return; + var line = Encoding.UTF8.GetString(lineBytes); if (line.StartsWith("old mode ", StringComparison.Ordinal)) { @@ -168,7 +166,7 @@ private void ParseLine(string line) _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, lineBytes, 0, 0); _result.TextDiff.Lines.Add(_last); } } @@ -177,7 +175,7 @@ private void ParseLine(string line) if (line.Length == 0) { ProcessInlineHighlights(); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, Array.Empty(), _oldLine, _newLine); _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; @@ -195,7 +193,7 @@ private void ParseLine(string line) } _result.TextDiff.DeletedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, lineBytes[1..], _oldLine, 0); _deleted.Add(_last); _oldLine++; } @@ -209,7 +207,7 @@ private void ParseLine(string line) } _result.TextDiff.AddedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, lineBytes[1..], 0, _newLine); _added.Add(_last); _newLine++; } @@ -221,7 +219,7 @@ private void ParseLine(string line) { _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, lineBytes, 0, 0); _result.TextDiff.Lines.Add(_last); } else @@ -233,7 +231,7 @@ private void ParseLine(string line) return; } - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, lineBytes[1..], _oldLine, _newLine); _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 32fff76ce..6f79192d4 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -23,6 +23,7 @@ public class TextRange(int p, int n) public class TextDiffLine { public TextDiffLineType Type { get; set; } = TextDiffLineType.None; + public byte[] RawContent { get; set; } = []; public string Content { get; set; } = ""; public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; @@ -33,10 +34,13 @@ public class TextDiffLine public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); public TextDiffLine() { } - public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine) + public TextDiffLine(TextDiffLineType type, byte[] rawContent, int oldLine, int newLine) { + if (rawContent == null) + throw new System.ArgumentNullException(nameof(rawContent)); Type = type; - Content = content; + Content = System.Text.Encoding.UTF8.GetString(rawContent); + RawContent = rawContent; OldLineNumber = oldLine; NewLineNumber = newLine; } @@ -158,7 +162,7 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD writer.WriteLine($"+++ b/{change.Path}"); // If last line of selection is a change. Find one more line. - string tail = null; + TextDiffLine tail = null; if (selection.EndLine < Lines.Count) { var lastLine = Lines[selection.EndLine - 1]; @@ -173,7 +177,7 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD (revert && line.Type == TextDiffLineType.Added) || (!revert && line.Type == TextDiffLineType.Deleted)) { - tail = line.Content; + tail = line; break; } } @@ -256,8 +260,8 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD } } - if (!string.IsNullOrEmpty(tail)) - writer.WriteLine($" {tail}"); + if (tail != null) + WriteLine(writer, ' ', tail); writer.Flush(); } @@ -273,7 +277,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG writer.WriteLine($"+++ b/{change.Path}"); // If last line of selection is a change. Find one more line. - string tail = null; + TextDiffLine tail = null; if (selection.EndLine < Lines.Count) { var lastLine = Lines[selection.EndLine - 1]; @@ -288,7 +292,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) { - tail = line.Content; + tail = line; break; } } @@ -296,7 +300,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) { - tail = line.Content; + tail = line; break; } } @@ -408,8 +412,8 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG } } - if (!string.IsNullOrEmpty(tail)) - writer.WriteLine($" {tail}"); + if (tail != null) + WriteLine(writer, ' ', tail); writer.Flush(); } @@ -564,7 +568,10 @@ private bool ProcessIndicatorForPatchSingleSide(StreamWriter writer, TextDiffLin private static void WriteLine(StreamWriter writer, char prefix, TextDiffLine line) { - writer.WriteLine($"{prefix}{line.Content}"); + writer.Write($"{prefix}"); + writer.Flush(); + writer.BaseStream.Write(line.RawContent); // write original bytes + writer.WriteLine(); if (line.NoNewLineEndOfFile) writer.WriteLine("\\ No newline at end of file");