100 lines · 4.7 KB
import { hasConflictMarkers, splitLines } from "../git/merge3";
import { parseConflictText } from "~/lib/conflict-blocks";
import type { Segment } from "~/lib/conflict-blocks";
import type { ConflictHunk } from "../git/merge3";
import { scanText } from "../security/detectors";

export type CandidateSource = "ours" | "theirs" | "ours_then_theirs" | "theirs_then_ours" | "ai";
export type Candidate = { id: string; source: CandidateSource; label: string; text: string };

function terminate(text: string): string {
  return text === "" || text.endsWith("\n") ? text : `${text}\n`;
}

/** The four deterministic resolutions, deduplicated by text so Clef never sees two identical options. */
export function baseCandidates(hunk: ConflictHunk): Candidate[] {
  const ours = terminate(hunk.ours);
  const theirs = terminate(hunk.theirs);
  const all: Candidate[] = [
    { id: "ours", source: "ours", label: "Keep the head branch side only", text: ours },
    { id: "theirs", source: "theirs", label: "Keep the base branch side only", text: theirs },
    { id: "ours_then_theirs", source: "ours_then_theirs", label: "Head side, then base side", text: ours + theirs },
    { id: "theirs_then_ours", source: "theirs_then_ours", label: "Base side, then head side", text: theirs + ours },
  ];
  return dedupe(all);
}

export function dedupe(candidates: Candidate[]): Candidate[] {
  const seen = new Set<string>();
  return candidates.filter((candidate) => {
    if (seen.has(candidate.text)) return false;
    seen.add(candidate.text);
    return true;
  });
}

/**
 * Applies one chosen text per conflict block. Refuses when the parsed blocks do not line up with the
 * merge's hunks (for example a file that already contained marker-like lines) or markers would remain.
 */
export function assembleResolution(content: string, hunks: ConflictHunk[], choices: string[]): string | null {