Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions scripts/pm/os-regen-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ rr_read_record() {
# Repeated key, so this one is read as a SET rather than by `rr_field`.
rr_rec_conflicted="$RR_NL$(awk 'index($0, "conflicted=") == 1 { print substr($0, 12) }' "$rr_record")$RR_NL"
case "$rr_rec_phase" in
pending | done) ;;
pending | handoff | done) ;;
*) return 0 ;;
esac
for rr_sha in "$rr_rec_base" "$rr_rec_branch_tip" "$rr_rec_main_tip"; do
Expand Down Expand Up @@ -626,6 +626,16 @@ rr_write_record() {
# history is the safe one: a missing ancestor object answers "not contained",
# which routes to `plain` — today's behaviour — and never to a repair on a base
# this script cannot prove.

# Is HEAD the very merge this record was written for? ⚠️ Containment is NOT that question, and
# asking it for the rerun was the measured defect: every later branch commit contains the recorded
# pre-merge tip too, so a run made after the operator finished step 3 BY HAND re-entered `rerun`,
# took main's side back over their own regeneration commit, and step 3 COMMITTED it.
rr_head_is_recorded_merge() {
[ "$(git rev-parse --verify --quiet 'HEAD^1' || true)" = "$rr_rec_branch_tip" ] &&
[ "$(git rev-parse --verify --quiet 'HEAD^2' || true)" = "$rr_rec_main_tip" ]
}

rr_classify() {
if [ "$rr_have_record" = yes ]; then
if [ "$rr_record_ok" != yes ]; then
Expand All @@ -638,8 +648,15 @@ rr_classify() {
fi
if git merge-base --is-ancestor "$rr_rec_main_tip" HEAD 2>/dev/null; then
# The recorded merge is IN this HEAD, so step 1 is behind us either way.
if [ "$rr_rec_phase" != done ]; then
# ⛔ Redoing step 2 is sound ONLY while HEAD still IS that merge — a commit past
# it is work step 2 would discard. `handoff` (step 3 refused, the index left
# discharged) AT the merge means that index was dropped and step 2 is owed again;
# PAST it means the operator committed, so step 4 is owed. A `pending` record past
# its merge says nobody ever marked step 2 done — refused below, ⛔ never redone.
if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then
echo rerun
elif [ "$rr_rec_phase" = pending ]; then
echo advanced
elif [ "$(git rev-parse origin/main)" = "$rr_rec_main_tip" ]; then
echo settled
else
Expand Down Expand Up @@ -673,6 +690,26 @@ rr_refuse_stale() {
exit 1
}

# Recorded `pending`, yet the branch has commits PAST the recorded merge — the state
# step 3's refusal used to leave behind, since it exits before marking anything while
# instructing the operator to finish the commit OUTSIDE this script. Marked `handoff`
# that refusal no longer lands here; a record nothing ever marked still does.
rr_refuse_advanced() {
echo "✗ the recorded merge is in HEAD, the record still says \`pending\`, and this branch has" >&2
echo " commits PAST that merge — refusing to redo step 2 over them." >&2
rr_print_record
echo " \`pending\` means no run ever marked step 2 discharged, and the later commits mean" >&2
echo " somebody did. Redoing it now pins the base to the PRE-MERGE shas above, takes main's side" >&2
echo " of every generated path both sides moved — discarding those commits' bytes — and step 3" >&2
echo " COMMITS that, exit 0: ⛔ a revert wearing a repair's message. Finished step 3 BY HAND, as" >&2
echo " its refusal says to? Then step 2 IS discharged: \`rm -f $rr_record\` and resume at STEP 4." >&2
echo " If NOT, do step 2 by hand against the RECORDED base, commit, and continue with step 4:" >&2
echo " git diff --name-only $rr_rec_base $rr_rec_main_tip -- <the merge=os-regen patterns>" >&2
echo " git diff --name-only $rr_rec_base $rr_rec_branch_tip -- <the same patterns>" >&2
echo " git restore --source=$rr_rec_main_tip -- <every path in BOTH lists> # tree only" >&2
exit 1
}

# The state the old conflict messages sent operators into, now named out loud.
rr_refuse_orphan() {
echo "✗ HEAD already contains origin/main and NO pre-merge base is recorded — refusing to run" >&2
Expand Down Expand Up @@ -759,6 +796,7 @@ mode_run() {
rr_mode="$(rr_classify)"
case "$rr_mode" in
stale) rr_refuse_stale ;;
advanced) rr_refuse_advanced ;;
orphan) rr_refuse_orphan ;;
settled)
rr_report_settled
Expand Down Expand Up @@ -1132,20 +1170,33 @@ mode_run() {
if [ -n "$(git status --porcelain)" ]; then
git add -A
if ! git commit --no-edit -m "merge origin/main (os-regen artifacts taken from main; regeneration follows)"; then
# ⛔ MARKED before this exit, and the marking is the repair: step 2 is discharged in
# the index at this instant and the line below hands the commit to the operator — the
# ONE path that finishes step 3 outside this script. Left `pending`, the next run read
# containment, re-entered `rerun` and took main's side back over whatever that hand-off
# committed. The conflict set rides along, so a DROPPED index still keeps resolutions.
rr_write_record handoff "$merge_base" "$branch_tip" "$main_side" "$rr_rec_conflicted"
echo "✗ step 3's commit was refused — the merge is staged but NOT committed." >&2
echo " ⛔ Do not regenerate yet: with a staged index a bare \`git commit\` after" >&2
echo " regeneration lands the STAGED side, not the tree you inspected." >&2
echo " Clear what the hook reported, then \`git add -A && git commit\` before step 4." >&2
echo " Step 2 IS discharged in that index and the record says so (phase=handoff), so the" >&2
echo " sequence resumes at STEP 4 once that commit exists: $rr_record" >&2
echo " ⛔ Do NOT rerun this script to finish it — that redoes step 2 against the pre-merge" >&2
echo " base and takes main's side back over what you just committed." >&2
exit 1
fi
else
echo " (merge left no additional changes to commit)"
fi

# Hand-off assertion: step 4 regenerates on top of this tree and commits the
# result, so anything uncommitted here rides into that commit unread.
# result, so anything uncommitted here rides into that commit unread. ⛔ This exit marks
# the record too: step 2 is COMMITTED by now, and a record left `pending` would send the
# next run back through step 2 over it.
handoff="$(git status --porcelain -- "${regen_paths[@]}")"
if [ -n "$handoff" ]; then
rr_write_record handoff "$merge_base" "$branch_tip" "$main_side" "$rr_rec_conflicted"
echo "✗ refusing to hand off to step 4 — generated paths are not committed:" >&2
printf '%s\n' "$handoff" >&2
echo " A regen path whose INDEX and WORKTREE disagree (porcelain \`MM\`) is the trap:" >&2
Expand Down Expand Up @@ -2098,6 +2149,45 @@ mode_self_test() {
"$(git show HEAD:gen/deferred.txt | grep -c 'deferred v1-MAIN' || true)" 0
cd "$here"

# --- 11. REPRO 1 (this card): step 3's commit REFUSED hands the commit to the operator, and
# until the record said so the NEXT run re-entered `rerun`, redid step 2 against the
# pre-merge base and COMMITTED a revert of that operator's own regeneration (`FLOWX`
# below) — exit 0, nothing refused.
st_fixture_rerun "$tmp/m"
bash "$SELF" >/dev/null 2>&1 || true # run 1 stops on the MIXED conflict
st_resolve_and_commit && st_record="$(rr_record_path)" # the merge, committed by hand
printf '#!/bin/sh\nexit 1\n' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 2: the rerun, step 3 refused
st_case 'r1: the refused step-3 commit fails, MARKS handoff, warns against rerunning' \
"$rc/$(rr_field phase "$st_record")/$(printf '%s' "$out" | grep -c 'Do NOT rerun this script' || true)" '1/handoff/1'
rm -f .git/hooks/pre-commit && printf 'deferred v1-MAIN\nFLOWX\n' > gen/deferred.txt # as told
git add -A && git commit -qm 'step 4 by hand: regenerate (FLOWX)'
cp -a "$tmp/m" "$tmp/m-mut" # 11b replays run 3 from right here
out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? # run 3: the card's own run
st_case 'r1: run 3 exits 0 discharged — no rerun, no side taken, and FLOWX SURVIVES (the card read 0)' \
"$rc/$(printf '%s' "$out" | grep -c '^→ RERUN:' || true)/$(printf '%s' "$out" | grep -c 'already discharged' || true)/$(printf '%s' "$out" | grep -c "TAKING main.s side" || true)/$(git show HEAD:gen/deferred.txt | grep -c FLOWX || true)" '0/0/1/0/1'
# The safety net for a record nothing ever marked: refused, ⛔ never silently redone.
sed 's/^phase=.*/phase=pending/' "$st_record" > "$st_record.t" && mv "$st_record.t" "$st_record"
out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$?
st_case 'r1: a pending record past its own merge is REFUSED with the by-hand step 2' \
"$rc/$(printf '%s' "$out" | grep -c "git restore --source=$(rr_field main_tip "$st_record")" || true)" '1/1'
cd "$here"

# --- 11b. THE DISCRIMINATING MUTATION for case 11: put the containment gate back —
# the ONE line — and the card reproduces on demand, in the tree case 11 copied aside
# one commit earlier. Same perl/\Q..\E replacement 6b, 8b, 9b and 10b use.
mutated_reentry="$tmp/mutated-reentry-os-regen-merge.sh"
MUT_ANCHOR=' if [ "$rr_rec_phase" != done ] && rr_head_is_recorded_merge; then' \
MUT_INSERT=' if [ "$rr_rec_phase" != done ]; then' \
perl -0777 -pe 's/\Q$ENV{MUT_ANCHOR}\E/$ENV{MUT_INSERT}/' "$SELF" > "$mutated_reentry"
st_case 'the equality gate has ONE call site, the mutation took, and it parses' \
"$(sed -n '1,/^# --- self-test/p' "$SELF" | grep -c 'rr_head_is_recorded_merge; then' || true)/$(diff -q "$SELF" "$mutated_reentry" >/dev/null 2>&1; echo $?)/$(bash -n "$mutated_reentry" >/dev/null 2>&1; echo $?)" '1/1/0'
cd "$tmp/m-mut/work"
mut_out="$(bash "$mutated_reentry" 2>&1)" && mut_rc=0 || mut_rc=$?
st_case 'mutated: run 3 re-enters the rerun, exits 0, takes main s side back, and step 3 COMMITS the revert — FLOWX gone' \
"$mut_rc/$(printf '%s' "$mut_out" | grep -c '^→ RERUN:' || true)/$(printf '%s' "$mut_out" | grep -c 'TAKING main.s side of gen/deferred.txt' || true)/$(git show HEAD:gen/deferred.txt | grep -c FLOWX || true)" '0/1/1/0'
cd "$here"

if [ "$st_fail" -ne 0 ]; then
printf '✗ os-regen-merge self-test: %d case(s) failed.\n' "$st_fail"
return 1
Expand Down
Loading