In this tutorial, we’ll learn how to convert a git branch from many commits into a single commit before merging it. There’s one key command to learn:
git rebase -i
Let’s assume that you have a branch,
feature/foo, that was branched off of
develop. The git log might look something like this:
* baec937 (HEAD -> feature/foo) remove TODO comment * 4fb174d add spec for foo model * bb84984 update the foo serializers * 3b0c937 a small documentation change * c528a03 (develop) a bug fix * 1d9cf0c a cool new feature * a5ce4cc (master) initial commit
In the simplest case, all of our
feature/foo commits are newer than the most recent
Squashing - the garden path version
We’re done reviewing our branch and are ready to merge it.
Start the interactive rebase
> git rebase -i develop
and you’ll see something like this in your editor:
pick 3b0c937 a small documentation change pick bb84984 update the foo serializers pick 4fb174d add spec for foo model pick baec937 remove TODO comment # Rebase c528a03..baec937 onto c528a03 (4 command(s)) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # ... (additional lines edited out) ...
Some things to note about this list:
- The oldest commit is at the top of the list. The newest is at the bottom (
baec937in this example).
- Editing this file allows you to rewrite history in a variety of powerful ways.
- The top commit of
developdoes not appear in this list. We’re rebasing onto
develop, meaning we’ll leave
developalone. The result of this rebase will affect only the world immediately after
Edit the rebase file
We want to
squash all of the commits below (in other words, newer than)
3b0c937. To do that, change every
pick to a
s for short) except for the top one, which we’ll leave alone. Your file should look like this before you save it:
pick 3b0c937 a small documentation change squash bb84984 update the foo serializers squash 4fb174d add spec for foo model squash baec937 remove TODO comment # Rebase c528a03..baec937 onto c528a03 (4 command(s)) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # ... (additional lines edited out) ...
Name the newly squashed commit
Once you save the rebase file, you’ll be asked to name the new commit created by the squash. Here is where we will include the story number and other relevant information.
By default the file looks like this:
# This is a combination of 4 commits. # The first commit's message is: a small documentation change # This is the 2nd commit message: update the foo serializers # This is the 3rd commit message: add spec for foo model # This is the 4th commit message: remove TODO comment # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Mon Aug 29 13:32:13 2016 -0500 # # interactive rebase in progress; onto c528a03 # Last commands done (4 commands done): # s 4fb174d add spec for foo model # s baec937 remove TODO comment # No commands remaining. # You are currently editing a commit while rebasing branch 'feature/foo' on 'c528a03'.
Edit it to look like this:
Adds feature Foo Added new feature Foo which can do all kinds of good stuff. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Mon Aug 29 13:32:13 2016 -0500
Finish the rebase
Save the rebase file, and our git history will look like this:
* 9f9b750 (HEAD -> feature/foo) Adds feature foo * c528a03 (develop) a bug fix * 1d9cf0c a cool new feature * a5ce4cc (master) initial commit
Notice that we’re only one commit ahead of
develop instead of several!
Merge the branch into develop
Now that we’ve squashed our feature branch down to a single commit, we can merge it into
develop as per usual. Nice!
> git checkout develop > git merge feature/foo > git log --graph --oneline --decorate * 9f9b750 (HEAD -> develop, feature/foo) Add feature foo * c528a03 a bug fix * 1d9cf0c a cool new feature * a5ce4cc (master) initial commit
Look how clean that is. Aw yeah.
Squashing when there are conflicts
In the real world,
develop is probably going to change between when you start and finish your branch. Let’s see how to deal with that in the rebase workflow.
Don’t merge develop into your branch, rebase over develop
Normally we like to
git merge develop into our branch frequently to stay up to date. Let’s use
rebase instead, in order to keep all of our branch’s commits on top, rather than interspersed.
> git checkout develop # only applicable if using a remote > git fetch origin # only applicable if using a remote > git merge origin/develop # only applicable if using a remote > git checkout feature/foo > git rebase develop
Note that here, we’re just using regular rebase, not interactive, so there’s no
By rebasing over
develop rather than reverse merging it, we’re making life a lot easier for ourselves when we do the final squash.
Resolving conflicts mid-rebase
At some point, you will run into conflicts between branches that you have to resolve. When it happens, you’ll see a message like this:
First, rewinding head to replay your work on top of it... Applying: removes TODO comment Using index info to reconstruct a base tree... M README.md Falling back to patching base and 3-way merge... Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: Failed to merge in the changes. Patch failed at 0001 removes TODO comment The copy of the patch that failed is found in: .git/rebase-apply/patch When you have resolved this problem, run "git rebase --continue". If you prefer to skip this patch, run "git rebase --skip" instead. To check out the original branch and stop rebasing, run "git rebase --abort".
You can edit the conflicting files just like a regular merge conflict, with one important exception:
DO NOT COMMIT when you’ve resolved the conflict. Instead,
git add the changes and then say
> git rebase --continue
Getting out of trouble
Rebasing can be tricky sometimes. Here are a couple of strategies to use when you get into trouble.
If you get into a weird state mid-rebase and want to bail out entirely, you can
> git rebase --abort
The git reflog is the ultimate source of a repo’s history on your machine. It even keeps track of how things were before an attempted rebase. It’s worth taking the time to read up on the reflog, so that you have more powerful tools for resolving rebase-related problems.
When rebasing, you will sometimes need to decide multiple times in a row which branch’s code to keep. You may want to look into git rerere to make this easier.
If you use git-flow, you can say
git-flow feature finish feature/foo --squash (or
-S instead of
--squash) to accomplish basically all of the above. I’m not super familiar with git-flow, so if there’s some nuance to this that I missed, please let me know and I’ll update this document.
Questions / comments
If you have any questions or comments, feel free to leave them right here in this gist! This is a living document, so I’ll be incorporating feedback as I get it.