As part of The 12 Days of #Commitmas, we’re all practicing our git-fu. I’ve learned a bit about rebasing that I’d like to show you now.
Why are we rebasing?
The definition, from man git-rebase, is “Forward-port local commits to the updated upstream head.” Clear as mud, if you ask me. Another way of putting it is that a rebase moves a branch to a new base commit. Let’s say you create a new branch from master on the 1st of the month and make two changes, then a week goes by. A half dozen changes have been made to master and your branch doesn’t have it. You can rebase your commits against the current master rather than the original master. Underneath the hood, git is rewriting the project history to achieve all of this. All the commits are actually new. This means the checksums for commits are unique (we’ll look at why that matters in a moment).
This is great for integrating your feature against the master and maintaining a linear history. There’s another reason we might want to rebase, though – maintaining a clear history.
Let’s say that you start feature branch and start making some changes, then you commit them. You realize a few minutes later that there’s a typo, or something shows up in your tests that indicate a minor tweak is required. You make another change and commit. At the end of the day, you’re ready to submit a pull request, but your history’s a mess. The one major commit to add the feature is followed by a dozen commits with comments like “Fixed typo” and “Oops the test failed.” If you submit a PR now, you’ll likely get some flak from the project maintainer.
You can use git rebase to rewrite that history. It’s not going to fix every problem out there, but it can help in many cases.
Let’s mess up, so we can see how the history gets messed up. I’ll create a new feature branch, add a file with a typo, commit, then fix the typo and commit again.
You can see I even left a typo in my commit message. Whoops! Let’s look at the log for the differences between master and HEAD, or the current commit.
And lastly, we’ll look at the file diffs:
Now we have our new file and a messed up history.
We can now use git rebase to rewrite the history between master, where we branched from, and our current position:
You can see that we have a number of options. Pick is the default, which means you want to use the commit as is. Reword lets you edit the commit. This is the least invasive rewriting of your history, just updating the commit messages.
The next three options are more intrusive. Edit lets you use a commit but amend to it. Squash keeps the changes in commits but attaches the changes to the previous (upper) commit, allowing you to keep or edit the commit message. Fixup keeps the changes but discards the log message. This is what we want – we don’t need everyone to know we’re a poor typist, but we do want the corrected file contents.
Lastly, we have the ability to wipe out a change completely. Just delete the line entirely. As you are warned, the commit is lost from the history. If you’ve already pushed your changes to origin, as we have, you could always pull the change down, but if you’ve only made edits locally, this is a mostly permanent, destructive action with no undo option. There is a way to restore deleted commits through git-reflog, but it is not for the faint of heart. Always take care when rewriting your history.
Leave the first line as pick, change the second line to fixup, and hit ZZ to save the changes.
If we take a look at the logs and diffs, we’ll see there’s now a single commit instead of two and no evidence that we can’t type!
We’ve got one last thing to do – push our changes upstream. As I mentioned earlier, a rebase creates new commits and new checksums. Check out the commit <checksum> lines above, they’re different than the earlier checksums. If we try and push our changes to origin, we are warned that non-fast-forward updates were rejected. We’ve basically broken the chain of commits, so we need to override that. Add -f to your push statement to force it through. Since we did this on purpose, this is easy, but if you didn’t use rewrite then you’ll want to be worried.
Submit your PR
Now you can feel free to submit your pull request without being ridiculed! In fact, if you had already submitted a PR via github, bitbucket, or another service, the PR should be automatically updated with your changes. This is very helpful when someone comments on your PR and you need to make a minor tweak that doesn’t deserve its own commit.
I hope this helps you with your git-fu!
Small and easy to understand introduction to Git rebase. Once you went through this, the explanation from the man page makes sense. 😉 One thing: commits dropped via rebase are actually not lost but still accessible through the reflog. But this only works in clones which had the commits once.
Mathias, thank you for pointing this out. I’ve updated the article with a link to git-reflog!