using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Avalonia; using Avalonia.Media.Imaging; namespace SourceGit.Models { public enum TextDiffLineType { None, Normal, Indicator, Added, Deleted, } public class TextInlineRange { public int Start { get; set; } public int End { get; set; } public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; } } public class TextDiffLine { public TextDiffLineType Type { get; set; } = TextDiffLineType.None; public string Content { get; set; } = ""; public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; public List Highlights { get; set; } = new List(); public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString(); public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); public TextDiffLine() { } public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine) { Type = type; Content = content; OldLineNumber = oldLine; NewLineNumber = newLine; } } public class TextDiffSelection { public int StartLine { get; set; } = 0; public int EndLine { get; set; } = 0; public bool HasChanges { get; set; } = false; public bool HasLeftChanges { get; set; } = false; public int IgnoredAdds { get; set; } = 0; public int IgnoredDeletes { get; set; } = 0; public bool IsInRange(int idx) { return idx >= StartLine - 1 && idx < EndLine; } } public partial class TextDiff { public string File { get; set; } = string.Empty; public List Lines { get; set; } = new List(); public Vector SyncScrollOffset { get; set; } = Vector.Zero; public int MaxLineNumber = 0; public string Repo { get; set; } = null; public DiffOption Option { get; set; } = null; public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide) { var rs = new TextDiffSelection(); rs.StartLine = startLine; rs.EndLine = endLine; for (int i = 0; i < startLine - 1; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Added) { rs.HasLeftChanges = true; rs.IgnoredAdds++; } else if (line.Type == TextDiffLineType.Deleted) { rs.HasLeftChanges = true; rs.IgnoredDeletes++; } } for (int i = startLine - 1; i < endLine; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Added) { if (isCombined) { rs.HasChanges = true; break; } else if (isOldSide) { rs.HasLeftChanges = true; } else { rs.HasChanges = true; } } else if (line.Type == TextDiffLineType.Deleted) { if (isCombined) { rs.HasChanges = true; break; } else if (isOldSide) { rs.HasChanges = true; } else { rs.HasLeftChanges = true; } } } if (!rs.HasLeftChanges) { for (int i = endLine; i < Lines.Count; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Added || line.Type == TextDiffLineType.Deleted) { rs.HasLeftChanges = true; break; } } } return rs; } public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) { var isTracked = !string.IsNullOrEmpty(fileBlobGuid); var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000"; var builder = new StringBuilder(); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); if (!revert && !isTracked) builder.Append("new file mode 100644\n"); builder.Append("index 00000000...").Append(fileGuid).Append('\n'); builder.Append("--- ").Append((revert || isTracked) ? $"a/{change.Path}\n" : "/dev/null\n"); builder.Append("+++ b/").Append(change.Path).Append('\n'); var additions = selection.EndLine - selection.StartLine; if (selection.StartLine != 1) additions++; if (revert) { var totalLines = Lines.Count - 1; builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@"); for (int i = 1; i <= totalLines; i++) { var line = Lines[i]; if (line.Type != TextDiffLineType.Added) continue; builder.Append(selection.IsInRange(i) ? "\n+" : "\n ").Append(line.Content); } } else { builder.Append("@@ -0,0 +0,").Append(additions).Append(" @@"); for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { var line = Lines[i]; if (line.Type != TextDiffLineType.Added) continue; builder.Append("\n+").Append(line.Content); } } builder.Append("\n\\ No newline at end of file\n"); System.IO.File.WriteAllText(output, builder.ToString()); } public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, string output) { var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; var builder = new StringBuilder(); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); builder.Append("index 00000000...").Append(fileTreeGuid).Append(" 100644\n"); builder.Append("--- a/").Append(orgFile).Append('\n'); builder.Append("+++ b/").Append(change.Path); // If last line of selection is a change. Find one more line. var tail = null as string; if (selection.EndLine < Lines.Count) { var lastLine = Lines[selection.EndLine - 1]; if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) { for (int i = selection.EndLine; i < Lines.Count; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) break; if (revert) { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) { tail = line.Content; break; } } else { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) { tail = line.Content; break; } } } } } // If the first line is not indicator. if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) { var indicator = selection.StartLine - 1; for (int i = selection.StartLine - 2; i >= 0; i--) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { indicator = i; break; } } var ignoreAdds = 0; var ignoreRemoves = 0; for (int i = 0; i < indicator; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Added) { ignoreAdds++; } else if (line.Type == TextDiffLineType.Deleted) { ignoreRemoves++; } } for (int i = indicator; i < selection.StartLine - 1; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, tail != null); } else if (line.Type == TextDiffLineType.Added) { if (revert) builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Deleted) { if (!revert) builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Normal) { builder.Append("\n ").Append(line.Content); } } } // Outputs the selected lines. for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { if (!ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, tail != null)) { break; } } else if (line.Type == TextDiffLineType.Normal) { builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Added) { builder.Append("\n+").Append(line.Content); } else if (line.Type == TextDiffLineType.Deleted) { builder.Append("\n-").Append(line.Content); } } builder.Append("\n ").Append(tail); builder.Append("\n"); System.IO.File.WriteAllText(output, builder.ToString()); } public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeGuid, TextDiffSelection selection, bool revert, bool isOldSide, string output) { var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; var builder = new StringBuilder(); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); builder.Append("index 00000000...").Append(fileTreeGuid).Append(" 100644\n"); builder.Append("--- a/").Append(orgFile).Append('\n'); builder.Append("+++ b/").Append(change.Path); // If last line of selection is a change. Find one more line. var tail = null as string; if (selection.EndLine < Lines.Count) { var lastLine = Lines[selection.EndLine - 1]; if (lastLine.Type == TextDiffLineType.Added || lastLine.Type == TextDiffLineType.Deleted) { for (int i = selection.EndLine; i < Lines.Count; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) break; if (revert) { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Added) { tail = line.Content; break; } } else { if (line.Type == TextDiffLineType.Normal || line.Type == TextDiffLineType.Deleted) { tail = line.Content; break; } } } } } // If the first line is not indicator. if (Lines[selection.StartLine - 1].Type != TextDiffLineType.Indicator) { var indicator = selection.StartLine - 1; for (int i = selection.StartLine - 2; i >= 0; i--) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { indicator = i; break; } } var ignoreAdds = 0; var ignoreRemoves = 0; for (int i = 0; i < indicator; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Added) { ignoreAdds++; } else if (line.Type == TextDiffLineType.Deleted) { ignoreRemoves++; } } for (int i = indicator; i < selection.StartLine - 1; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, revert, isOldSide, tail != null); } else if (line.Type == TextDiffLineType.Added) { if (revert) builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Deleted) { if (!revert) builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Normal) { builder.Append("\n ").Append(line.Content); } } } // Outputs the selected lines. for (int i = selection.StartLine - 1; i < selection.EndLine; i++) { var line = Lines[i]; if (line.Type == TextDiffLineType.Indicator) { if (!ProcessIndicatorForPatchSingleSide(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, revert, isOldSide, tail != null)) { break; } } else if (line.Type == TextDiffLineType.Normal) { builder.Append("\n ").Append(line.Content); } else if (line.Type == TextDiffLineType.Added) { if (isOldSide) { if (revert) { builder.Append("\n ").Append(line.Content); } else { selection.IgnoredAdds++; } } else { builder.Append("\n+").Append(line.Content); } } else if (line.Type == TextDiffLineType.Deleted) { if (isOldSide) { builder.Append("\n-").Append(line.Content); } else { if (!revert) { builder.Append("\n ").Append(line.Content); } else { selection.IgnoredDeletes++; } } } } builder.Append("\n ").Append(tail); builder.Append("\n"); System.IO.File.WriteAllText(output, builder.ToString()); } private bool ProcessIndicatorForPatch(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool tailed) { var match = REG_INDICATOR().Match(indicator.Content); var oldStart = int.Parse(match.Groups[1].Value); var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; var oldCount = 0; var newCount = 0; for (int i = idx + 1; i < end; i++) { var test = Lines[i]; if (test.Type == TextDiffLineType.Indicator) break; if (test.Type == TextDiffLineType.Normal) { oldCount++; newCount++; } else if (test.Type == TextDiffLineType.Added) { if (i < start - 1) { if (revert) { newCount++; oldCount++; } } else { newCount++; } if (i == end - 1 && tailed) { newCount++; oldCount++; } } else if (test.Type == TextDiffLineType.Deleted) { if (i < start - 1) { if (!revert) { newCount++; oldCount++; } } else { oldCount++; } if (i == end - 1 && tailed) { newCount++; oldCount++; } } } if (oldCount == 0 && newCount == 0) return false; builder.Append($"\n@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); return true; } private bool ProcessIndicatorForPatchSingleSide(StringBuilder builder, TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool revert, bool isOldSide, bool tailed) { var match = REG_INDICATOR().Match(indicator.Content); var oldStart = int.Parse(match.Groups[1].Value); var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds; var oldCount = 0; var newCount = 0; for (int i = idx + 1; i < end; i++) { var test = Lines[i]; if (test.Type == TextDiffLineType.Indicator) break; if (test.Type == TextDiffLineType.Normal) { oldCount++; newCount++; } else if (test.Type == TextDiffLineType.Added) { if (i < start - 1) { if (revert) { newCount++; oldCount++; } } else { if (isOldSide) { if (revert) { newCount++; oldCount++; } } else { newCount++; } } if (i == end - 1 && tailed) { newCount++; oldCount++; } } else if (test.Type == TextDiffLineType.Deleted) { if (i < start - 1) { if (!revert) { newCount++; oldCount++; } } else { if (isOldSide) { oldCount++; } else { if (!revert) { newCount++; oldCount++; } } } if (i == end - 1 && tailed) { newCount++; oldCount++; } } } if (oldCount == 0 && newCount == 0) return false; builder.Append($"\n@@ -{oldStart},{oldCount} +{newStart},{newCount} @@"); return true; } [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] private static partial Regex REG_INDICATOR(); } public class LFSDiff { public LFSObject Old { get; set; } = new LFSObject(); public LFSObject New { get; set; } = new LFSObject(); } public class BinaryDiff { public long OldSize { get; set; } = 0; public long NewSize { get; set; } = 0; } public class ImageDiff { public Bitmap Old { get; set; } = null; public Bitmap New { get; set; } = null; public long OldFileSize { get; set; } = 0; public long NewFileSize { get; set; } = 0; public string OldImageSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0"; public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0"; } public class NoOrEOLChange { } public class FileModeDiff { public string Old { get; set; } = string.Empty; public string New { get; set; } = string.Empty; } public class SubmoduleDiff { public RevisionSubmodule Old { get; set; } = null; public RevisionSubmodule New { get; set; } = null; } public class DiffResult { public bool IsBinary { get; set; } = false; public bool IsLFS { get; set; } = false; public string OldMode { get; set; } = string.Empty; public string NewMode { get; set; } = string.Empty; public TextDiff TextDiff { get; set; } = null; public LFSDiff LFSDiff { get; set; } = null; public string FileModeChange => string.IsNullOrEmpty(OldMode) ? string.Empty : $"{OldMode} → {NewMode}"; } }