1da46f10f4
Opening a conflicted file in the Changes panel now shows each conflict block as its two sides, labelled from git's markers and tinted apart, with Keep mine / Take theirs / Keep both per block (undoable, ^/v to jump). Save and mark resolved writes the chosen text through the whole-file editor's PUT /git/file (baseSha stale check) then runs resolve done. The Merge conflicts group's buttons are text, and a conflicted file is marked with a red ! in the Files tree. Fix: ls-files --cached lists an unmerged path once per stage, so the tree showed a conflicted file three times; lsFiles now dedupes.
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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;
|
|
}
|