using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClaudeDo.Data.Git; /// /// One piece of a conflicted file: either common ("stable") text both sides agree on, /// or a conflict region holding the two — or, with diff3 markers, three — competing versions. /// public sealed record MergeSegment { public bool IsConflict { get; init; } /// Stable text (verbatim, line endings preserved) when is false. public string Text { get; init; } = ""; /// "Ours" side (the target branch) when is true. public string Ours { get; init; } = ""; /// Merge base, present only when the merge used diff3 conflict style; null otherwise. public string? Base { get; init; } /// "Theirs" side (the incoming branch) when is true. public string Theirs { get; init; } = ""; /// 1-based line number in the file where this segment starts (the marker line for a conflict). public int StartLine { get; init; } = 1; public static MergeSegment Stable(string text, int startLine = 1) => new() { Text = text, StartLine = startLine }; public static MergeSegment Conflict(string ours, string? @base, string theirs, int startLine = 1) => new() { IsConflict = true, Ours = ours, Base = @base, Theirs = theirs, StartLine = startLine }; } /// /// Parses a conflicted file's text into ordered stable / conflict segments and reassembles it. /// Reads git conflict markers verbatim, so a file with no markers yields a single stable /// segment, and reassembling the stable text plus one chosen resolution per conflict /// round-trips the file exactly (line endings included). /// public static class ConflictMarkerParser { private const string OursMarker = "<<<<<<<"; private const string BaseMarker = "|||||||"; private const string SepMarker = "======="; private const string TheirsMarker = ">>>>>>>"; public static IReadOnlyList Parse(string fileText) { var segments = new List(); var lines = SplitKeepLineEndings(fileText); var stable = new StringBuilder(); var stableStartLine = 1; var i = 0; while (i < lines.Count) { if (!IsMarker(lines[i], OursMarker)) { if (stable.Length == 0) stableStartLine = i + 1; stable.Append(lines[i++]); continue; } if (stable.Length > 0) { segments.Add(MergeSegment.Stable(stable.ToString(), stableStartLine)); stable.Clear(); } var conflictStartLine = i + 1; i++; // consume "<<<<<<<" var ours = new StringBuilder(); while (i < lines.Count && !IsMarker(lines[i], BaseMarker) && !IsMarker(lines[i], SepMarker)) ours.Append(lines[i++]); string? @base = null; if (i < lines.Count && IsMarker(lines[i], BaseMarker)) { i++; // consume "|||||||" var baseText = new StringBuilder(); while (i < lines.Count && !IsMarker(lines[i], SepMarker)) baseText.Append(lines[i++]); @base = baseText.ToString(); } if (i < lines.Count && IsMarker(lines[i], SepMarker)) i++; // consume "=======" var theirs = new StringBuilder(); while (i < lines.Count && !IsMarker(lines[i], TheirsMarker)) theirs.Append(lines[i++]); if (i < lines.Count && IsMarker(lines[i], TheirsMarker)) i++; // consume ">>>>>>>" segments.Add(MergeSegment.Conflict(ours.ToString(), @base, theirs.ToString(), conflictStartLine)); } if (stable.Length > 0) segments.Add(MergeSegment.Stable(stable.ToString(), stableStartLine)); return segments; } /// True when the file still contains an opening conflict marker. public static bool HasConflicts(string fileText) => SplitKeepLineEndings(fileText).Any(l => IsMarker(l, OursMarker)); /// /// Reassembles a file from its segments. Stable segments emit their text verbatim; /// each conflict segment emits whatever returns for it. /// public static string Compose( IEnumerable segments, Func resolveConflict) => string.Concat(segments.Select(s => s.IsConflict ? resolveConflict(s) : s.Text)); // A marker line starts with exactly the 7-char marker, then end-of-line or whitespace/label. private static bool IsMarker(string line, string marker) { if (!line.StartsWith(marker, StringComparison.Ordinal)) return false; if (line.Length == marker.Length) return true; return line[marker.Length] is ' ' or '\t' or '\r' or '\n'; } // Splits into physical lines, each retaining its trailing "\n" (and "\r" if present). private static List SplitKeepLineEndings(string s) { var lines = new List(); var i = 0; while (i < s.Length) { var nl = s.IndexOf('\n', i); if (nl < 0) { lines.Add(s[i..]); break; } lines.Add(s[i..(nl + 1)]); i = nl + 1; } return lines; } }