A refactor is only as safe as its smallest revertible step. If nobody can undo your change with one command, the characterization suite is decoration.
This post starts after the tests are green. It covers cutting the change, ordering the cuts, and proving each commit stands alone.
The contract: green and revertible at every commit
Two properties per commit. The suite is green. git revert <sha> restores green with no hand edits.
Most teams check the first property only at the branch tip. That is not enough. A broken middle commit blocks git bisect and forces a full rollback later.
Step 1 — Print a per-commit ledger
Run this before you ask for review.
#!/usr/bin/env bash # ledger.sh — churn and API surface per commit set -euo pipefail BASE="${1:?usage: ledger.sh <base-ref>}" git log --reverse --format='%H %s' "$BASE"..HEAD | while read -r sha subject; do files=$(git show --pretty='' --name-only "$sha" | grep -c . || true) stats=$(git show --pretty='' --numstat "$sha" | awk '{a+=$1; d+=$2} END{print a+0" +"d+0}') surface=$(git show --pretty='' -U0 "$sha" \ | grep -E '^\+[^+].*(export |public |func [A-Z]|def [a-z_]+\()' | wc -l | tr -d ' ') printf '%-9s files=%-3s lines=%-10s surface+=%-3s %s\n' \ "${sha:0:8}" "$files" "$stats" "$surface" "$subject" done The surface+ column counts added lines that look like public API. That number matters more than total churn. A 400-line commit inside one private function is safer than a 20-line commit that changes a signature.
Step 2 — Apply a diff budget
Set the budget before you read the diff, not after. These are my defaults as a starting policy. They are not measurements from a study.
| Commit kind | Files | Changed lines | API surface+ | Revert target |
|---|---|---|---|---|
| Move or rename | <= 3 | <= 150 | 0 | under 60 s |
| Extract function | <= 2 | <= 80 | 0 to 1 | under 60 s |
| Signature change | <= 5 | <= 120 | >= 1 | under 5 min |
| Delete old path | <= 2 | any | 0 | under 60 s |
One rule overrides the table. Never combine a signature change with a move. If the revert must touch two concerns, it is two commits.
Step 3 — Order the cuts seam-first
Order matters more than size. Use this sequence.
- Extract the logic into a new function while the old name still calls it.
- Change call sites one file per commit.
- Move the file only when no logic changed in that commit.
- Delete the old path in a commit that only deletes.
Pure moves produce diffs git can follow. Logic and location changes in one commit defeat rename detection. Split them and reviewers read less.
Step 4 — Run the revert drill
Do not test revertibility at the branch tip. Cut a branch at the commit itself.
drill() { sha="$1"; short=$(git rev-parse --short "$sha") git switch -q --detach "$sha" t0=$(date +%s) if git revert --no-edit "$sha" >/dev/null 2>&1; then if ./run_tests.sh >/dev/null 2>&1; then echo "REVERTIBLE $short $(( $(date +%s) - t0 ))s" else echo "RED-ON-REVERT $short — consumed state from earlier commits" fi else echo "CONFLICT $short — too coupled to remove alone" fi git switch -q - } RED-ON-REVERT is the useful failure. The commit established something later code needs, without a clean seam. Reorder it, or add the adapter that makes removal safe.
Run the drill on every commit in the branch. Expect at least one red result on a first pass.
Step 5 — Walk the branch green
A revertible commit can still be red. Check the whole sequence.
BASE=origin/main git log --reverse --format='%H' "$BASE"..HEAD | while read -r sha; do git switch -q --detach "$sha" if ./run_tests.sh >/dev/null 2>&1; then echo "GREEN $(git rev-parse --short "$sha")" else echo "RED $(git rev-parse --short "$sha")" fi done git switch -q - This catches the "passes only at the tip" case. That case hides a commit that cannot ship alone.
Two practical notes. Cache dependencies before the loop, or each checkout pays install cost. Skip commits that predate a dependency bump and record the skip in the PR description.
Where a free model and a free server fit
Two mechanical jobs here. First, proposing the split: "divide this diff into commits that each touch one concern" is a text task, and a free model gives a usable first draft to review. Second, the green walk across many commits is a long unattended loop, which is exactly what the free server option is for.
MonkeyCode's free model access and free server option cover both without a spend approval. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Keep the model's output as a suggestion. Verify each proposed commit with the drill. Do not let it choose the order, because ordering depends on your dependency graph, which the model does not see.
Limitations
- Characterization tests pin observed behavior only. Untested paths can still break silently.
- The green walk assumes a fast, deterministic suite. If it takes 20 minutes per run, sample the commits instead.
- Small commits cost review time. On a two-person repo, four commits may be worse than two medium ones.
- Clean revert is not correct revert. Caches, migrations, and deployed config survive a
git revert. - Revert speed does not measure design quality. It measures coupling, which is what you asked.
Do not use this on a repo with no working test command. Fix that first. Do not use it when the "refactor" is a feature change wearing a refactor label.
What to record
Put three numbers in the pull request. Commit count, longest revert time, count of red commits found and fixed.
Those numbers survive review. "Looks cleaner" does not. Try the drill on your current branch and post the commit that failed it.
Tags: choose what fits your repo. The method does not change.

