mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
146 lines
6.3 KiB
Bash
Executable File
146 lines
6.3 KiB
Bash
Executable File
#!/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)"
|
|
|