This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
A Broken Merge Can't Landfilter← all gifts

Conflict

A merge that goes wrong leaves <<<<<<<, =======, >>>>>>> markers wedged into a file; once committed, that file no longer parses — it isn't 'a merge in progress,' it's broken source that landed, and it hides until something tries to read it. Conflict is the one-command gate: wire it into a pre-commit hook or CI and a file carrying the marker triad simply cannot land. The clever part is the TRIAD RULE — ======= alone is a legal line (a Markdown rule, a comment banner), so conflict fires only on all three markers together, line-start only, and never cries wolf on a legal ======= or a marker mid-line.

The honest edge
This is a check, not an immunity — it protects you only when it is RUN, so wire it into a hook or CI rather than trusting a human to remember. And it detects the standard git marker triad; a tool that uses different markers needs a different pattern. Visibility, not immunity.
Run it
python3 conflict.py --help test_conflict.py (18 checks / 7 tests, mutation-bitten, triad-rule + line-start guards + throwaway-repo end-to-end) Python standard library only, read-only, deterministic
The code — every file that ships
conflict.py172 lineson GitHub →
#!/usr/bin/env python3
"""conflict — refuse to commit a file that no longer parses.

A merge that goes wrong leaves `<<<<<<<`, `=======`, `>>>>>>>` markers wedged
into a file. Once committed, that file no longer parses — it isn't "a merge in
progress," it's broken source that landed. Worse, it hides: the failure surfaces
only when something tries to read the file, and if that read happens in a quiet
place, it can sit on your main branch for days while everything downstream of it
silently degrades.

`conflict` is the one-command gate that catches it. It scans a git tree for the
merge-marker triad and exits non-zero if any file carries an unresolved merge, so
you can wire it into a pre-commit hook or CI and a broken file simply cannot land.

WHY IT'S HONEST

- **The triad, never the lone middle.** `=======` on its own is a legal line — a
  Markdown horizontal rule, a Python separator comment, an ASCII banner. It is a
  conflict marker ONLY in the company of `<<<<<<<` and `>>>>>>>` in the same
  file. `conflict` fires on the *triad within one file*, never on a bare
  `=======`. A lint that cries wolf on a legal line gets disabled, and a disabled
  lint is worse than none.

- **Decidable from bytes.** A conflict marker on a branch needs no judgment, no
  review, no discussion — it is arithmetic. `conflict` reports the finding as an
  exit code, not a paragraph: exit codes don't decay, don't need finding, and
  cost nothing to re-run.

- **It is read-only.** It reads blobs out of git (or off disk) and compares. It
  writes nothing and fixes nothing.

THE HONEST EDGE

This is a check, not an immunity — it protects you only when it is RUN. Wire it
into a hook or CI so "run it" isn't something a human has to remember. And it
detects the standard git marker triad; a tool that uses different markers needs a
different pattern. Visibility, not immunity.

USAGE
    python3 conflict.py                 # scan the working tree (tracked files)
    python3 conflict.py --ref main      # scan any committed tree by ref
    python3 conflict.py --ignore _snapshots/ --ignore vendor/
                                        # skip path substrings (repeatable)

EXIT  0 = clean · 3 = unresolved merge found (the alarm) · 2 = error/usage

MIT licensed. Python standard library only. Read-only. Deterministic.
"""
from __future__ import annotations

import subprocess
import sys

# The three markers git leaves on a conflicted merge. `=======` is only a marker
# in the company of the other two — see the triad rule in main().
MARKERS = ("<<<<<<< ", "=======", ">>>>>>> ")


def git_files(ref: str | None) -> list[str]:
    """List tracked files at `ref` (committed tree) or in the working tree."""
    if ref:
        out = subprocess.run(
            ["git", "ls-tree", "-r", "--name-only", ref],
            capture_output=True, text=True,
        ).stdout
    else:
        out = subprocess.run(["git", "ls-files"], capture_output=True, text=True).stdout
    return out.split()


def read_blob(f: str, ref: str | None) -> bytes:
    """Read a file's bytes at `ref` or from the working tree."""
    if ref:
        return subprocess.run(["git", "show", f"{ref}:{f}"], capture_output=True).stdout
    with open(f, "rb") as fh:
        return fh.read()


def marker_lines(text: str) -> dict[str, list[int]]:
    """Map each marker to the 1-based line numbers where a line STARTS with it."""
    lines = text.split("\n")
    return {m: [i + 1 for i, ln in enumerate(lines) if ln.startswith(m)] for m in MARKERS}


def has_conflict(text: str) -> bool:
    """True iff `text` contains the full marker TRIAD (not a lone `=======`)."""
    found = marker_lines(text)
    return all(found[m] for m in MARKERS)


