Derek Gourlay's Website

Git - When to Merge vs. When to Rebase

Does this (messy) branch history look familiar to you?

merge_commits

Keeping a clean history in git comes down to knowing when to use merge vs. rebase.  Great quote describing when to use each:

Rebases are how changes should pass from the top of hierarchy downwards and merges are how they flow back upwards.

Rule of thumb:

  • When pulling changes from origin/develop onto your local develop  use rebase.
  • When finishing a feature branch merge the changes back to develop.1

Use git pull --rebase  when pulling changes from origin

Difference between git pull & git pull --rebase:

Situation #1: You haven’t made any changes to your local develop branch and you want to pull changes from origin/develop 

In this case, git pull and git pull --rebase will produce the same results.  No problems.

Situation #2: You’ve got one or two small changes of your own on your local develop branch that have not yet been pushed.  You want to pull any changes you are missing from origin/develop to your local develop before you can push.


                     origin/develop
                       |
         +---- (E)----(F)
        /                
(A) -- (B)------- (C) -- (D) 
                          |         
                        develop

 


A regular git pull will often result in the following:


                 origin/develop
                       |       
         +---- (E)----(F)----------------
        /                                \
(A) -- (B) --------(C) -- (D)−---(G - merge commit) 
                                          |          
                                        develop 

 


 

Unfortunately you no longer have a linear commit history.  And after pushing your git history looks like:


         +---- (E)----(F)----------------
        /                                \
(A) -- (B) --------(C) -- (D)----(G - merge commit) 
                                          |         
                                        develop 
                                          |
                                   origin/develop

Instead use git pull --rebase


(A) -- (B)------- (E) -- (F) ------- (C') -- (D')
                          |                   |
                    origin/develop         develop

During the rebase your local commits C & D are played in order on top of the changes you pulled from origin/developThese commits are replaced with C' & D' as you solve local conflicts one commit at a time when they are replayed. 

Now pushing to origin/develop results in a fast-forward and a nice clean git history


(A) -- (B)------- (E) -- (F) ------- (C') -- (D')
                                              |
                                           develop  
                                              |
                                        origin/develop

Use git merge  when finishing off a feature branch

Example of merging a feature branch back into develop before pushing:


                 origin/develop                       develop
                       |                                 |       
         +---- (C)----(D)-----------(H - Merging completed Feature for TMS-123)-----
        /                           /     
(A) -- (B) --------(E) -- (F)----(G) 
                                  |         
                         feature/TMS-123/myCoolFeature

After pushing to origin:


                                                   origin/develop
                                                          |
                                                       develop
                                                         |       
         +---- (C)----(D)-----------(H - Merging completed Feature for TMS-123)-----
        /                           /     
(A) -- (B) --------(E) -- (F)----(G) 
                                  |         
                         feature/TMS-123/myCoolFeature

Hold on, git pull --rebase isn't all gravy!

While it is possible to set your system to default to git pull --rebase over using the regular git pull you will occasionally find you run into problems such as the following scenario:

You slave away working on a local feature branch and finish it off by merging it back into develop (using the --no-ff flag of course) resulting in the same history as the previous example:


                                                   origin/develop
                                                         |
                                                       develop
                                                         |       
         +---- (C)----(D)-----------(H - Merging completed Feature for TMS-123)-----
        /                           /     
(A) -- (B) --------(E) -- (F)----(G) 
                                  |         
                         feature/TMS-123/myCoolFeature

However after running git fetch you notice that origin/develop is 2 commits of head of develop:


                                                       develop                        origin/develop
                                                         |                                  |
         +---- (C)----(D)-----------(H - Merging completed Feature for TMS-123)-----(I)----(J)
        /                           /     
(A) -- (B) --------(E)----(F)----(G) 
                                  |         
                         feature/TMS-123/myCoolFeature

You run git pull --rebase as told and all of a sudden something strange has happened:


(A) -- (B) ------(C)---(D)----(I)----(J)----(E')-----(F')-----(G')---- 
        \                             |                        |        
         \                      origin/develop             develop
          \
            --------(E)----(F)----(G)
                                   |         
                          feature/TMS-123/myCoolFeature

 


Your merge commit has disappeared!

What we really wanted was:


                                    origin/develop                 develop 
                                         |                           |         
         +---- (C)----(D)-------(I)-----(J)--------(H' - Merging completed Feature for TMS-123)
        /                                                        /     
(A) -- (B) --------(E)----(F)----------------------------------(G) 
                                                                |         
                                                 feature/TMS-123/myCoolFeature   

The problem is... 

Rebasing Deletes Merge Commits


Welcome the --preserve-merges flag to center stage:

From the git-rebase manpage:

-p, --preserve-merges
           Instead of ignoring merges, try to recreate them.
           This uses the --interactive machinery internally, but combining it with the --interactive option explicitly is generally not a good idea unless you
           know what you are doing (see BUGS below).

So, instead of using git pull --rebase, use:

git fetch origin and git rebase -p origin/develop


Downsides to git rebase -p:

Git pull is dead!

Unfortunately the -p flag cannot be used in conjunction with git pull ( git pull --rebase -p doesn't work!) and as a result you have to explicitly fetch & rebase changes from origin.

ORIG_HEAD is no longer preserved

Picture of Finn the Human with an upset expression

ORIG_HEAD can be quite handy for multiple scenarios (If you want to review all changes you've just merged: git log -p --reverse ORIG_HEAD).  Unfortunately, git rebase -p sets ORIG_HEAD for each commit being rebased.

(Check out this google+ conversation for more tips RE: how to use ORIG_HEAD)

Branch tracking is not used

Unlike git pull --rebase, which will fetch changes from the branch your current branch is tracking, git rebase -p doesn’t have a sensible default to work from. You have to give it a branch to rebase from (which is why we specify origin/develop in the above example).


Conclusion

To avoid messy merge commits and help keep a relatively clean git commit history use the following workflow when fetching upstream changes:

git fetch origin
git rebase −p origin/develop

For further reading about the inner workings of Git, I found the Git Internals section of the Pro Git book very insightful!





  1. Thanks to the folks at the following sites I read while creating this post:
    - http://viget.com/extend/only-you-can-prevent-git-merge-commits 
    - http://gitready.com/advanced/2009/02/11/pull-with-rebase.html 
    - http://www.sbf5.com/~cduan/technical/git/git-5.shtml 
    - http://notes.envato.com/developers/rebasing-merge-commits-in-git/ []
 
Comments

cheers for the `-p`; I have always just re-rebased & re-merged. :-D

I think there is an error near the beginning. When you first suggest "Instead use /git pull --rebase/", the (E) and (F) commits switch from being on origin/develop to the local develop. The resulting history should look like:

(A) -- (B)------- (E) -- (F) ------- (C') -- (D')

i think there is an error there.

status

origin/develop
|
+---- (E)----(F)
/
(A) -- (B)------- (C) -- (D)
|
develop

now git pull –rebase will lead to:

(A) -- (B)------- (E) -- (F) ------- (C') -- (D')
| |
origin/develop develop

Thanks! Corrected this error (with some renaming of commits so the end result is easy to understand).

Additionally noticed there were some crazy formatting problems with '--' being turned into an em-dash.