An Easy Way To Rewrite Commit History
Sometimes, my commit history is a mess: I'll commit something I didn't mean to, then remove it in the next commit, or a few commits later. Or I'll accidentally miss something from a commit then commit it separately. Or I'll fix something in a follow-up commit, then try a different fix in another commit, and again and again.
A messy commit history isn't always a problem, but sometimes I want or need to be precise about having particular changes in particular commits. For example, at Cookpad, we deploy database migrations in isolation. After a PR that
includes a database migration is approved, we open a
separate PR to merge only the migration and schema change. This is simpler if we put the migration and schema change in a single commit. We can create a new branch,
git cherry-pick
that commit, and that's it - we've extracted the migration to its own branch ready to open the migration-only PR.
The problem
Re-writing a messy commit history can be difficult. I've spent too much time getting into frustrating tangles with
git rebase
.
Interactive rebase is really handy when I want to remove specific commits, combine (i.e. 'squash' or 'fixup') specific commits, or reorder commits. But I've hit a wall with it several times. What if I want to move changes from one commit to another? What if I want to move changes into a new commit? And combine them with other changes from other commits? 😬😅
Maybe
git rebase -i HEAD~10
does have the answers... but recently I've found an alternative that feels a lot simpler and less painful.
The solution
Suppose you have a branch with a messy commit history called
cool_feature
, and you've already opened a draft PR for that branch.
1. From the branch, merge in
main
to ensure the diff against main is up-to-date:
1 | git checkout cool_feature
|
2 | git merge main
|
2. Store the difference between
cool_feature
and
main
in a file:
1 | git diff main --patch > "full_diff_compared_with_main"
|
3. Delete the branch with the messy commit history and create a new branch with the same name:
1 | git checkout main
|
2 | git branch -D cool_feature
|
3 | git checkout -b cool_feature
|
4. Apply the changes from the file to the new
cool_feature
branch:
1 | git apply "full_diff_compared_with_main"
|
5. Now, all of the changes that were in the messy commit history are unstaged in the
cool_feature
branch. You can stage and commit them in any order and grouping you want. I use
Sourcetree to visualise what I'm staging and committing and what is still unstaged.
6. Force push the branch to update your draft PR. Only do this when you are happy with the new version of the branch:
1 | git push origin cool_feature -f
|
No more wrangling
git rebase
. No need to re-write or copy-and-paste your changes. Full flexibility to organse those changes as you want/need.