def is_ignored(path: str, ignore: tuple[str, ...]) -> bool:
    """True iff `path` contains any ignore substring (skip it)."""
    return any(ig in path for ig in ignore)


def scan(ref: str | None = None, ignore: tuple[str, ...] = ()) -> list[tuple[str, dict]]:
    """Return [(path, marker_lines)] for every tracked file carrying the triad."""
    hits: list[tuple[str, dict]] = []
    for f in git_files(ref):
        if is_ignored(f, ignore):
            continue
        try:
            raw = read_blob(f, ref)
        except (OSError, FileNotFoundError):
            continue
        try:
            text = raw.decode("utf-8")
        except UnicodeDecodeError:
            continue  # a binary blob has no markers to read
        if has_conflict(text):
            hits.append((f, marker_lines(text)))
    return hits


def _parse_args(argv: list[str]) -> tuple[str | None, tuple[str, ...]]:
    ref: str | None = None
    ignore: list[str] = []
    i = 0
    while i < len(argv):
        a = argv[i]
        if a == "--ref":
            if i + 1 >= len(argv):
                raise ValueError("--ref needs a git ref")
            ref = argv[i + 1]
            i += 2
        elif a == "--ignore":
            if i + 1 >= len(argv):
                raise ValueError("--ignore needs a path substring")
            ignore.append(argv[i + 1])
            i += 2
        elif a in ("-h", "--help"):
            raise ValueError("__help__")
        else:
            raise ValueError(f"unknown argument: {a}")
    return ref, tuple(ignore)


def main(argv: list[str]) -> int:
    try:
        ref, ignore = _parse_args(argv)
    except ValueError as exc:
        if str(exc) == "__help__":
            sys.stdout.write(__doc__)
            return 2
        sys.stderr.write(f"conflict: {exc}\n")
        return 2

    hits = scan(ref, ignore)
    where = f"`{ref}`" if ref else "the working tree"

    if not hits:
        sys.stdout.write(f"conflict: clean — no unresolved merge in {where}.\n")
        return 0

    sys.stderr.write(
        f"conflict: {len(hits)} file(s) in {where} carry an UNRESOLVED MERGE.\n"
    )
    for f, found in hits:
        ln = found["<<<<<<< "][0]
        sys.stderr.write(
            f"  {f}:{ln}  <<<<<<< / ======= / >>>>>>>  "
            f"({len(found['<<<<<<< '])} conflict block(s))\n"
        )
    sys.stderr.write(
        "\nA conflict marker on a branch is not a merge in progress — it is a "
        "FILE THAT NO LONGER PARSES, landed. Resolve it. Do not commit over it.\n"
    )
    return 3


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
test_conflict.py181 lineson GitHub →
#!/usr/bin/env python3
"""test_conflict.py — the certifying properties of the conflict gift.

Run:  python3 test_conflict.py    (exit 0 = all pass, 1 = a failure)

The load-bearing property is the TRIAD RULE: a lone `=======` (a legal line) must
NOT fire, while the full marker triad must. Each test is MUTATION-BITTEN — it is
here because a plausible mutation of conflict.py makes it fail loud. The core is
pure (marker_lines / has_conflict / is_ignored), so it's tested directly; the
git-facing shell is exercised in a throwaway repo.
"""
import os
import subprocess
import sys

import conflict as c

_FAILURES: list[str] = []
_PASSES = 0


def check(cond: bool, msg: str) -> None:
    global _PASSES
    if cond:
        _PASSES += 1
    else:
        _FAILURES.append(msg)


CONFLICTED = """\
line before
<<<<<<< HEAD
ours
=======
theirs
>>>>>>> origin/main
line after
"""

# a lone `=======` in legal, innocent company — MUST NOT fire
INNOCENT_RULE = """\
Section One
=======
Section Two

Some prose under a Markdown setext rule.
"""

INNOCENT_BANNER = """\
# ============================================================
# a decorative comment banner, not a conflict
# ============================================================
def f(): pass
"""


# --- 1. the triad fires; a lone middle does not (the core property) ----------
def test_triad_rule():
    check(c.has_conflict(CONFLICTED), "the full marker triad must be detected")
    check(not c.has_conflict(INNOCENT_RULE), "a lone ======= (Markdown rule) must NOT fire")
    check(not c.has_conflict(INNOCENT_BANNER), "a ==== banner must NOT fire")
    # a triad that is missing one leg must NOT fire
    missing_close = CONFLICTED.replace(">>>>>>> origin/main\n", "")
    check(not c.has_conflict(missing_close), "two-of-three markers must NOT fire (needs the triad)")


