I read many blogs about the infamous git rebase
command, and it took me quite some time of reading and testing, and getting help from co-workers to finally get this through my head.
Sooo, why is it so hard?
Look, I don't know. Much more can be written about it. But, here's my try at explaining the basics and getting practical knowledge on it. Maybe, just maybe, this will be the post where you will finally say "I get it".
I'm assuming you know your way around git -- committing work, merging, creating pull requests and resolving conflicts. If you don't, this post is probably not for you yet. Once you got the basics, head back here and give this a read!
Let's assume we have a branch called master
, and you have a feature branch called my-feature
. You branched off a commit in master
with an id of 'C' into your own feature branch, and you did a little work, which you committed (that would be the 'X' and 'Y' commits):
First thing to note, 'C' is your branch's 'base', because it is the point you branched off master
. This gives you a hint as to what a "re-base" might do for you.
Anyway, let's assume that now there are 3 new commits on master
(with IDs 'D', 'E', and 'F'):
As I said, we branched off 'C', now there is new work you don't have in your feature branch, so you are a bit behind. You need to bring 'D', 'E', and 'F' into your branch. This is where merging OR rebasing is usually necessary.
Merge
When you merge, you basically will append a new commit to my-feature
, that merges 'D', 'E', and 'F', from master
into your work. This is a new commit added after your work, and it's represented as the 'Z' commit in the image below:
Umm OK, so what's the problem with that?
It's fine. It just creates ugly merge commits.
Rebase
If you rebase, you clean things up by actually re-writing history, so that git will now move your branch's 'base' from 'C' as it was before, to 'F'.
There is no need for a merge commit ('Z') as we did with a merge. The history is re-written as if we had branched off 'F' all along. Here's how that looks like:
You basically accomplish the same as a merge, but you get rid of the merge commits.
Oh OK, so it makes the commit history sorta cleaner, right?
Exactly :) It also makes reviewing pull requests a little more straight-forward as it does not have merge commits.
OK great, so what is so mysterious about this?
I think it's because it has to do with the behavior of git, as you are rebasing, and after you rebase, that throws people's brains all out of wack. Let me break the process down and try to explain.
Command line? That sounds scary!
Actually, it's not. It's quite simple. In fact this is how I finally understood how to do it.
Let's go back to my cool little graphic (well I think it's cool :P) showing our repo state before merging/rebasing our feature branch:
This is the state we will start with, before rebasing.
Overview of Rebase Steps
Let's just go over every step before we actually do this. I don't want to lead you through this tunnel of steps without a handy mind-map of sorts:
master
('F'). This is done with the rebase command.Now with that said, let's get on with rebasing!
git checkout my-feature
master
:git rebase master
You will get a output similar to:
First, rewinding head to replay your work on top of it...
Applying: added X change
Using index info to reconstruct a base tree...
M index.js
Falling back to patching base and 3-way merge...
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
error: Failed to merge in the changes.
Patch failed at 0001 added X change
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
.
Woah, what is all that? Now I'm lost for sure
Before your blood pressure trends up-ward, let me help break this down for you:
First, rewinding head to replay your work on top of it...
Here git is simply telling you, it is going to do what we talked about earlier. It is going to "replay" your commits, on top of the commit from master you want to use as your new base.
Applying: added X change
Here, git is telling you what commit it is working on, by noting that commit's commit message. In my example repo, my commit message for the 'X' commit was "added X change". So here, we know git is currently working with my example 'X' commit.
Using index info to reconstruct a base tree...
M index.js
Falling back to patching base and 3-way merge...
Auto-merging index.js
I don't know line for line the meaning of each message, but it's not practically important. Here git is basically telling us, it is going to attempt to merge the changes to index.js
in this commit ('X') to our new 'base' which is 'F'
CONFLICT (content): Merge conflict in index.js
error: Failed to merge in the changes.
Patch failed at 0001 added X change
This is an important one, and it's sorta doesn't stand out much. But here git is telling you it failed to merge the changes, and it flagged index.js
as conflicted, as it says 'CONFLICT' right next to it.
Before we continue, it's important to know in what state your repo is in right now. Without getting too technical, let's just say that your repo is in the middle of the rebase process and has paused until you tell it what to do. Your feature branch has not been modified, and neither has your master branch. You are in an interim, paused, state.
At the end of the output, git offers you some options, to move on with the rebasing:
Since we DO want this commit to be applied, let's be brave, and go with option 1. Fixing the conflict.
I won't go into the details of how to fix the conflict -- like I said earlier, I will assume you know how to do this. So, let's skip past those details, and let's assume you have now resolved the conflict as you usually do, and now you are ready to continue with the rebasing.
You can't continue on with the rebase just yet. You resolved a conflict, and now have a modified file you need to stage. To stage it, do what you typically do and add the files. I usually add all files at once, so something like this works for me:
git add .
With the modification added, now you can continue by telling git to continue:
git rebase --continue
Now, git will apply the next commit ('Y'), and you will get an output similar to the previously posted.
If we run into another conflict, we simply resolve it as we already did, hit git add .
, and git rebase --continue
again. Fun stuff
When the rebase is complete, git will simply exit, which I don't really like. I wish it would congratulate us or something, but it doesn't. It just exits, all unassumingly. At any rate, you have finished rebasing :)
Your branch will now have a new base of 'F':
You are now ready to push your work!
git push
Now, if you have never pushed this branch to the remote counterpart, you are done!
But, if you already had pushed this branch before rebasing. You got another step. Here's the output you probably get now:
! [rejected] my-feature -> my-feature (non-fast-forward)
error: failed to push some refs to 'https://somerepo/my-project.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Our push was rejected :( What gives!
Well, we re-wrote the history on our branch locally with our rebase, now the remote history is completely different.
OK, well at least git helps us with some hints. I will git pull
then and...
STOP! Let's not git pull
. In fact, never git pull
after rebasing, you will be in for a world of hurt should you do that.
But, but, git says...
I know, I know, git says. Git is trying to help you, but git is way off here. It doesn't realize you re-wrote your history with a rebase, and it wants to try and merge your remote un-rebased version of the branch, with your shiny new rebased branch. It will not be pretty to try and do that. Just ignore the hint. Here is what you do:
git push -f
-f
forces the push. This means it doesn't care about what's in the remote right now. It will replace it with what you are pushing -- your rebased work.
-f? I don't like that. I'm forcing the push? It feels wrong. Shouldn't it merge nicely without forcing it?
It won't force nicely, your remote branch and the local one now look drastically different in the commit structure. Plus there is no point to try and merge them. We know the local is perfectly rebased. We just want the remote to look just like that. So in this case, forcing it up, is perfectly fine.
I should point out that, if you are pushing to a branch that others are also pushing to, you should avoid rebasing at all, that is unless you are absolutely sure that forcing your rebased branch will not wipe out anyone else's work. I don't recommend working with someone on the same branch anyway. I have never worked too much this way. It causes problems. It's best if everyone has their own warm fuzzy branch just for them.
Again, here is how our rebased branch would look like:
Good luck! Done.