/** * @file Parses git's merge-conflict markers out of a file's lines, and writes * the file back with one side (or both) chosen per block — the model behind * the Changes panel's conflict view. Handles diff3 style (`|||||||` base * section, dropped: it is neither side). Pure, no React. * @author Nguyễn Ngọc Trí Vĩ */ export type ConflictChoice = "ours" | "theirs" | "both"; export type ConflictPart = | { kind: "text"; lines: string[] } | { kind: "conflict"; ours: string[]; theirs: string[]; oursLabel: string; theirsLabel: string }; const OPEN = /^<{7}(?: (.*))?$/; const BASE = /^\|{7}(?: .*)?$/; const MID = /^={7}$/; const CLOSE = /^>{7}(?: (.*))?$/; /** * The file as text runs and conflict blocks, or null when its markers do not * balance (a hand edit left half a block) — the caller falls back to the * plain editor then rather than guess. */ export function parseConflicts(lines: string[]): ConflictPart[] | null { const parts: ConflictPart[] = []; let text: string[] = []; let block: { ours: string[]; theirs: string[]; oursLabel: string } | null = null; let section: "ours" | "base" | "theirs" = "ours"; for (const line of lines) { if (!block) { const open = line.match(OPEN); if (open) { if (text.length) parts.push({ kind: "text", lines: text }); text = []; block = { ours: [], theirs: [], oursLabel: open[1] ?? "" }; section = "ours"; } else if (CLOSE.test(line) || MID.test(line)) { return null; } else { text.push(line); } continue; } if (section === "ours" && BASE.test(line)) section = "base"; else if (section !== "theirs" && MID.test(line)) section = "theirs"; else if (section === "theirs" && CLOSE.test(line)) { const close = line.match(CLOSE)!; parts.push({ kind: "conflict", ...block, theirsLabel: close[1] ?? "" }); block = null; } else if (OPEN.test(line)) return null; else if (section === "ours") block.ours.push(line); else if (section === "theirs") block.theirs.push(line); } if (block) return null; if (text.length || !parts.length) parts.push({ kind: "text", lines: text }); return parts; } /** The file's lines with block `i` replaced by `choices[i]`; an unchosen block keeps its markers. */ export function applyChoices( parts: ConflictPart[], choices: (ConflictChoice | undefined)[] ): string[] { const out: string[] = []; let i = 0; for (const p of parts) { if (p.kind === "text") { out.push(...p.lines); continue; } const c = choices[i++]; if (c === "ours") out.push(...p.ours); else if (c === "theirs") out.push(...p.theirs); else if (c === "both") out.push(...p.ours, ...p.theirs); else out.push( `<<<<<<< ${p.oursLabel}`.trimEnd(), ...p.ours, "=======", ...p.theirs, `>>>>>>> ${p.theirsLabel}`.trimEnd() ); } return out; }