# --- 2. golden: marker_lines reports the exact line numbers ------------------
def test_marker_lines_golden():
    found = c.marker_lines(CONFLICTED)
    check(found["<<<<<<< "] == [2], f"open marker line wrong: {found['<<<<<<< ']}")
    check(found["======="] == [4], f"middle marker line wrong: {found['=======']}")
    check(found[">>>>>>> "] == [6], f"close marker line wrong: {found['>>>>>>> ']}")
    # the three exact line-number asserts above ARE the golden: they pin the
    # detected positions, not merely the count.


# --- 3. only a line that STARTS with a marker counts ------------------------
def test_startswith_only():
    # a single marker mid-line (e.g. inside a string) is not a line-start conflict
    embedded = 'x = "<<<<<<< not a real marker"\n'
    check(not c.has_conflict(embedded), "a marker mid-line must not count as a conflict")
    # the discriminating case: ALL THREE markers mid-line. Under startswith this
    # is clean (no line STARTS with a marker); a naive `m in ln` would flag it.
    triad_midline = "a <<<<<<< b\nc ======= d\ne >>>>>>> f\n"
    check(
        not c.has_conflict(triad_midline),
        "three markers mid-line must NOT fire (startswith, not substring)",
    )


# --- 4. is_ignored honors substrings ----------------------------------------
def test_is_ignored():
    check(c.is_ignored("a/_snapshots/x.txt", ("_snapshots/",)), "ignore substring should match")
    check(not c.is_ignored("a/src/x.txt", ("_snapshots/",)), "non-matching path must not be ignored")
    check(not c.is_ignored("anything", ()), "empty ignore never ignores")


# --- 5. arg parse: --ref, --ignore (repeatable), unknown ---------------------
def test_parse_args():
    ref, ig = c._parse_args(["--ref", "main", "--ignore", "a/", "--ignore", "b/"])
    check(ref == "main", f"--ref not parsed: {ref}")
    check(ig == ("a/", "b/"), f"--ignore not repeatable: {ig}")
    raised = False
    try:
        c._parse_args(["--bogus"])
    except ValueError:
        raised = True
    check(raised, "unknown arg must raise")


# --- 6. END-TO-END against a throwaway repo ---------------------------------
def _run_git(cwd, *args):
    subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, check=True)


def test_end_to_end():
    import tempfile
    with tempfile.TemporaryDirectory() as repo:
        _run_git(repo, "init", "-q")
        _run_git(repo, "config", "user.email", "t@t")
        _run_git(repo, "config", "user.name", "t")
        # one clean file, one conflicted file
        with open(os.path.join(repo, "clean.py"), "w") as fh:
            fh.write("def ok(): pass\n")
        with open(os.path.join(repo, "broken.py"), "w") as fh:
            fh.write(CONFLICTED)
        _run_git(repo, "add", "-A")
        _run_git(repo, "commit", "-qm", "with a conflict")

        cwd0 = os.getcwd()
        os.chdir(repo)
        try:
            hits = c.scan(ref=None)
        finally:
            os.chdir(cwd0)
        paths = sorted(f for f, _ in hits)
        check(paths == ["broken.py"], f"end-to-end should flag only broken.py: {paths}")


# --- 7. ignore actually excludes a conflicted file --------------------------
def test_end_to_end_ignore():
    import tempfile
    with tempfile.TemporaryDirectory() as repo:
        _run_git(repo, "init", "-q")
        _run_git(repo, "config", "user.email", "t@t")
        _run_git(repo, "config", "user.name", "t")
        os.makedirs(os.path.join(repo, "_snapshots"))
        with open(os.path.join(repo, "_snapshots", "past.txt"), "w") as fh:
            fh.write(CONFLICTED)  # a deliberate record of a past conflict
        _run_git(repo, "add", "-A")
        _run_git(repo, "commit", "-qm", "snapshot")
        cwd0 = os.getcwd()
        os.chdir(repo)
        try:
            with_ignore = c.scan(ref=None, ignore=("_snapshots/",))
            without = c.scan(ref=None)
        finally:
            os.chdir(cwd0)
        check(with_ignore == [], f"ignored path should not be flagged: {with_ignore}")
        check(len(without) == 1, f"without ignore the file should flag: {without}")


def run() -> int:
    tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
    for t in tests:
        try:
            t()
        except Exception as exc:
            _FAILURES.append(f"{t.__name__} raised: {exc!r}")
    total = _PASSES + len(_FAILURES)
    if _FAILURES:
        print(f"FAIL — {len(_FAILURES)} of {total} checks failed:")
        for f in _FAILURES:
            print(f"  ✗ {f}")
        return 1
    print(f"OK — {_PASSES}/{total} checks passed ({len(tests)} tests).")
    return 0


if __name__ == "__main__":
    sys.exit(run())
Take the whole folder → MIT Python standard library only, read-only, deterministic