Cleaning Commit History
The oplog is the safety net.
Operating Procedure
Phase 0: Safety
Note the current operation ID with jj op log -n 1, and the branch tip's commit ID (commit, not change: the change ID survives the rewrite, the commit ID pins the old tree):
jj log -r <tip> --no-graph -T 'commit_id ++ "\n"'
Phase 1: Inventory
Determine the base branch, inventory feature-only changes with jj log -r 'trunk()..@', and note large files, generated paths, vendored code, and migrations.
Phase 2: Sea of Changes
Compute the net diff from BASE to FEATURE_BRANCH (not commit-by-commit).
Phase 3: Classify & Cluster
Cluster changes into logical buckets (strict priority):
- Generated/Vendored/Lockfiles -- isolated to dedicated commits
- Pure renames/moves -- separated from content changes
- Formatting-only (whitespace, import order, lint fixes) -- isolated
- Refactors without behavior change -- separate from logic
- Feature/Logic changes -- grouped by cohesive unit
- Tests -- co-located with their corresponding logic changes
Split when a commit mixes mechanical and semantic changes. Squash when multiple tiny edits serve the same concern.
Phase 4: Determine Commit Order
Order for buildability and minimal noise:
- Pure renames/moves
- Formatting-only sweep
- Refactors (non-behavioral)
- Schema/Migrations
- Feature/Logic in dependency order
- Tests (accompany or immediately follow their logic)
- Docs/Changelog
- Vendored/lockfile updates
Every intermediate state must build and pass tests.
Phase 5: Rebuild Commits
jj workflow:
Pushed commits are immutable under the remote_bookmarks() config, hence --ignore-immutable; rules/pr-safety.md still gates whether the rewrite may happen at all.
# Squash related changes
jj squash --from <change1> --into <change2> --ignore-immutable -m "combined message"
# Split one change into several — non-interactive, needs BOTH paths and -m.
# The named paths go to the split-out commit; the rest stays in the child.
jj split -r <change> --ignore-immutable -m "first part" path/a path/b
# Reorder
jj rebase -r <change> -o <new-parent> --ignore-immutable
# If anything goes wrong
jj op restore <before-surgery>
Phase 6: Validation
- The rebuilt tip has the original tree (no loss of intent):
jj diff --from <old-tip-commit-id> --to <new-tip> --summaryprints nothing. (--statstill prints a0 files changedline when the trees match.)
Commit Message Style
Use the format in committing-changes (conventional type(scope), 72-char subject, bullets for why).
Deliverables
- Commit Plan: Ordered list with title, scope, type, rationale, and files
- Summary Report: Changes vs original, tradeoffs, recovery instructions