Conclusion first: the smallest safe change is the one you can undo in one command and re-check in one run.
Most messy-repo refactors fail on the undo, not the edit. A branch goes green at the tip. A middle commit was broken. Nobody can point at which one.
This post builds two small scripts for that gap. One rehearses the revert. One checks every commit on its own. Both run locally, on any repo, with no service dependency.
One precondition matters more than the scripts. You need a characterization suite you have already watched fail on a deliberate break. Without that, both gates measure nothing.
Why per-commit green beats one big green
A squashed branch hides the broken middle. git bisect then has nothing to search.
A revert of a forty-file commit is a second refactor. You pay the risk twice, and you pay it under time pressure.
Per-commit green converts the work into small, reversible steps. Each step is provable. Each step is undoable in one command.
Step 1 — Pin the baseline output
First, make the suite write a byte-stable report. Then hash it.
mkdir -p /tmp/refactor
./scripts/test-char.sh > /tmp/refactor/baseline.out 2>&1
sha256sum /tmp/refactor/baseline.out
Sort unordered collections before printing. Strip timestamps, temp paths, and process IDs. A hash that changes every run is noise, not evidence.
Step 2 — Rehearse the revert
Do not trust git stash pop under pressure. Script it once, then reuse it.
#!/usr/bin/env bash
# rollback_drill.sh — prove the undo restores behavior
set -euo pipefail
SUITE="${SUITE:-./scripts/test-char.sh}"
OUT=/tmp/refactor
mkdir -p "$OUT"
sum_of() { sha256sum "$1" | cut -d' ' -f1; }
"$SUITE" > "$OUT/before.out" 2>&1
before=$(sum_of "$OUT/before.out")
git stash push -u -m "refactor-wip" >/dev/null
"$SUITE" > "$OUT/reverted.out" 2>&1 || true
after=$(sum_of "$OUT/reverted.out")
if [ "$before" = "$after" ]; then
echo "REVERT OK"
else
echo "REVERT DIVERGED"
diff "$OUT/before.out" "$OUT/reverted.out" | head -40
fi
git stash pop
Read this as a drill, not a backup. It answers one question: after an undo, does the suite print identical bytes?
Two caveats bite. -u does not stash ignored files. Generated artifacts can survive the stash and skew the output. Clean them before the run.
Stash behavior is documented in the git-stash reference.
Step 3 — Gate every commit
Now check the branch one commit at a time. Use a throwaway worktree per commit.
#!/usr/bin/env bash
# green_gate.sh <base-rev> [head-rev]
set -euo pipefail
BASE="${1:?usage: green_gate.sh <base-rev> [head-rev]}"
HEAD_REV="${2:-HEAD}"
SUITE="${SUITE:-./scripts/test-char.sh}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fail=0
for rev in $(git rev-list --reverse "$BASE..$HEAD_REV"); do
wt="$TMP/$rev"
git worktree add --detach "$wt" "$rev" >/dev/null 2>&1
if ( cd "$wt" && "$SUITE" >/dev/null 2>&1 ); then
printf 'PASS %s %s\n' "${rev:0:8}" "$(git log -1 --format=%s "$rev")"
else
printf 'FAIL %s %s\n' "${rev:0:8}" "$(git log -1 --format=%s "$rev")"
fail=1
fi
git worktree remove --force "$wt"
done 2>/dev/null || true
exit "$fail"
Exit code one means the branch is not bisectable. Fix the middle commit before you merge.
The obvious limitation: every worktree needs dependencies. With heavy installs, share a vendor directory or cache, or bake a prepared image.
Worktree mechanics are in the git-worktree reference.
Step 4 — Budget the diff
Bisectable commits can still be oversized. Measure them, per commit.
git diff --numstat "$BASE..HEAD" \
| awk '{add+=$1; del+=$2; f+=1} END {printf "files=%d +%d -%d\n", f, add, del}'
A commit that touches three files is reviewable. One that touches thirty is a rewrite wearing a refactor label.
Print the same numbers inside the gate loop by swapping $BASE..HEAD for "$rev^..$rev". You get a diff size next to every PASS and FAIL line.
Step 5 — Fill the table with your own numbers
The cells below are empty on purpose. Measure your repo. Do not copy thresholds from a blog post, including this one.
| Signal | How to measure | Your target | Block the merge if |
|---|---|---|---|
| Suite wall time | time ./scripts/test-char.sh |
over budget | |
| Commit count | git rev-list --count base..HEAD |
any commit red | |
| Files per commit | git show --stat <rev> |
exceeds your limit | |
| Revert time | run rollback_drill.sh
|
output diverges | |
| Output stability | run suite 3x, compare hashes | hashes differ |
That last row is the quiet killer. Unstable output makes both gates meaningless. Fix determinism before you fix structure.
Where a free model and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Two parts of this loop are cheap to offload. One is scope review. The other is execution time.
Scope review works on the patch alone. Paste the diff, never the repo. Use a constraint-heavy prompt:
You are reviewing one patch, not a codebase.
Output exactly two lists:
1. Behavior changes this patch claims to make.
2. Symbols whose observable behavior could change but are not listed.
No code. No suggestions. No praise.
Treat the answer as a hypothesis list. The characterization suite decides what is true. A model with no repo context still catches the classic miss: a helper's return shape changed, and the commit message never said so.
Execution is the second part. MonkeyCode offers free model access and a free server option. Both are availability claims from the operator, not guarantees. Verify quotas, limits, and terms on your own account before depending on them. Do not assume permanence, and keep a local fallback.
If the free server is available, run green_gate.sh there. Your laptop keeps the editor. The gate keeps the CPU cycles. Both scripts are plain bash, so moving them back costs nothing.
One soft ask: if your suite is slow enough to break the loop, test the free server option before you buy capacity.
Limitations
- Both scripts assume a deterministic suite. Non-deterministic output invalidates both gates.
- The rollback drill covers tracked code. It does not cover migrations, queues, or remote state.
- Worktree gating multiplies install cost. Heavy dependency trees need a cache or image.
- Per-commit green conflicts with strict squash-only merge policies. Get agreement first.
- Free-tier availability can change without notice. Keep the workflow portable.
Who should not use this
Skip it if your repo has no deterministic test path. There is nothing to hash.
Skip it for irreversible data migrations. Rehearse those against a restored dump instead.
Skip it if your team merges single squashed commits. You will do the work and lose the benefit.
The short version
Pin the baseline bytes. Rehearse the undo. Gate each commit. Budget the diff.
Fill the table with your own measurements. Then make the smallest change that survives all four checks.
Top comments (0)