Coordinating a large refactor without conflict storms Jump to heading
A rename applied across four hundred files is trivial to produce and expensive to land. Every open branch that touches any of those files now conflicts on every touched line, and the conflicts are resolved by people who did not make the change, under time pressure, against work they are trying to finish. The refactor itself is usually fine; the coordination is what goes wrong. This recipe makes the sweep survivable, within conflict prevention by design.
When to use this approach Jump to heading
- A change will touch more files than anyone can review line by line.
- The change is mechanical: a rename, a signature change, an import rewrite.
- Several branches are open against the affected paths.
- A previous sweep produced a bad week and you would rather not repeat it.
- If the change touches five files, just make it; this procedure costs more than it saves at that scale.
Step 1 β Find out what is actually in flight Jump to heading
Landing a sweep blind is the root cause of most of the pain.
git fetch --prune origin
# Which open branches touch the paths the sweep will rewrite?
git for-each-ref --format='%(refname:short)' refs/remotes/origin | while read -r b; do
case "$b" in origin/main|origin/HEAD) continue ;; esac
n=$(git diff --name-only "origin/main...$b" -- src/payments/ 2>/dev/null | wc -l)
[ "$n" -gt 0 ] && printf '%3s file(s) %-45s %s\n' "$n" "$b" \
"$(git log -1 --format='%an, %ar' "$b")"
done | sort -rn # Verification: the list is the set of people to talk to before landing
git for-each-ref --format='%(refname:short)' refs/remotes/origin | wc -l Step 2 β Split the mechanical half from the semantic half Jump to heading
A sweep that also changes behaviour is unreviewable and unrebaseable. Separate them.
# 1. The semantic change, small and reviewable, landed first
git switch -c refactor/introduce-adapter origin/main
# ... add the new interface, keep the old one working ...
gh pr create --title 'refactor: introduce the payment adapter interface' # 2. The mechanical sweep, after the first has merged
git switch -c refactor/adopt-adapter origin/main
comby 'oldCall(:[args])' 'adapter.call(:[args])' -in-place -matcher .ts src/ # Verification: the sweep commit changes no behaviour
git diff --stat origin/main
npm test 2>&1 | tail -3 Landing the interface first means the sweep is purely a replacement, which makes it reviewable by re-running the tool and comparing β and makes a rebase across it mechanical rather than a judgement call.
Step 3 β Make the sweep reproducible Jump to heading
A reviewer who can regenerate the diff does not have to read it, and anyone rebasing can re-run the tool instead of resolving conflicts.
# Commit the command that produced the change, in the message
git commit -am 'refactor: adopt the payment adapter across src/
Produced mechanically with:
comby "oldCall(:[args])" "adapter.call(:[args])" -in-place -matcher .ts src/
No behavioural change. If you are rebasing across this commit, drop your
conflicting hunks and re-run the command above instead of resolving by hand.' # Verification: re-running the tool on the merged result changes nothing
comby 'oldCall(:[args])' 'adapter.call(:[args])' -in-place -matcher .ts src/
git diff --exit-code && echo "reproducible" Step 4 β Land it in a quiet window, having told people Jump to heading
# Announce with specifics: what, when, which paths, what to do
cat <<'MSG'
Sweep landing Thursday 09:00 UTC: adopting the payment adapter across src/payments.
Affected branches (please merge or park before Thursday):
feat/PAY-812-refund-window (Ada, 40 files)
fix/PAY-901-timeout (Grace, 12 files)
After it lands, rebase and re-run:
comby 'oldCall(:[args])' 'adapter.call(:[args])' -in-place -matcher .ts src/
MSG # Verification: on the day, confirm the list has shrunk
git for-each-ref --format='%(refname:short)' refs/remotes/origin | while read -r b; do
git diff --name-only "origin/main...$b" -- src/payments/ 2>/dev/null | grep -q . && echo "$b"
done SAFETY WARNING β do not land a sweep and a release in the same window. If the sweep introduces a subtle problem, bisecting through it is painful precisely because every commit in the range touches everything, and separating βthe release broke itβ from βthe sweep broke itβ becomes guesswork. Leave a clear gap, and make sure the sweep commit is identifiable by its message.
Step 5 β Give everyone a scripted rebase Jump to heading
The people rebasing did not make the change, so hand them the procedure rather than the conflicts.
#!/usr/bin/env sh
# scripts/rebase-across-sweep β drop conflicting hunks, re-run the tool.
set -eu
sweep=$(git log --format='%H' --grep='adopt the payment adapter' -1 origin/main)
git fetch origin
git rebase origin/main || true
while [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; do
git checkout --theirs -- src/payments/ 2>/dev/null || true
comby 'oldCall(:[args])' 'adapter.call(:[args])' -in-place -matcher .ts src/
git add -A && git rebase --continue || break
done
npm test # Verification: the rebased branch passes its own tests and contains the sweep
git merge-base --is-ancestor "$sweep" HEAD && echo "sweep included"
npm test 2>&1 | tail -3 Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if a branch cannot be merged or parked before the sweep? Jump to heading
Then rebase it for its author, or let them go first. A branch that is nearly finished should land before the sweep; one that is weeks from done should be rebased by whoever is landing the sweep, since they understand the change and the author does not yet.
Should the sweep be a squash or several commits? Jump to heading
One commit for the mechanical part, because it is reviewed by reproduction rather than by reading, and a single identifiable commit is what makes the rebase script and the blame-ignore entry possible. Splitting it into fifty commits multiplies the rebase work for everyone.
Does rerere help here? Jump to heading
For a person rebasing several branches across the same sweep, yes β the resolutions repeat, and rerere replays them. It does not help the first branch, and it does not substitute for re-running the tool, which is both faster and more reliable. The setup is in automating repeated conflict resolution with rerere.
Related Jump to heading
- Conflict Prevention by Design β the parent topic and the four collision shapes.
- Structuring a Codebase to Reduce Merge Conflicts β the restructures that themselves need this coordination.
- Resolving Whitespace Conflicts With Strategy Options β the formatting sweep, which is the same problem in miniature.