| name | preferences-git-history-cleanup |
| description | Git history cleanup patterns for rewriting, squashing, and reorganizing commit history. Load when cleaning up branch history before merge. |
git history cleanup
scope
This skill covers history refinement in git-native and GitButler modes (git rebase -i, git revise, but move/but squash/but reword, GIT_SEQUENCE_EDITOR patterns).
For jj-mode history refinement (jj rebase, jj squash, jj split, jj abandon, jj describe -r), see ~/.claude/skills/jj-history-cleanup/SKILL.md.
In a jj-colocated repository, do not apply any git rebase -i or git revise recipe from this file to the working copy; jj history refinement and the @-stays-empty-[wip] development-join invariant are owned by ~/.claude/skills/jj-history-cleanup/SKILL.md and ~/.claude/skills/jj-version-control/diamond-workflow.md.
Mode detection is described in ~/.claude/skills/preferences-git-version-control/SKILL.md §"Commit behavior override".
purpose
Transform experimental development history into a clean, reviewable commit sequence where:
- Each commit is atomic: contains one logical change that builds/tests successfully
- Commits are logically ordered: dependencies come before dependents, related changes are grouped
- Intermediate commits are removed: no "WIP", "fix typo", "oops", or checkpoint commits
- Each commit message follows conventional commit format and accurately describes its diff
This prepares branches for PR review by creating a clear narrative of what changed and why.
GitButler mode
When gitbutler/workspace is checked out, the but CLI provides native history editing commands that replace git rebase -i.
The git-rebase patterns in the rest of this skill apply only to git-native mode.
| Git operation | but equivalent |
|---|
git rebase -i (reorder) | but move <commit> <target-commit> (within branch) or but move <commit> <branch> (cross-stack) |
git rebase -i (squash) | but squash <source-commit> <target-commit> |
git rebase -i (reword) | but reword <commit> -m "new message" |
git rebase -i (edit) | but uncommit <commit> then re-commit with changes |
git commit --amend | but amend <file-id> <commit-id> |
git revise | Same but commands above --- but subsumes git revise functionality |
git rebase --abort | but undo (reverts last operation) |
All but mutation commands accept --status-after to display the updated workspace state.
Always use fresh CLI IDs from but status -fv before each operation, as IDs change after every mutation.
The git-native patterns below (interactive rebase, GIT_SEQUENCE_EDITOR, git revise) remain the correct approach when working in repositories not managed by GitButler.
principle (git-native mode)
Never trigger interactive editors. All operations use GIT_SEQUENCE_EDITOR and GIT_EDITOR environment variables with non-interactive commands.
core technique
GIT_SEQUENCE_EDITOR="command-to-edit-todo" git rebase -i <base>
The rebase todo file has one line per commit: <action> <hash> <message>
operations
reorder commits
Use sed/awk to rearrange lines in the todo file:
GIT_SEQUENCE_EDITOR="awk 'NR==3 {saved=\$0; next} NR==1 {print saved} {print}' > /tmp/rebase-todo && cat /tmp/rebase-todo" git rebase -i HEAD~5
GIT_SEQUENCE_EDITOR="tac" git rebase -i HEAD~3
GIT_SEQUENCE_EDITOR="awk '{lines[NR]=\$0} END {for (i in order) print lines[order[i]]}'" git rebase -i HEAD~N
squash/fixup commits
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/squash/'" git rebase -i HEAD~5
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/fixup/'" git rebase -i HEAD~5
GIT_SEQUENCE_EDITOR="sed -i.bak '2,\$s/^pick/squash/'" git rebase -i HEAD~5
drop commits
GIT_SEQUENCE_EDITOR="sed -i.bak 'Nd'" git rebase -i HEAD~5
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/drop/'" git rebase -i HEAD~5
reword commit messages
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/reword/'" \
GIT_EDITOR="echo 'new message' >" \
git rebase -i HEAD~5
edit commit content
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/edit/'" git rebase -i HEAD~5
git add <files>
git commit --amend --no-edit
git rebase --continue
split commits
GIT_SEQUENCE_EDITOR="sed -i.bak 'Ns/^pick/edit/'" git rebase -i HEAD~5
git reset HEAD^
git add <files-for-first-commit>
git commit -m "first part"
git add <files-for-second-commit>
git commit -m "second part"
git rebase --continue
robust patterns
use temporary files for complex edits
#!/bin/bash
awk '{lines[NR]=$0} END {
# Print in desired order
print lines[3]
print lines[1]
print lines[2]
}' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
chmod +x reorder-script.sh
GIT_SEQUENCE_EDITOR="./reorder-script.sh" git rebase -i HEAD~3
multi-step workflow
For complex history rewrites:
- First pass: reorder commits
- Second pass: squash/fixup related commits
- Third pass: reword messages
- Final pass: test and verify
Run separate rebase operations rather than one complex edit.
handle conflicts
git status
git add <resolved-files>
git rebase --continue
git rebase --abort
complete example
Clean up 5 commits: reorder, squash 2 & 3, drop 4, reword 5
cat > /tmp/rebase-edit.sh << 'EOF'
awk '{lines[NR]=$0} END {
print lines[1]
gsub(/^pick/, "squash", lines[3])
print lines[3]
gsub(/^pick/, "drop", lines[4])
print lines[4]
gsub(/^pick/, "reword", lines[5])
print lines[5]
print lines[2]
}' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
EOF
chmod +x /tmp/rebase-edit.sh
GIT_SEQUENCE_EDITOR="/tmp/rebase-edit.sh" \
GIT_EDITOR="echo 'New message for commit 5' >" \
git rebase -i HEAD~5
verification
After any history rewrite:
git log --oneline --graph -n 10
git diff <original-branch>..HEAD
git revise (preferred for simple cases)
git revise is faster than git rebase (in-memory, doesn't touch working directory) and well-suited for linear history cleanup.
key differences from git rebase
- No
drop command: all commits must be accounted for (use fixup to absorb unwanted commits)
- Todo format:
<action> <hash> <message> (simpler than rebase)
- Single-commit reword:
git revise -m <target> -m "new message" (no editor needed)
- Undo with
git reset @{1} (single reflog entry)
non-interactive patterns
git revise --no-gpg-sign <target-hash> -m "new commit message"
cat > /tmp/revise-editor.sh << 'SCRIPT'
cat > "$1" << 'TODO'
pick <hash1> first commit message
fixup <hash2> fixup for first
pick <hash3> second commit message
fixup <hash4> fixup for second
TODO
SCRIPT
chmod +x /tmp/revise-editor.sh
GIT_SEQUENCE_EDITOR="/tmp/revise-editor.sh" git revise -i --no-gpg-sign <base>^
multi-pass workflow (recommended)
Complex rewrites are safer as separate operations:
- Structure pass: reorder and fixup/squash related commits
- Message pass: reword each commit with
git revise -m <hash> -m "message"
- Verify pass:
git log --oneline, git show --stat <hash> for each commit
gpg signing
When commit.gpgSign is configured but key unavailable, use --no-gpg-sign:
git revise --no-gpg-sign -i <base>
git revise --no-gpg-sign <hash> -m "message"
fixup ordering
Fixup commits must immediately follow their target:
pick abc123 feature implementation
fixup def456 fix typo in feature
fixup def456 fix typo in feature
pick abc123 feature implementation
absorbing removal + restoration commits
When a commit removes something and a later commit restores it (net effect: just comments/modifications), squash them:
cat > /tmp/revise-editor.sh << 'SCRIPT'
cat > "$1" << 'TODO'
pick <hash-A> remove X
fixup <hash-B> restore X with comment
fixup <hash-C> fix comment
TODO
SCRIPT
GIT_SEQUENCE_EDITOR="/tmp/revise-editor.sh" git revise -i --no-gpg-sign <hash-A>^
git revise --no-gpg-sign <new-hash> -m "add comment explaining X"
when to use which tool
| Scenario | Tool | Reason |
|---|
| Simple linear cleanup | git revise | Faster, in-memory |
| Reword single commit | git revise -m | No editor needed |
| Complex reordering | git rebase -i | More flexible |
| Conflicts expected | git rebase -i | Better conflict UX |
| exec/break needed | git rebase -i | revise lacks these |
key reminders
- Always work on a backup branch first
- Use
-i.bak with sed for safety (creates backup)
- Test rebase scripts on throwaway branches
- Check
git rebase --abort or git reset @{1} is available if things go wrong
- For AI agents: create temporary shell scripts rather than inline complex sed/awk
- Never use bare
git rebase -i or git revise -i without GIT_SEQUENCE_EDITOR set
- Use
--no-gpg-sign when GPG key is unavailable