import { diffArrays } from "diff"; type Hunk = { start: number; end: number; lines: string[]; mine: boolean }; /** Base lines `start` to `end` replaced by `lines`, for each run where `next` differs from `base`. */ function hunks(base: string[], next: string[], mine: boolean): Hunk[] { const found: Hunk[] = []; let at = 0; let current: Hunk | null = null; for (const part of diffArrays(base, next)) { if (!part.added && !part.removed) { if (current) found.push(current); current = null; at += part.count!; continue; } current ??= { start: at, end: at, lines: [], mine }; if (part.removed) current.end = at += part.count!; else current.lines.push(...part.value); } if (current) found.push(current); return found; } /** * Both sides' changes to `base` as git merges them: by lines, refusing (with `false`) where they touch or overlap * unless they make the same change. */ export function merge3(base: string, mine: string, theirs: string): string | false { const lines = (text: string) => text.split(/(?<=\n)/).filter((line) => line !== ""); const original = lines(base); const all = [ ...hunks(original, lines(mine), true), ...hunks(original, lines(theirs), false), ].sort((a, b) => a.start - b.start || a.end - b.end); const out: string[] = []; let at = 0; for (let i = 0; i < all.length;) { const group = [all[i++]]; let end = group[0].end; while (i < all.length && all[i].start <= end) { end = Math.max(end, all[i].end); group.push(all[i++]); } const start = group[0].start; const side = (mine: boolean) => { const result: string[] = []; let from = start; for (const hunk of group.filter((hunk) => hunk.mine === mine)) { result.push(...original.slice(from, hunk.start), ...hunk.lines); from = hunk.end; } return [...result, ...original.slice(from, end)]; }; const ours = side(true); if (group.some((hunk) => !hunk.mine) && group.some((hunk) => hunk.mine)) { if (ours.join("") !== side(false).join("")) return false; } out.push(...original.slice(at, start), ...(group.some((h) => h.mine) ? ours : side(false))); at = end; } return [...out, ...original.slice(at)].join(""); }