I use git’s rebase command daily, it’s an invaluable tool for maintaining a clean and sane Git history. However, most people find it difficult to understand, or use it incorrectly, as it’s not the clearest command to use.
The first thing to understand, is that rebasing typically refers to two different (but similar) operations:
- “Rebasing a branch” is the most common use of rebase, and refers to pulling changes from an upstream branch (like
master
ordevelop
) to a feature branch (rebasing your branch is cleaner than the upstream branch into your branch) - “Interactive rebasing” can refer to cleaning up your commit history, squashing commits, and editing commit messages. You typically do an interactive rebase before submitting a pull request/patch/branch for review
Rebasing your branch
Say you have a main branch called master
. Of course, you don’t develop directly against master
, as that would be bad. Instead, you create feature branches (e.g. feature/foo-widget
), develop on those, and when they’re done, you merge them back into master
(or submit a merge request).
Now, let’s say you created your feature branch a few days ago, and now there are changes on master
that you need on your branch to continue (or maybe your company has a policy of rebasing before you create a merge request).
The Wrong Way:
You could simply merge master
into your feature branch:
git merge master
This is what a lot of people do. However, this dirties up your git history and is not the correct solution. Let’s look at how the branch looks after you merge your feature branch into master
:
Commits are represented by circles, and named in the order they were authored. Both colored circles represent a merge commit.
And here is the git log output (notice the extra merge commit, cluttering up my history):
f403759 Merge branch 'feature/foobar' 647f7a5 Merge branch 'master' into feature/foobar 62e8839 Updating master after my feature branch b9287ba Adding a foobar feature c026dc1 This is another commit on master ef03c90 This is a master commit
This problem is compounded on large feature branches where you’re merging master
into your branch many times.
The Right Way:
Instead, you should rebase:
git checkout master git pull git checkout feature/foo-widget git rebase master git push -f origin feature/foo-widget
Before rebasing, your commit graph looks like this:
After rebasing, your commit graph looks like this (your commit was reapplied on the tip of master
, which is D):
And once your feature branch has been merged into master
, your commit history should look like this:
The very basic explanation is that you rewind all of your commits on feature/foo-widget
, fast foward feature/foo-widget
to the latest commit on master
, then you reapply each of your commits on top of the latest commit from master
.
It’s important to note that you should never rebase a shared branch (like master
or develop
) as it rewrites history and requires a force push, which can disrupt other developers.
Here is a more detailed walkthrough:
- Branch
develop
has commits 1-20 - You fork off of
develop
to createfeature/foobar
, from commit #20 - You add commits 21-24 to feature/foobar
- Some other dev add commits 21-22 to
develop
- You want to get the most recent changes from
develop
- You run ‘git rebase develop’ on your branch and this happens:
- Git looks at the last shared commit between
feature/foobar
anddevelop
which is commit #20 - Git rewinds
feature/foobar
to commit #20, storing your commits off the branch - Git fast forwards
feature/foobar
to commit #22 FROM thedevelop
branch feature/foobar
is now the same asdevelop
- Git re-applies each commit you had before (21-24) on top of #22, becoming 23-26
- Git looks at the last shared commit between
- If you run
git status
, you’ll like see something like this:Your branch and ‘origin/feature/foobar’ have diverged, and have 6 and 4 different commits each, respectively.
This is because commits are identified by their SHA1 hash, and your original 4 commits were rolled back and re-applied, which gives them a different SHA1 hash. Git now thinks you’re missing the original 4 commits, and sees you have 6 new commits, the 2 new ones from
develop
and the 4 new commit hashes from re-applying your original 4 commits. - Now however, if you try and push it’ll fail because the upstream version of
feature/foobar
has commits #21-24 that you made, which aren’t the same as your local branch’s commits #21-24, so you have to force push like this:git push -f origin feature/foobar
. It is critical to always specify the branch.-f
will overwrite the remote branch allowing you to push your corrected branch
Git Pull With Rebase
Another useful trick I use is git pull --rebase
instead if regular git pull
.
Let’s say you have some local changes on feature/foobar
that you haven’t pushed yet, and your co-worker just pushed his local changes to feature/foobar
. If you do a regular git pull
, git will do a merge, and you’ll end up with ugly git history and a commit message like this:
Merged ‘feature/foobar’ into ‘feature/foobar’
You want to avoid this clutter, so if you run git pull --rebase
, it’ll do the following:
- Rewind your local branch to the last commit that is shared with the remote
- Pull down the latest changes from the remote branch and apply them using a fast-foward
- Re-apply your local changes
Then you can do a regular git push
. Since you are only modifying history of commits you haven’t yet pushed, you do not need a force push.
Rebase Merge Conflicts
When rebasing of any sort, it isn’t uncommon to run into merge conflicts. It’s important to understand how to resolve conflicts and continue your rebase.
What happens is that Git has rolled back your commits, fast forwarded your branch to a specified point, and is now re-applying commits one by one. Sometimes this will work fine, but sometimes your commits will now conflict with the updated branch, and Git will pause the rebase and ask you to resolve the conflicts.
[email protected] ~/Projects/dotfiles (test-branch ✔) ± git rebase master First, rewinding head to replay your work on top of it... Applying: Commit I'm rebasing 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 Failed to merge in the changes. Patch failed at 0001 Commit I'm rebasing The copy of the patch that failed is found in: /home/brandon/Projects/dotfiles/.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".
So here, I branched off of master, then I created a new commit on master changing line 1 of README.md. Then on my branch, I created a new commit changing line 1 of README.md. Then I rebased my test branch on to master, and got a merge conflict.
To see which files have an error, you can just read the error message, or run git status:
[email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡) ± git status rebase in progress; onto b6afd87 You are currently rebasing branch 'test-branch' on 'b6afd87'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git reset HEAD..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a")
So here, git is telling me that both of my branches modified README.md. If I open README.md, I’ll see the merge conflict indicators:
<<<<<<< HEAD These are the changes from my master branch ======= These are the changes from my test branch >>>>>>> Commit I'm rebasing ================== This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.
Merge conflicts are always marked with “<<<<<<<“. The first part of the merge conflict, as separated by “=======”, indicate the conflicting line from your current working branch. The second half indicates the conflicting line from whatever your merging in (e.g. another branch, or in this case, or commit that was rewound). To pick the correct code, you can just edit the file normally. You don’t even have to pick one or the other. Make sure to remove the merge conflict indicators though!
These are the changes from BOTH of my branches ================== This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.
Now that I’ve resolved the conflict, I have to add the file to my staging area using git add README.md, then I can continue my rebase:
[email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡) ± git add README.md [email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡) ± git rebase --continue Applying: Commit I'm rebasing
And since that was my only commit, the rebase finished successfully. If I look at my git history, I see the commit from my test branch AFTER the commit from the master branch:
commit d331fa3648a4534e2f39a99365f60fa17045e7f1 Author: Brandon Wamboldt <[email protected]> Date: Thu Aug 27 13:20:35 2015 -0300 Commit from my test branch that I'll rebase commit b6afd872152a7b404bff13bfc01da05a668b3def Author: Brandon Wamboldt <[email protected]> Date: Thu Aug 27 13:20:11 2015 -0300 Commit from my master branch after I created my test branch
And if I looked at the diff for d331fa3, I can see how the rebase affected it:
diff --git a/README.md b/README.md index 87e2d96..6a47c12 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -These are the changes from my master branch +These are the changes from BOTH of my branches ================== This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.
And that concludes the section on merge conflicts.
Interactive Rebasing
Interactive rebasing is the use of git rebase
with the -i
flag. This tool lets you edit commit history which can be very useful when you have a feature branch that you want to clean up before submitting it. It’s very common to squash commits (combine multiple commits), edit commit messages, amend commits (roll back to a specific commit, make a change, then re-apply later commits), all of which you can do with interactive rebasing.
To start an interactive rebase session, you must specify the range of commits you wish to deal with. The most common way of doing this is using HEAD~n
where n is some number of commits:
git rebase -i HEAD~5
The above command will start an interactive rebase session with the last 5 commits. After running that command, you’ll likely see vim (or some other editor) pop up with something like the following:
pick 1f7036f More useful terminal title pick d61a3b3 Useful git aliases pick 8439627 Add git pushu pick cc8ba32 Tweak default email pick daadd1b Add install script # Rebase df47733..daadd1b onto df47733 # # 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 # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. #
At the top you see the 5 commits you are dealing with, from oldest to newest. In the comments, you see various actions. For example, if you modify the text to put this:
pick 1f7036f More useful terminal title pick d61a3b3 Useful git aliases s 8439627 Add git pushu pick cc8ba32 Tweak default email pick daadd1b Add install script
Git will combine d61a3b3
and 8439627
into a single commit (this is called squashing a commit). Like rebasing your branch, an interactive rebase alters git history. Each commit you modify will get a new commit hash, so as far as git is concerned, it is a completely different commit. This means that if you have previously pushed the commits you altered, you’ll have to do a force push on your branch. This also means you should never interactively rebase a shared branch like master
or develop
.