"""cohen_transform.py — Three-way line-level merge with Manyana-style action labels. Named in honour of Bram Cohen (creator of BitTorrent, inventor of the Manyana CRDT weave algorithm), whose conflict-presentation insight is the direct inspiration for this module. The Cohen Transform observation -------------------------------- Traditional VCS tools display a merge conflict as two opaque blobs — "ours" and "theirs" — and leave the user to reconstruct *what each side actually did*. Bram Cohen's Manyana project labels every conflict hunk with the action and the side that performed it: <<<<<<< ours [deleted] def calculate(x): a = x * 2 ||||||| base def calculate(x): a = x * 2 b = a + 1 return b ======= theirs [inserted] logger.debug(f"a={a}") >>>>>>> end conflict You can immediately see: ours deleted a function, theirs inserted a line into the middle of it. That is a structurally different level of information from two unlabelled blobs. Public API ---------- three_way_merge_lines(base, ours, theirs, ...) Core merge function. Returns (merged_lines, has_conflict). MergeRegion Dataclass representing one segment of the three-way analysis. classify_action(base_lines, other_lines) Classify a change as 'inserted', 'deleted', or 'modified'. annotate_hunk_action(hunk_lines, side_label) Post-process a unified-diff hunk list, rewriting @@ headers with action labels for use in ``muse diff --conflict`` output. format_conflict_diff(path, root, base_manifest, ours_manifest, theirs_manifest, read_object_fn, *, use_color, ours_label, theirs_label) Render a Manyana-style two-sided labeled diff for a single conflicting file. Used by ``muse diff --conflict``. References ---------- - Bram Cohen, Manyana: https://github.com/bramcohen/manyana - Bram Cohen's blog: https://bramcohen.com/ """ from __future__ import annotations import difflib import pathlib from dataclasses import dataclass, field from typing import Callable, Sequence # ── Public constants ────────────────────────────────────────────────────────── #: Separator used between the two sides of a conflict-aware diff header. CONFLICT_SEPARATOR = "═" * 54 #: Marker tokens — match git's diff3 style so editors recognise them, but #: the action label suffix (e.g. ``[deleted]``) is the Cohen extension. _MARKER_OURS_PREFIX = "<<<<<<< " _MARKER_BASE = "||||||| base" _MARKER_SEP_PREFIX = "======= " _MARKER_END = ">>>>>>> end conflict" # ── Data classes ────────────────────────────────────────────────────────────── @dataclass class MergeRegion: """One segment of a three-way merge analysis. Attributes: kind: ``'stable'`` — identical in all three versions; output base. ``'ours_only'`` — only ours changed from base; output ours. ``'theirs_only'`` — only theirs changed from base; output theirs. ``'both_same'`` — both sides made the same change; output either. ``'conflict'`` — both sides changed differently; needs markers. base_lines: Lines from the common ancestor for this segment. ours_lines: Lines from the ours version for this segment. theirs_lines: Lines from the theirs version for this segment. """ kind: str base_lines: list[str] = field(default_factory=list) ours_lines: list[str] = field(default_factory=list) theirs_lines: list[str] = field(default_factory=list) # ── Core algorithm ──────────────────────────────────────────────────────────── def _find_sync_regions( base: Sequence[str], ours: Sequence[str], theirs: Sequence[str], ) -> list[tuple[int, int, int, int, int, int]]: """Return maximal regions identical in all three sequences. Each entry is ``(base_s, base_e, ours_s, ours_e, theirs_s, theirs_e)`` representing a contiguous block where ``base[base_s:base_e] == ours[ours_s:ours_e] == theirs[theirs_s:theirs_e]``. The algorithm finds base positions matched in BOTH ``base→ours`` and ``base→theirs`` LCS runs, then groups them into maximal consecutive triples (consecutive in base, ours, *and* theirs simultaneously). """ sm_a = difflib.SequenceMatcher(None, base, ours, autojunk=False) sm_b = difflib.SequenceMatcher(None, base, theirs, autojunk=False) a_map: dict[int, int] = {} # base_pos → ours_pos b_map: dict[int, int] = {} # base_pos → theirs_pos for bi, ai, n in sm_a.get_matching_blocks(): for k in range(n): a_map[bi + k] = ai + k for bi, ti, n in sm_b.get_matching_blocks(): for k in range(n): b_map[bi + k] = ti + k stable = sorted(set(a_map) & set(b_map)) if not stable: return [] sync_regions: list[tuple[int, int, int, int, int, int]] = [] run_bp = run_ap = run_tp = -1 prev_bp = prev_ap = prev_tp = -1 for bp in stable: ap = a_map[bp] tp = b_map[bp] if bp == prev_bp + 1 and ap == prev_ap + 1 and tp == prev_tp + 1: # Extend current run. prev_bp, prev_ap, prev_tp = bp, ap, tp else: # Flush previous run (if any) and start a new one. if run_bp >= 0: sync_regions.append(( run_bp, prev_bp + 1, run_ap, prev_ap + 1, run_tp, prev_tp + 1, )) run_bp, run_ap, run_tp = bp, ap, tp prev_bp, prev_ap, prev_tp = bp, ap, tp # Flush final run. if run_bp >= 0: sync_regions.append(( run_bp, prev_bp + 1, run_ap, prev_ap + 1, run_tp, prev_tp + 1, )) return sync_regions def compute_regions( base: Sequence[str], ours: Sequence[str], theirs: Sequence[str], ) -> list[MergeRegion]: """Decompose three sequences into a list of :class:`MergeRegion` objects. Uses the sync-region algorithm: finds maximal blocks identical in all three sequences (the "stable skeleton"), then classifies each gap between stable blocks as one of: - ``ours_only`` — only ours changed from base. - ``theirs_only`` — only theirs changed from base. - ``both_same`` — both changed to the same content (clean). - ``conflict`` — both changed to different content. Handles pure insertions (both sides insert different content at the same base position) as conflicts with an empty base section. Args: base: Common ancestor lines (each ending with ``\\n`` or empty). ours: Working-tree / our-branch lines. theirs: Target-branch / their-branch lines. Returns: Ordered list of :class:`MergeRegion` objects covering the entire three-way merge. """ base = list(base) ours = list(ours) theirs = list(theirs) sync_regions = _find_sync_regions(base, ours, theirs) # Sentinel at each end so the loop below handles the leading and trailing # unstable regions uniformly without special-casing. sentinels: list[tuple[int, int, int, int, int, int]] = [ (0, 0, 0, 0, 0, 0), *sync_regions, (len(base), len(base), len(ours), len(ours), len(theirs), len(theirs)), ] regions: list[MergeRegion] = [] for idx in range(len(sentinels) - 1): cur = sentinels[idx] nxt = sentinels[idx + 1] # ── Stable block (the sync region itself) ───────────────────────── bs, be, as_, ae, ts, te = cur if idx > 0 and be > bs: regions.append(MergeRegion( kind="stable", base_lines=list(base[bs:be]), ours_lines=list(ours[as_:ae]), theirs_lines=list(theirs[ts:te]), )) # ── Unstable gap between this sync region and the next ──────────── nbs, nbe, nas, nae, nts, nte = nxt b_chunk = list(base[be:nbs]) a_chunk = list(ours[ae:nas]) t_chunk = list(theirs[te:nts]) if not b_chunk and not a_chunk and not t_chunk: continue if a_chunk == b_chunk and t_chunk == b_chunk: # All three identical — degenerate stable region (shouldn't # normally occur given sync detection, but guard it). regions.append(MergeRegion("stable", b_chunk, a_chunk, t_chunk)) elif a_chunk == b_chunk: # Only theirs changed. regions.append(MergeRegion("theirs_only", b_chunk, b_chunk, t_chunk)) elif t_chunk == b_chunk: # Only ours changed. regions.append(MergeRegion("ours_only", b_chunk, a_chunk, b_chunk)) elif a_chunk == t_chunk: # Both sides made the same change — clean (take either). regions.append(MergeRegion("both_same", b_chunk, a_chunk, t_chunk)) else: # True conflict — both sides changed differently. regions.append(MergeRegion("conflict", b_chunk, a_chunk, t_chunk)) return regions # ── Action classification ───────────────────────────────────────────────────── def classify_action(base_lines: list[str], other_lines: list[str]) -> str: """Classify the action performed from *base_lines* to *other_lines*. Returns one of: - ``'inserted'`` — *other_lines* is non-empty, *base_lines* is empty. - ``'deleted'`` — *base_lines* is non-empty, *other_lines* is empty. - ``'modified'`` — both are non-empty but differ. Used to annotate conflict markers with the Cohen-transform action label, e.g. ``<<<<<<< ours [deleted]``. """ if not base_lines and other_lines: return "inserted" if base_lines and not other_lines: return "deleted" return "modified" def annotate_hunk_action(hunk_lines: list[str], side_label: str) -> list[str]: """Rewrite ``@@`` hunk headers with a Manyana-style action annotation. Scans the ``+``/``-`` lines in each hunk to classify the dominant action on *side_label* (``'ours'`` or ``'theirs'``), then rewrites each ``@@`` header line from:: @@ -12,4 +12,4 @@ to:: @@ -12,4 +12,4 @@ [ours: deleted] @@ -12,4 +12,4 @@ [theirs: inserted] @@ -12,4 +12,4 @@ [ours: modified] This is the direct translation of Bram Cohen's ``begin deleted left`` / ``begin added right`` labeling into unified-diff format. Args: hunk_lines: Lines produced by :func:`difflib.unified_diff`, including the ``---``/``+++`` header and all ``@@`` blocks. side_label: Human-readable side identifier, e.g. ``'ours'`` or ``'theirs'``. Returns: Annotated copy of *hunk_lines* with ``@@`` lines rewritten. """ result: list[str] = [] # Collect lines in the current @@ block to classify when we see the next @@. current_hunk_body: list[str] = [] pending_at: str | None = None # the @@ line waiting to be annotated def _flush(body: list[str]) -> str: """Classify and return the annotated @@ line.""" assert pending_at is not None adds = sum(1 for ln in body if ln.startswith("+") and not ln.startswith("+++")) dels = sum(1 for ln in body if ln.startswith("-") and not ln.startswith("---")) if adds > 0 and dels == 0: action = "inserted" elif dels > 0 and adds == 0: action = "deleted" else: action = "modified" base_at = pending_at.rstrip() # Remove any trailing existing annotation before appending a new one. if " [" in base_at: base_at = base_at[: base_at.rfind(" [")] return f"{base_at} [{side_label}: {action}]" for line in hunk_lines: if line.startswith("@@"): if pending_at is not None: result.append(_flush(current_hunk_body)) result.extend(current_hunk_body) current_hunk_body = [] pending_at = line elif pending_at is not None: current_hunk_body.append(line) else: result.append(line) # Flush the final hunk. if pending_at is not None: result.append(_flush(current_hunk_body)) result.extend(current_hunk_body) return result # ── High-level merge function ───────────────────────────────────────────────── def three_way_merge_lines( base: Sequence[str], ours: Sequence[str], theirs: Sequence[str], *, label_ours: str = "ours", label_base: str = "base", label_theirs: str = "theirs", ) -> tuple[list[str], bool]: """Three-way line merge with Manyana-style labeled conflict markers. Implements the Cohen Transform: conflict markers include an action label (``[inserted]``, ``[deleted]``, or ``[modified]``) so the reader immediately sees *what each side did*, not just *that they conflicted*. Marker format (diff3 style with Cohen extensions):: <<<<<<< ours [deleted] [lines from ours — what ours changed base to] ||||||| base [lines from common ancestor — the original context] ======= theirs [inserted] [lines from theirs — what theirs changed base to] >>>>>>> end conflict The ``||||||| base`` section is always included (diff3 style) because it gives both humans and agents the context needed to understand *why* each side made its change. Clean merge rules: - Stable (no change on either side): output base unchanged. - Ours-only change: output ours version. - Theirs-only change: output theirs version. - Both changed to the same content: output that content once. - Both changed differently: emit conflict markers. Args: base: Lines from the common ancestor. ours: Lines from our version (working tree or our branch). theirs: Lines from their version (target branch). label_ours: Label shown in the ``<<<<<<<`` marker. Defaults to ``'ours'``. label_base: Label shown in the ``|||||||`` marker. Defaults to ``'base'``. label_theirs: Label shown in the ``=======`` marker. Defaults to ``'theirs'``. Returns: A ``(merged_lines, has_conflict)`` tuple. *merged_lines* is the fully merged sequence; *has_conflict* is ``True`` when at least one conflict block was written. """ regions = compute_regions(base, ours, theirs) merged: list[str] = [] has_conflict = False for region in regions: if region.kind == "stable": merged.extend(region.base_lines) elif region.kind == "ours_only": merged.extend(region.ours_lines) elif region.kind == "theirs_only": merged.extend(region.theirs_lines) elif region.kind == "both_same": merged.extend(region.ours_lines) else: # conflict has_conflict = True ours_action = classify_action(region.base_lines, region.ours_lines) theirs_action = classify_action(region.base_lines, region.theirs_lines) merged.append(f"{_MARKER_OURS_PREFIX}{label_ours} [{ours_action}]\n") merged.extend(region.ours_lines) merged.append(f"||||||| {label_base}\n") merged.extend(region.base_lines) merged.append(f"{_MARKER_SEP_PREFIX}{label_theirs} [{theirs_action}]\n") merged.extend(region.theirs_lines) merged.append(f"{_MARKER_END}\n") return merged, has_conflict # ── Conflict-diff renderer (for muse diff --conflict) ──────────────────────── def format_conflict_diff( path: str, root: pathlib.Path, base_manifest: dict[str, str], ours_manifest: dict[str, str], theirs_manifest: dict[str, str], read_object_fn: Callable[[pathlib.Path, str], bytes | None], *, use_color: bool = False, ours_label: str = "ours", theirs_label: str = "theirs", ) -> list[str]: """Render a labeled two-sided diff for a single conflicting file. Produces a Cohen-transform conflict view: two separate unified diffs (``base→ours`` and ``base→theirs``), each with hunk headers annotated with the action performed by that side. This replaces the opaque ``<<<<<<< / ======= / >>>>>>>`` blob with a clear narrative of *what each side did*. Example output:: ══════════════════════════════════════════════════════ CONFLICT src/utils.py ══════════════════════════════════════════════════════ [ours] what ours changed from base --- base/src/utils.py +++ ours/src/utils.py @@ -12,4 +12,4 @@ [ours: deleted] - def calculate(x): - return x * 2 + def compute(x): + return x * 3 [theirs] what theirs changed from base --- base/src/utils.py +++ theirs/src/utils.py @@ -12,4 +12,4 @@ [theirs: modified] def calculate(x): - return x * 2 + return x * 4 Args: path: Workspace-relative POSIX path of the conflicting file. root: Repository root (used to read objects from the store). base_manifest: Manifest of the merge-base commit. ours_manifest: Manifest of our branch at merge time. theirs_manifest: Manifest of their branch. read_object_fn: Callable ``(root, object_id) → bytes | None``. use_color: When ``True``, emit ANSI colour escapes. ours_label: Human-readable label for the ours side (e.g. the branch name). theirs_label: Human-readable label for the theirs side. Returns: A list of text lines (each *without* a trailing newline) ready to print. Returns an empty list when no diff exists for this path. """ def _read_lines(manifest: dict[str, str], fallback_disk: bool = False) -> list[str]: oid = manifest.get(path) if oid: raw = read_object_fn(root, oid) if raw is not None: text = raw.decode("utf-8", errors="replace") return text.splitlines(keepends=True) if fallback_disk: disk = root / path if disk.is_file(): return disk.read_text(encoding="utf-8", errors="replace").splitlines(keepends=True) return [] safe_path = path.replace("\x1b", "?") # sanitize ANSI injection base_lines = _read_lines(base_manifest) ours_lines = _read_lines(ours_manifest, fallback_disk=True) theirs_lines = _read_lines(theirs_manifest) # ── ANSI colour helpers ─────────────────────────────────────────────────── def _c(code: str, text: str) -> str: return f"\x1b[{code}m{text}\x1b[0m" if use_color else text def _bold(t: str) -> str: return _c("1", t) def _cyan(t: str) -> str: return _c("36", t) def _green(t: str) -> str: return _c("32", t) def _red(t: str) -> str: return _c("31", t) def _yellow(t: str) -> str: return _c("33", t) # ── Header ──────────────────────────────────────────────────────────────── output: list[str] = [] output.append(_bold(CONFLICT_SEPARATOR)) output.append(_bold(f"CONFLICT {safe_path}")) output.append(_bold(CONFLICT_SEPARATOR)) # ── Ours diff (base → ours) ─────────────────────────────────────────────── ours_hunks = list(difflib.unified_diff( base_lines, ours_lines, fromfile=f"base/{safe_path}", tofile=f"{ours_label}/{safe_path}", lineterm="", )) annotated_ours = annotate_hunk_action(ours_hunks, ours_label) output.append("") output.append(_yellow(f"[{ours_label}] what {ours_label} changed from base")) if annotated_ours: for line in annotated_ours: if line.startswith("---") or line.startswith("+++"): output.append(_bold(line)) elif line.startswith("@@"): output.append(_cyan(line)) elif line.startswith("+"): output.append(_green(line)) elif line.startswith("-"): output.append(_red(line)) else: output.append(line) else: output.append(" (no changes from base on this side)") # ── Theirs diff (base → theirs) ─────────────────────────────────────────── theirs_hunks = list(difflib.unified_diff( base_lines, theirs_lines, fromfile=f"base/{safe_path}", tofile=f"{theirs_label}/{safe_path}", lineterm="", )) annotated_theirs = annotate_hunk_action(theirs_hunks, theirs_label) output.append("") output.append(_yellow(f"[{theirs_label}] what {theirs_label} changed from base")) if annotated_theirs: for line in annotated_theirs: if line.startswith("---") or line.startswith("+++"): output.append(_bold(line)) elif line.startswith("@@"): output.append(_cyan(line)) elif line.startswith("+"): output.append(_green(line)) elif line.startswith("-"): output.append(_red(line)) else: output.append(line) else: output.append(" (no changes from base on this side)") output.append("") return output