#!/usr/bin/env bash
# conflictmarkers — refuse a repo that has committed an unresolved merge.
# One implementation, every caller.
#
#   conflictmarkers [dir]
#
# WHAT THIS CATCHES
#
# `git merge` writes its disagreement INTO the file and stops. Resolving means
# editing those lines out; nothing forces you to. `git add` on a file that still
# contains them is accepted without complaint, and from that moment the markers
# are ordinary committed content — `git status` is clean, `git diff` is empty,
# and the conflict is now a permanent feature of the branch.
#
# It survives because of WHERE it lands. The file is usually generated or
# vendored — large, rarely opened, excluded from review by its own size. Nobody
# reads line 23,807 of a .d.ts. hanzoai/base carried exactly this on main:
#
#   plugins/jsvm/internal/types/generated/types.d.ts:23807   <<<<<<< HEAD
#   plugins/jsvm/internal/types/generated/types.d.ts:23850   >>>>>>> upstream/master
#
# from an upstream merge nobody finished, sitting in the shipped package.
#
# The compilers are no help, which is the whole problem. In TypeScript the
# markers are a syntax error — but a .d.ts nothing imports is never parsed, so
# there is no error to see. In Go they are a parse error only in a file the
# build reaches. In YAML, JSON, Markdown, SQL, HTML and every config format we
# ship, they are silently VALID content: a marker in a values file is a key
# nobody notices, and a marker in a Markdown doc renders as text.
#
# So the defect class is "both sides of a merge shipped, and nothing in the
# pipeline had an opinion". That is the same shape as its two siblings in this
# step — a declaration and reality disagreeing, with no reader positioned to
# notice — which is why it belongs here rather than in any one repo's gate.
#
# WHAT IT LOOKS FOR, AND WHY NOT MORE
#
# Only the two LABELLED markers git actually writes:
#
#   ^<<<<<<< <label>     the start marker, seven '<' then a space
#   ^=======              the divider, seven bare equals
#   ^>>>>>>> <label>     the end marker, seven '>' then a space
#
# ALL THREE, IN THAT ORDER, EACH ON ITS OWN LINE. Not any one of them — the
# whole shape. That is not belt-and-braces, it is the difference between a gate
# and a nuisance, and it was measured the expensive way: matching the start
# marker ALONE failed hanzoai/app, whose builder tests carry
#
#   <<<<<<< START_TITLE index.html >>>>>>> END_TITLE
#
# as literal fixture data. That line opens with seven '<' and a space and is
# not a merge conflict — both markers sit on ONE line, which is a thing git
# never writes. A start marker that also carries an end marker is therefore
# excluded outright, and a file must show the divider and a separate end line
# before this refuses it.
#
# The divider is only ever matched as part of that conjunction, never alone:
# seven bare equals signs at column 1 is also how reStructuredText and Setext
# Markdown underline a heading, and failing every doc in the estate is how a
# gate gets switched off.
#
# Exactly seven, anchored at column 1: `<<<<<<<<` (eight — a heredoc, a C++
# stream) does not match, and neither does an indented mention inside a comment
# explaining conflict markers, which is what lets this file describe them
# without flagging itself.
#
# Tracked files only, via `git grep`, so a stray marker in an untracked scratch
# file or in node_modules cannot fail a build. `-I` skips binaries.
set -euo pipefail

cd "${1:-.}"

git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
  echo "conflictmarkers: not a git work tree ($(pwd)) — skipping"; exit 0; }

# Built from variables rather than written literally, so this script is not its
# own first finding when it scans the repo that carries it.
L=$(printf '<%.0s' $(seq 7))
E=$(printf '=%.0s' $(seq 7))
R=$(printf '>%.0s' $(seq 7))

# A FENCED CODE BLOCK IS QUOTATION, NOT CONTENT
#
# Indentation is the escape everywhere else here, and inside a fence it is not
# available: leading spaces are rendered, so indenting would change what the
# document SHOWS. And the shape alone cannot separate the two cases —
# hanzoai/code documents Void's Search/Replace format, which deliberately spells
# itself the way git does:
#
#   <<<<<<< ORIGINAL
#   =======
#   >>>>>>> UPDATED
#
# All three markers, in order, each at column 1. That is a real merge conflict
# to every test above, and it is a code sample. Search/replace blocks in this
# form are how most edit-applying tools prompt a model, so any repo that
# documents one lands here.
#
# So read the fence: for Markdown, scan the file with fenced regions removed.
# Only Markdown, and only into a temp file — the defect this exists to catch
# lives in 23,000-line generated files, and those keep the direct path.
# `--` is grep's option terminator, not awk's: awk reads it as a FILENAME and
# dies "can't open file --". Paths reaching here come from `git grep -l`, so
# they are tracked files, never options.
scannable() {
  awk '/^[[:space:]]*(```|~~~)/ { fence = !fence; next } !fence' "$1"
}

# Candidate files: those carrying a start marker that is NOT also an end marker
# on the same line. `-l` for names only; the line numbers come later, per file.
cands=$(git grep -I -l -E "^${L} " -- . || true)

bad=""
tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT
for f in $cands; do
  case "$f" in
    *.md|*.markdown|*.mdx) scannable "$f" > "$tmp" 2>/dev/null; v="$tmp" ;;
    *) v="$f" ;;
  esac
  grep -qE "^${L} " -- "$v" 2>/dev/null || continue
  # The same-line form (app's fixture) is not a conflict. Require at least one
  # start line that does not also carry an end marker.
  grep -E "^${L} " -- "$v" 2>/dev/null | grep -qv -- "$R" || continue
  # And require the rest of the shape: a bare divider and a separate end line.
  grep -qE "^${E}$" -- "$v" 2>/dev/null || continue
  grep -E "^${R} " -- "$v" 2>/dev/null | grep -qv -- "$L" || continue
  bad="$bad $f"
done

if [ -n "$bad" ]; then
  echo "::error::committed merge-conflict markers — an unresolved merge is in the tree"
  for f in $bad; do
    grep -nE "^(${L} |${E}$|${R} )" -- "$f" | while IFS= read -r line; do
      echo "  $f:$line"
    done
    echo "::error file=${f}::committed conflict marker"
  done
  echo
  echo "Resolve the merge and commit the result. If the file is GENERATED, do not"
  echo "hand-edit it — re-run its generator and commit that."
  exit 1
fi

echo "OK: no committed conflict markers ($(git ls-files | wc -l) tracked files)"

