124 lines
6.4 KiB
Bash
Executable File
124 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify the Git semantics that SKILL.md asserts, by experiment.
|
|
#
|
|
# Scope: this script proves the GIT behaviours exercised below. It does NOT prove
|
|
# GitHub CLI behaviour (gh pr merge, run selection, merge queues) — those are
|
|
# verified procedurally in SKILL.md §6, not here. Do not read a green run as
|
|
# "every claim in the skill is verified"; read it as "these specific git claims
|
|
# hold". Adding a claim to the skill without adding a check here leaves it
|
|
# unverified, which is the failure the skill itself warns about.
|
|
set -euo pipefail
|
|
|
|
pass=0; fail=0
|
|
chk() { # chk "description" expected actual
|
|
if [ "$2" = "$3" ]; then printf ' PASS %s\n' "$1"; pass=$((pass+1));
|
|
else printf ' FAIL %s\n expected=[%s] actual=[%s]\n' "$1" "$2" "$3"; fail=$((fail+1)); fi
|
|
}
|
|
newrepo() {
|
|
T=$(mktemp -d); cd "$T"
|
|
git init -q -b master .; git config user.email t@t; git config user.name t
|
|
echo base > base.txt; git add .; git commit -qm base
|
|
}
|
|
cleanup() { cd /; rm -rf "$T"; }
|
|
|
|
echo "--- §7: two-tree diff vs the actual merge result (the D trap) ---"
|
|
newrepo
|
|
git checkout -qb feature
|
|
git checkout -q master; echo m > master-only.txt; git add .; git commit -qm "master adds file"
|
|
chk "two-tree diff reports D for a file the branch merely predates" \
|
|
"D master-only.txt" "$(git diff --name-status master feature)"
|
|
TREE=$(git merge-tree --write-tree master feature | head -1)
|
|
chk "merge-tree PRESERVES it -> a D does NOT mean the merge deletes it" \
|
|
"1" "$(git ls-tree -r --name-only "$TREE" | grep -c '^master-only.txt$')"
|
|
chk "three-dot diff is empty for a stale-only branch" "" "$(git diff --name-status master...feature)"
|
|
if git merge-base --is-ancestor master feature; then rc=0; else rc=1; fi
|
|
chk "is-ancestor exits 1 when the branch predates master" "1" "$rc"
|
|
cleanup
|
|
|
|
echo "--- §7: a genuine branch-side deletion IS visible in the three-dot diff ---"
|
|
newrepo
|
|
git checkout -q master; echo shared > shared.txt; git add .; git commit -qm "add shared"
|
|
git checkout -qb feature; git merge -q master; git rm -q shared.txt; git commit -qm "branch deletes shared"
|
|
chk "three-dot diff shows the real branch-side deletion" \
|
|
"D shared.txt" "$(git diff --name-status master...feature)"
|
|
cleanup
|
|
|
|
echo "--- §6: --no-merges excludes the merge commit from a commit-message gate ---"
|
|
newrepo
|
|
git checkout -qb f; echo x > x.txt; git add .; git commit -qm x
|
|
git checkout -q master; git merge -q --no-ff -m "a merge commit" f
|
|
MERGE=$(git rev-parse master)
|
|
chk "the merge commit exists and is found by --merges" "$MERGE" "$(git rev-list --merges master)"
|
|
# The real assertion: the merge sha is ABSENT from the --no-merges enumeration.
|
|
# (An earlier version of this script tested `--no-merges --merges`, which is a
|
|
# contradiction: it returns nothing for ANY repo and so proved nothing.)
|
|
chk "the merge sha is absent from the --no-merges list" \
|
|
"0" "$(git rev-list --no-merges master | grep -c "^$MERGE$" || true)"
|
|
chk "non-merge commits are still listed" \
|
|
"2" "$(git rev-list --no-merges master | wc -l | tr -d ' ')"
|
|
cleanup
|
|
|
|
echo "--- §7: squash captures the head AT MERGE TIME ---"
|
|
newrepo
|
|
git checkout -qb work; echo a > a.txt; git add .; git commit -qm work
|
|
EARLY=$(git rev-parse work)
|
|
echo b > b.txt; git add .; git commit -qm "later commit, pushed after the head was selected"
|
|
git checkout -q master; git merge -q --squash "$EARLY" >/dev/null; git commit -qm "squash work (#1)"
|
|
chk "the selected head is in the merge" "1" "$(git ls-tree -r --name-only HEAD | grep -c '^a.txt$')"
|
|
chk "the later commit is NOT in the merge" "0" "$(git ls-tree -r --name-only HEAD | grep -c '^b.txt$')"
|
|
cleanup
|
|
|
|
echo "--- §6: stacked-branch recovery after a base squash-merge ---"
|
|
newrepo
|
|
git checkout -qb feat-base; echo b1 > b1.txt; git add .; git commit -qm "base: work"
|
|
B=$(git rev-parse feat-base) # base tip recorded BEFORE the merge
|
|
git checkout -qb feat-stack; echo s1 > s1.txt; git add .; git commit -qm "stack: work"
|
|
git checkout -q master; git merge -q --squash feat-base >/dev/null; git commit -qm "base: work (#1)"
|
|
M=$(git rev-parse master)
|
|
chk "log --no-merges M..branch does NOT isolate the real work (lists base too)" \
|
|
"2" "$(git log --oneline --no-merges $M..feat-stack | wc -l | tr -d ' ')"
|
|
chk "diff M...branch shows the base's file too (M is not an ancestor)" \
|
|
"2" "$(git diff --name-status $M...feat-stack | wc -l | tr -d ' ')"
|
|
git switch -q feat-stack
|
|
git rebase -q --onto $M $B feat-stack
|
|
chk "after rebase --onto M B, the branch's diff is ONLY its own work" \
|
|
"A s1.txt" "$(git diff --name-status $M...feat-stack)"
|
|
cleanup
|
|
|
|
echo "--- §7: merge-tree reports conflicts instead of silently succeeding ---"
|
|
newrepo
|
|
git checkout -qb c1; echo one > c.txt; git add .; git commit -qm one
|
|
git checkout -q master; echo two > c.txt; git add .; git commit -qm two
|
|
if git merge-tree --write-tree master c1 >/dev/null 2>&1; then rc=0; else rc=1; fi
|
|
chk "a conflicting merge makes merge-tree exit non-zero" "1" "$rc"
|
|
cleanup
|
|
|
|
|
|
echo "--- §6: the merge guards fail closed (not just warn) ---"
|
|
# These are the forms that DO NOT fail closed, so the skill's assertions matter.
|
|
set +e
|
|
out=$(echo '{"a":1}' | jq 'select(.b=="x")' 2>/dev/null); rc=$?
|
|
set -e
|
|
chk "jq select with no match emits nothing and exits 0 (the trap)" "0" "$rc"
|
|
chk "...and emits no output, so an assertion on its output is required" "" "$out"
|
|
set +e
|
|
out=$(echo '[]' | jq 'sort_by(.x) | last | .y' 2>/dev/null); rc=$?
|
|
set -e
|
|
chk "last on an empty array yields the literal null" "null" "$out"
|
|
set +e; bash -c '[ 1 = 2 ] || { echo warn; }' >/dev/null 2>&1; rc=$?; set -e
|
|
chk "a bare '|| echo' warning still exits 0 (why guards must exit 1)" "0" "$rc"
|
|
|
|
guard() { # guard <expr-rc-cmd> ; emulate the skill's guard shape
|
|
if "$@" >/dev/null 2>&1; then return 0; else return 1; fi
|
|
}
|
|
chk "empty RUN is rejected" "1" "$(guard bash -c '[ -n "" ] && [ "" != null ]' ; echo $?)"
|
|
chk "null RUN is rejected" "1" "$(guard bash -c '[ -n null ] && [ null != null ]' ; echo $?)"
|
|
chk "valid RUN is accepted" "0" "$(guard bash -c '[ -n 123 ] && [ 123 != null ]' ; echo $?)"
|
|
chk "empty MATCH is rejected" "1" "$(guard bash -c '[ -n "" ]' ; echo $?)"
|
|
chk "skipped step is rejected" "1" "$(guard bash -c 'echo skipped | grep -qx success' ; echo $?)"
|
|
chk "success step is accepted" "0" "$(guard bash -c 'echo success | grep -qx success' ; echo $?)"
|
|
chk "missing step is rejected" "1" "$(guard bash -c 'echo "" | grep -qx success' ; echo $?)"
|
|
|
|
printf '\n%d passed, %d failed\n' "$pass" "$fail"
|
|
[ "$fail" -eq 0 ]
|