feature: allow partial stage/unstage/discard for non-UTF8 text in diff view (#2260)

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.
This commit is contained in:
Shun-ichi Goto
2026-04-14 15:45:37 +09:00
committed by GitHub
parent 0d5185b13d
commit c5eb024043
2 changed files with 41 additions and 36 deletions

View File

@@ -45,24 +45,23 @@ namespace SourceGit.Commands
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 @@ namespace SourceGit.Commands
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 @@ namespace SourceGit.Commands
_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 @@ namespace SourceGit.Commands
if (line.Length == 0)
{
ProcessInlineHighlights();
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine);
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, Array.Empty<byte>(), _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
@@ -195,7 +193,7 @@ namespace SourceGit.Commands
}
_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 @@ namespace SourceGit.Commands
}
_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 @@ namespace SourceGit.Commands
{
_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 @@ namespace SourceGit.Commands
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++;

View File

@@ -23,6 +23,7 @@ namespace SourceGit.Models
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 @@ namespace SourceGit.Models
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 @@ namespace SourceGit.Models
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 @@ namespace SourceGit.Models
(revert && line.Type == TextDiffLineType.Added) ||
(!revert && line.Type == TextDiffLineType.Deleted))
{
tail = line.Content;
tail = line;
break;
}
}
@@ -256,8 +260,8 @@ namespace SourceGit.Models
}
}
if (!string.IsNullOrEmpty(tail))
writer.WriteLine($" {tail}");
if (tail != null)
WriteLine(writer, ' ', tail);
writer.Flush();
}
@@ -273,7 +277,7 @@ namespace SourceGit.Models
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 @@ namespace SourceGit.Models
{
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added)
{
tail = line.Content;
tail = line;
break;
}
}
@@ -296,7 +300,7 @@ namespace SourceGit.Models
{
if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted)
{
tail = line.Content;
tail = line;
break;
}
}
@@ -408,8 +412,8 @@ namespace SourceGit.Models
}
}
if (!string.IsNullOrEmpty(tail))
writer.WriteLine($" {tail}");
if (tail != null)
WriteLine(writer, ' ', tail);
writer.Flush();
}
@@ -564,7 +568,10 @@ namespace SourceGit.Models
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");