Git Rebase Example

What is rebase?

Rebase means taking commits to made to an older revision of a branch and applying them to a newer revision of that branch (usually when the origin branch has changed while we were commiting).

Rebase is an alternative for merge, it's main advantage over merging is that it allows you to maintain a clean linear history.

Example

Let's look at this scenario:
  1. User A and User B both clone the same repository. At the moment of cloning the repository had 2 commits:
      $ git log
    
      19abe80 - (HEAD, master) added file2
      8cb6ba4 - added file1
      
  2. User B works on the project and makes two commits, so his repository looks like this:
      $ git log
    
      07c8c6d - (HEAD, master) added file4 <UserB>
      e404363 - added file3 <UserB>
      19abe80 - (origin/master, origin/HEAD) added file2
      8cb6ba4 - added file1
      

    As you can see, User B's master branch is ahead of 'origin/master' by 2 commits ('origin/master' is the master branch in the origin repository).

  3. User B pushes the changes to origin (the original repository):
      $ git push
      $ git log
    
      07c8c6d - (HEAD, origin/master, origin/HEAD, master)
        added file4 <UserB>
      e404363 - added file3 <UserB>
      19abe80 - added file2
      8cb6ba4 - added file1
      
  4. At the same time, User A makes two commits, so his repository looks like this:
      $ git log
    
      ef5b54f - (HEAD, master) added file6 <UserA>
      5f7cff8 - added file5 <UserA>
      19abe80 - (origin/master, origin/HEAD) added file2
      8cb6ba4 - added file1
      
  5. When User A tries to push his changes, he gets this error:
      $ git push
    
      ! [rejected]        master -> master (non-fast-forward)
      error: failed to push some refs to '...'
      To prevent you from losing history, non-fast-forward 
      updates were rejected Merge the remote changes 
      (e.g. 'git pull') before pushing again.  See the
      'Note about fast-forwards' section of 
      'git push --help' for details.
      

    Git notices that the master branch in the origin repository no longer matches 'origin/master' so it won't allow the push.

  6. To solve this, User A first fetches the changes from the origin repository:
      $ git fetch
      $ git log --graph --all
    
      * ef5b54f - (HEAD, master) added file6 <UserA>
      * 5f7cff8 - added file5 <UserA>
      | * 07c8c6d - (origin/master, origin/HEAD) added file4
      | * e404363 - added file3 <UserB>
      |/  
      * 19abe80 - added file2
      * 8cb6ba4 - added file1
      

    Adding --all to the log command shows commits from all of the branches and adding --graph shows a graphic representation of the branches.

  7. The second step is to perform a rebase that will update the new commits by User A (ef5b54f and 5f7cff8) so it will appear as if they were commited after the new 'origin/master' (07c8c6d).

    Since User A and User B's commits involve different files there won't be any conflicts (I will talk about conflicts later on).

      $ git rebase origin/master
    
      First, rewinding head to replay your work on top of it...
      Applying: added file5
      Applying: added file6
    
      $ git log --graph --all
    
      * 4fca7e8 - (HEAD, master) added file6 <UserA>
      * 29ea4d2 - added file5 <UserA>
      * 07c8c6d - (origin/master, origin/HEAD) added file4 <UserB>
      * e404363 - added file3 <UserB>
      * 19abe80 - added file2
      * 8cb6ba4 - added file1
      
  8. Now User A can push his changes to the origin repository:
      $ git push
      $ git log --graph --all
    
      * 4fca7e8 - (HEAD, origin/master, origin/HEAD, master)
                  added file6 
      * 29ea4d2 - added file5 <UserA>
      * 07c8c6d - added file4 <UserB>
      * e404363 - added file3 <UserB>
      * 19abe80 - added file2
      * 8cb6ba4 - added file1
      

    We now have a clean, linear history :-)

What happens when there are conflicts?

Let's look at a similar scenario:
  1. User B added a line to file4.txt, commited and pushed:
      $ git push
      $ git log --graph --all
    
      * e1e4730 - (HEAD, origin/master, origin/HEAD, master) 
                  added a line to file4 (2 minutes) <UserB>
      * 4fca7e8 - added file6 <UserA>
      * 29ea4d2 - added file5 <UserA>
      ...
      
  2. At the same time User A added a line to file4.txt, and commited:
      $ git log --graph --all
    
      * 77a6486 - (HEAD, master) 
                  added another line to file4 <UserA>
      * 4fca7e8 - (origin/master, origin/HEAD) added file6 <UserA>
      * 29ea4d2 - added file5 <UserA>
      ...
      
  3. When User A tries to push the changes, he gets the same error we have seen before. So, he fetches the changes and tries to rebase:
      $ git fetch
      $ git rebase origin/master
      
      First, rewinding head to replay your work on top of it...
      Applying: added another line to file4
      Using index info to reconstruct a base tree...
      Falling back to patching base and 3-way merge...
      Auto-merging file4.txt
      CONFLICT (content): Merge conflict in file4.txt
      Failed to merge in the changes.
      Patch failed at 0001 added another line to file4
    
      When you have resolved this problem run 
        "git rebase --continue".
      If you would prefer to skip this patch, instead run 
        "git rebase --skip".
      To restore the original branch and stop rebasing run 
        "git rebase --abort".
      
      

    While trying to auto merge file4.txt git reached a conflict and paused the rebase.

    If we look at file4.txt we will see the following contents:

      file4
      <<<<<<< HEAD
      added a line to file4
      =======
      added another line to file4
      >>>>>>> added another line to file4
      
  4. This is how git handles conflicts, now User A can open his favorite editor (vim) and merge the file manually:
      file4
      added a line to file4
      added another line to file4
      
  5. After merging the file, User A must mark the conflict as solved (by using "git add") and continue the rebase:
      $ git add file4.txt
      $ git rebase --continue
    
      * c8c37cd - (HEAD, master) 
                  added another line to file4 <UserA>
      * e1e4730 - (origin/master, origin/HEAD) 
                  added a line to file4 <UserB>
      * 4fca7e8 - added file6 <UserA>
      ...
      
  6. User A can now push the changes back to origin:
      $ git push
      * c8c37cd - (HEAD, origin/master, origin/HEAD, master)
                  added another line to file4 <UserA>
      * e1e4730 - added a line to file4 <UserB>
      * 4fca7e8 - added file6 <UserA>
      ...
      

    And again, we get a clean, linear history.

I hope you find this post useful.
Until the next time,
David.

Comments

Nir Naor said…
Very interesting and clear post.
So, just for the sake of order, what is actually the meaning of rebase?
does it mean taking your changes and trying to commit them to an existing repository with newer commits?
David Elentok said…
Sort-of, but not completely accurate (I edited the post with an answer, see the beginning of the post).
-------------- said…
Very helpful post!
Thanks!
Anonymous said…
+1
zair said…
Very helpful . thanks.
Anonymous said…
Very Helpful post!

Popular posts from this blog

Restart the Windows File Sharing Service to fix weird problems

WPF, ImageSource and Embedded Resources

SharpDevelop dark color scheme