One of the most important (and confusing) git features in my new job was rebasing. Looking back now, the worst part was not finding a clear beginner's guide. So for the past me, and any future devs like the past me, this intro is for you!
Another small note to the past me from college: use Jekyll for your personal website. Don't waste your meager college budget on hosting a WordPress site...
Anyway, please enjoy! This article assumes you have basic knowledge of git and version control, and love cupcakes. You'll see why.
First, why would I Need to Rebase Something?
Let's say you're a junior dev starting at a cupcake store called Cupid's Cupcakes. It does lots of online selling, and has many experienced devs constantly improving it. You're brought in to mostly work on the front-end.
Your first assignment is updating a card component. When people look for cupcakes to buy, each is in one of these cards. So you go to the repo, pull the most recent version of the master branch, create a new branch from that one, and get to work!
A few commits later, you're all set. The card looks nicer, all the tests pass, and you've even improved the mobile layout. All that's left is to merge your feature branch back into master branch so it goes live!
But wait a moment!
Unsurprisingly, other people were working on the site while you were making this card component.
- One developer changed the navigation
- One adjusted the database fields to remove unneeded info
- Another added extra info about each cupcake
- Someone else secretly embezzled money through the store's bank records
All these changes make you worry. What if someone merged a change that affects or overlaps with the ones you made? It could lead to bugs in the cupcake website! If you look at the different changes made, one does! (Another change should be reported to the police, but that's actually less important). Is there a safe way to merge your changes without risking any conflicts, and missing out on all the other changes made?
Situations like these are a big example of when you'd want to rebase.
What are the details of Rebasing?
Let's say when you created your branch off of the master branch, the master branch was on commit #1. Every commit in your branch was layered on top of commit #1. When you're ready to merge your branch, you discover other developers made changes and the most recent commit is commit #5.
Rebasing is taking all your branch's commits and adding them on top of commit #5 instead of commit #1. If you consider commit #1 as the "base" of your branch, you're changing that base to the most recent one, commit #5. Hence why it's called rebasing!
Okay, so HOW do I Rebase something?
So you've got this great card component for Cupid's Cupcakes. Now that you know what a rebase is, let's look at the how in more detail.
First, make sure you have the most up-to-date version of the branch you're rebasing on. Let's keep assuming it's the master branch in this example. Run git checkout master
to, y'know, check it out, and then run git pull
to get the most recent version. Then checkout your branch again - here's it'd be with git checkout updated-card
or something similar.
A straightforward rebase has a pretty simple command structure: git rebase <branch>
. branch
is the one you're rebasing off of. So here you'd run git rebase master
. Assuming there's no conflicts, that's all the rebase needs!
The rebase itself technically removes your old commits and makes new commits identical to them, rewriting the repo's commit history. That means pushing the rebase to the remote repo will need some extra juice. Using git push --force
will do the trick fine, but a safer option is git push --force-with-lease
. The latter will alert you of any upstream changes you hadn't noticed and prevent the push. This way you avoid overwriting anyone else's work, so it's the safer option.
With all that, your rebase is now complete! However, rebases won't always go so smoothly...
How do I Handle Rebase Conflicts?
Remember how we worried our new card would conflict with someone else's changes? Turns out, one does! One developer added extra info onto the new cupcake card, such as calorie count or how many elves it takes to make it at night. The updated markups from both sets of change are in the same lines - this means the rebase can't happen automatically. Git won't know which parts of the changes to keep and which to remove. It must be resolved!
Thankfully, git makes this very easy. During the rebase, git adds each commit onto the new base one by one. If it reaches a commit with a conflict, it'll pause the rebase and resume once it's fixed.
If you've dealt with merge conflicts before, rebase conflicts are handled essentially the same way. Running git status
will tell you where the conflicts are, and the two conflicting sections of code will be next to each other so you can decide how to fix them.
Once everything is fixed, add
and commit
the changes like you would a normal merge conflict. Then run git rebase --continue
so git can rebase the rest of your commits. It'll pause for any more conflicts, and once they're set you just need to push --force-with-lease
.
There's two lesser-used options you could also use. One is git rebase --abort
, which would bring you back to before you started the rebase. It's useful for unexpected conflicts that you can't rush a decision for. Another is git rebase --skip
, which skips over the commit causing the conflict altogether. Unless it's an unneeded commit and you're feeling lazy, you likely won't use it much.
Can I Get Extra Control of the Rebase Process?
As you can see, Git Rebasing is powerful. As certain trademarked heroes have told us, great power brings great responsibility. Thankfully, rebasing lets you take more control over the process.
To do so, just include the --interactive
, or -i
, flag in your command! For our cupcake rebase, it'd be git rebase -i master
.
This shows a list of all the commits you're about to rebase. You can choose which ones to stop and edit, which ones to skip, and even merge some together! I won't go into too much detail here, but this article is a good starting point to read more.
Also be warned: all this is done through a VIM interface, which is very useful and even more difficult to learn. To run the rebase as is, press esc + : + w + q + enter
. I'll leave tutorials explaining what that means, and the rest of VIM, to someone else.
Lastly, a useful Rebasing trick: Autosquashing!
In interactive rebases, one useful trick I want to share is autosquashing commits. This is merging two commits together and renaming the new one. If you have lots of small, similar commits that could be combined, or need to amend an older commit, this is a great trick. Rebasing makes it easy to do this with the wonderful "autosquash."
Let's say we've got five commits in our branch for Cupid Cupcake's new card. We then realize we missed a small detail in the 2nd commit! If we want to keep our commit history clean, we need to merge this change into the second commit.
We do this using a fixup
commit, which is done like so:
- Make and stage your changes.
- Get the ID of the commit you want to add them too. for our example, let's say it's 123456ABCD. You can find this by running
git log
. - The formula for a fixup commit is
git commit --fixup <commitID>
. In this case, we'd usegit commit --fixup 123456ABCD
. This will commit the changes as normal, but the name will be the same as our second commit, and have "fixup!" prepended. - Next we run a rebase, but it's important to include the right parameters! We'd run
git rebase -i --autosquash master
. - We'll see the normal page for an interactive rebase, with all our commits listed in the VIM interface. But our fixup commit will be right below our second commit, set up to be merged into it automatically!
- Run the rebase and your commits will combine automatically! If there's a conflict it'll ask you to resolve it like normal, and the rebase will continue.
This is incredibly useful for updating commits without polluting the history with lots of "small fix" changes that make it tedious and confusing. The best commit histories tell an understandable story. Autosquashing keeps it from getting overwhelmed by obnoxious footnotes. So make use of it!
Enjoy your Cupcakes!
Hopefully this post helped explain the basics of git rebasing, what it does, and how to start making use of it. Finally understanding rebases myself has made them a frequent, essential tool for my job. I hope other junior devs can do the same without some of the frustrations/post-push panic attacks I had in the process.
After all, we all want to help more people buy and eat cupcakes, don't we? Of course! Git Rebasing, like all good things, brings us closer to that goal.
Top comments (58)
That image halfway down was a game changer. Thanks Max!
Glad you liked it! Thinking up that cartoon actually led me to write this entire post haha
Great article and I love your drawings (and cupcakes :) )
I've had many arguments before on the concept of rebase from master vs merge into master as a first step, mainly due to the commit timeline continuity. I'm 100% rebase then merge.
I insist that my team (data scientists who are git newbies) rebase from (an immediately re-pulled) master - this way, any conflicts are kept in their own branches and not master, which is kept pure. Only when their branch has been rebased, conflicts resolved, tested and checked for a rebase again do we merge their code into master.
After a few time consuming sessions de-conflicting master they're starting to see the benefit in doing it the way I impose :D
Autosquash is my "one thing I've learned today" - thanks :)
Thank you! Glad you liked the drawings, and honored I could be the daily thing something learned today. I can't ask for much more than that :D (well I suppose I could ask for my laptop battery replacement to be covered by my warranty, but I try to stay realistic haha)
Very nice explanation. I will definitely use this one next time I have to explain rebase ;-).
However, what I was missing is a word on the trouble you can cause when rebasing already pushed commits.
This has its reason. If there are several people working on the same branch, you are making changes that have not been pushed yet incompatible with the upstream branch for everyone else working on the branch. This means once you do this, everyone else has to pull with --force and reintegrate their changes (in the best case by rebasing themselves, in the worst case with much more complicated and tedius measures or having to recover "lost" commits from reflog if someone pulls with --force without care).
In the given example that clearly aims towards pull requests and single user WIP branches you might not get struck by this very often, but this can be a real problem.
Thus, my recommendation is always to use rebase as much as possible as long as you are not breaking the history for anyone else. This normally means that you shouldn't rebase commits you have already pushed or - if you do nevertheless - know exactly what the consequences are in your case and get consent of other people working on the branch if needed.
Very this. A simpler rule: just never use
git push --force*
except you're the one and only working on the branch and you're ready for consequences. I prefer periodical merges with master if the branch is already pushed, the history doesn't look as good as with rebase but OTOH it won't be messed up. Any usage of--force
(both push and pull) can unexpectedly remove commits you don't want to be removed so just don't do it. Fortunately, it's relatively easy to restore them, read aboutgit reflog
.That's pretty much the essence of what I wanted to say. Just wanted to point out that it is not the end of the world if you have to push with --force, you just need to know about the consequences and if it gets you into trouble.
But in general, I agree. In collaboration branches, you should not need --force in your all da work. I also prefer merging master (or an other parent branch) regularly to the topic branch and merging when ready with --squash and a proper message to remove WIP commits, but this is a topic that involves a lot of personal taste and philosophy, so discussion is difficult about this.
Thanks for the extra perspective! I work on many solo branch PRs myself so this kind of issue with rebases hasn't hit me that often. But there have been one or two moments where pushing rebased commits have caused issues with co-workers, and better communication with them was always how they would've been avoided.
A caviot to this, as long as commits aren't edited, both parties can rebase a shared branch and git will identify the existing commits.
Another incredibly practical use of rebase is the following:
You have to start a new feature "CupcakePictures" that is based on another feature "CupcakeColoring" that your colleague is still working on (or that hangs in a pull request), i.e. this is about working in parallel.
Just branch off from that non-merged incomplete feature branch "CupcakeColoring" of your colleague and get working. Whenever he produces more stuff that you need, you just rebase onto his latest commit.
When his feature has been merged to master, you rebase on master too, and the history will - once you're done - wonderfully show how "CupcakePictures" was started after "CupcakeColoring" as the dependency actually required it (the parallel work will not show up and no cluttering will take place, like you would have by constantly merging his stuff).
This is also another good example of rebasing to prevent conflict! Thank you for sharing this. I haven't done any branch work off another person's branch work yet, so I appreciate the extra perspective. Also love how you kept with the cupcake theme :)
Note that if you don’t want to learn all the syntax required for autosquashing, or lookup and copy/paste the commit IDs, you can just commit the fix normally, and when you rebase interactively, manually move the line next to the commit to fix and change it to squash. It’s not necessarily more work.
That is very true. I didn't go into too much detail about what someone can do about interactive rebasing, but since I went into detail about autosquashing, I probably should've mentioned it's basically a shortcut for what can also be done in a normal rebase. But since I've found it to be simpler and require less VIM knowledge I wanted to highlight it above the other options. Thank you for pointing this out though!
I also agree about about trying some different "Explain X with Cupcakes" posts. Maybe I can also try it with other pastries, like pies and brownies!
I forgot to mention that I do all my interactive git stuff with nano instead of VIM. I find it sufficient for this kind of work.
This can be as easy as putting
export VISUAL=nano
in your .profile .There's probably also a setting you can use with
git config
.I export
EDITOR=nano
(never heard ofVISUAL
before,) and in my.zshrc
, but yes.Since then I’ve switched to vim, though. I learned to do just what I did in nano (navigate with the arrow keys, type, cut and paste entire lines, search, save, and quit,) and the rest I’m learning as the needs occur. The first one being
:! sudo tee %
to save when you forgot to open the file as root. :-DHoly mao, using rebase for merge and wasting time to refix the same conflicts, and teaching other devs to do the same, that's a no go for me.
If you have to write 2 pages on a simple task maybe something is wrong, git merge is for merges, rebase and push force is for playing with fire and rescue the lost kittens :))
It's common sense if you reached - - force you screwed something.
Personally, I think rebase is an exceptionally useful tool however it's potential for misuse is great.
From my point of view, rebase should only be used on stuff that hasn't been pushed to a [shared] remote. There is a useful article here - daolf.com/posts/git-series-part-2/ - about that.
Consider the case where multiple developers are working on a single feature. The feature is in branch feature/j207, which was taken from branch develop. Potentially each developer could have their own branch off feature/j207, do some work, then do merges etc, but they might actually just all work directly from feature/j207, which is where rebase comes in. Let's just consider two developers for the moment, Bob and Jim.
1) Bob and Jim both clone the repo and checkout -b feature/j207
2) Bob makes a change and commits locally
3) Jim makes a change and commits locally
4) Jim makes a second change and commits locally
5) Bob makes a second change and commits locally
6) Bob pushes to the remote branch
7) Jim makes a third change and commits locally
8) Jim tries to push to the remote branch, but it's rejected due to Bob's changes having gone in
At this point Jim has two main options:
1) merge from the remote, e.g. use git pull, auto-merge, sort out conflicts across multiple commits, commit and push
2) rebase from the remote, e.g. use git pull --rebase, sort out conflicts on a commit-by-commit basis with git rebase --continue then, once complete, push
My preference is for that latter.
First of all, you avoid a merge commit that says (assuming your developers are what would appear typical in my experience) "Merge branch feature/j207 on blah into feature/j207". How useful is is it to know that you merged from a branch into another branch, in some unknown place, with the same name?
Secondly, you make the history of the branch linear, rather than having multiple branches, with the same name, being displayed on the graphical representation. As other people have mentioned, doing it this way also makes git bisect work better (I believe) when trying to track down bugs that may have been introduced during this process.
In this way you're only rewriting your local history that affects no one, other than yourself.
What's not so acceptable is rebasing feature/j207 from the current state of develop then pushing it. There's no reason why this should NEVER be done, but it's got to be done carefully, and not while multiple people are working on feature/j207.
So, in summary, there are circumstances where rebase is appropriate, and others where you need to think carefully about it.
Well, rebase is a nice tool to use, specially if you haven't pushed your branch.
About conflicts, in the rare cases where they would be recurrent, git rerere can help a lot.
Not entirely true, just uses your system default visual editor. Which is $VISUAL || $EDITOR ||
vi
IIRC.Git has a built in tool to help ease the git churn during merge conflict resolution. Anytime a command pauses because of a conflict you can run
git mergetool
and it will open each conflicting file in the default conflict resolution application. See git-mergetool man page for more info.Also the default editor that git uses is determined by your environment variable
$VISUAL
. It does not need to be Vim. For non-technical editing you could use nano or even your favorite GUI editor (millage may vary). For more information on this check out ${VISUAL}ize the future.How can one apply these concepts to a Git Flow workflow? I'm a huge fan of Git Flow and we use it a ton at my office but usually when a feature is being finished, it's just a simple merge with the
no-ff
flag, so we have to do the checkout, pull, and deal with merge conflicts (when they exist) then anyway. I definitely see the utility of this command, but I don't know if it's super applicable in our context.Also, I think it’s time to stop with these “Explain X like I’m five” posts, they’re out of fashion, and switch to “Explain X with cupcakes.”