# Rebasing

# Local Branch Rebasing

Rebasing (opens new window) reapplies a series of commits on top of another commit.

To rebase a branch, checkout the branch and then rebase it on top of another branch.

git checkout topic
git rebase master  # rebase current branch onto master branch

This would cause:


     A---B---C topic
     /
D---E---F---G master

To turn into:


             A'--B'--C' topic
             /
D---E---F---G master

These operations can be combined into a single command that checks out the branch and immediately rebases it:

git rebase master topic   # rebase topic branch onto master branch

Important: After the rebase, the applied commits will have a different hash. You should not rebase commits you have already pushed to a remote host. A consequence may be an inability to git push your local rebased branch to a remote host, leaving your only option to git push --force.

# Rebase: ours and theirs, local and remote

A rebase switches the meaning of "ours" and "theirs":

git checkout topic
git rebase   master    # rebase topic branch on top of master branch

Whatever HEAD's pointing to is "ours"

The first thing a rebase does is resetting the HEAD to master; before cherry-picking commits from the old branch topic to a new one (every commit in the former topic branch will be rewritten and will be identified by a different hash).

With respect to terminologies used by merge tools (not to be confused with local ref or remote ref (opens new window))

=> local is master ("ours"),
=> remote is topic ("theirs")

That means a merge/diff tool will present the upstream branch as local (master: the branch on top of which you are rebasing), and the working branch as remote (topic: the branch being rebased)

+-----------------------------------------+
| LOCAL:master |    BASE   | REMOTE:topic |
+-----------------------------------------+
|             MERGED                      |
+-----------------------------------------+

# Inversion illustrated

# On a merge:

c--c--x--x--x(*) <- current branch topic ('*'=HEAD)
    \
     \
      \--y--y--y <- other branch to merge

We don't change the current branch topic, so what we have is still what we were working on (and we merge from another branch)

c--c--x--x--x---------o(*)  MERGE, still on branch topic
    \       ^        /
     \     ours     /
      \            /
       --y--y--y--/
               ^
              theirs

# On a rebase:

But on a rebase we switch sides because the first thing a rebase does is to checkout the upstream branch to replay the current commits on top of it!

c--c--x--x--x(*) <- current branch topic ('*'=HEAD)
    \
     \
      \--y--y--y <- upstream branch

A git rebase upstream will first set HEAD to the upstream branch, hence the switch of 'ours' and 'theirs' compared to the previous "current" working branch.

c--c--x--x--x <- former "current" branch, new "theirs"
    \
     \
      \--y--y--y(*) <- set HEAD to this commit, to replay x's on it
               ^       this will be the new "ours"
               |
            upstream

The rebase will then replay 'their' commits on the new 'our' topic branch:

c--c..x..x..x <- old "theirs" commits, now "ghosts", available through "reflogs"
    \
     \
      \--y--y--y--x'--x'--x'(*) <- topic  once all x's are replayed,
               ^                      point branch topic to this commit
               |
        upstream branch

# Interactive Rebase

This example aims to describe how one can utilize git rebase in interactive mode. It is expected that one has a basic understanding of what git rebase is and what it does.

Interactive rebase is initiated using following command:

git rebase -i

The -i option refers to interactive mode. Using interactive rebase, the user can change commit messages, as well as reorder, split, and/or squash (combine to one) commits.

Say you want to rearrange your last three commits. To do this you can run:

git rebase -i HEAD~3

After executing the above instruction, a file will be opened in your text editor where you will be able to select how your commits will be rebased. For the purpose of this example, just change the order of your commits, save the file, and close the editor. This will initiate a rebase with the order you've applied. If you check git log you will see your commits in the new order you specified.

# Rewording commit messages

Now, you've decided that one of the commit messages is vague and you want it to be more descriptive. Let's examine the last three commits using the same command.

git rebase -i HEAD~3

Instead of rearranging the order the commits will be rebased, this time we will change pick, the default, to reword on a commit where you would like to change the message.

When you close the editor, the rebase will initiate and it will stop at the specific commit message that you wanted to reword. This will let you change the commit message to whichever you desire. After you've changed the message, simply close the editor to proceed.

# Changing the content of a commit

Besides changing the commit message you can also adapt the changes done by the commit. To do so just change pick to edit for one commit. Git will stop when it arrives at that commit and provide the original changes of the commit in the staging area. You can now adapt those changes by unstaging them or adding new changes.

As soon as the staging area contains all changes you want in that commit, commit the changes. The old commit message will be shown and can be adapted to reflect the new commit.

# Splitting a single commit into multiple

Say you've made a commit but decided at a later point this commit could be split into two or more commits instead. Using the same command as before, replace pick with edit instead and hit enter.

Now, git will stop at the commit you have marked for editing and place all of its content into the staging area. From that point you can run git reset HEAD^ to place the commit into your working directory. Then, you can add and commit your files in a different sequence - ultimately splitting a single commit into n commits instead.

# Squashing multiple commits into one

Say you have done some work and have multiple commits which you think could be a single commit instead. For that you can carry out git rebase -i HEAD~3, replacing 3 with an appropriate amount of commits.

This time replace pick with squash instead. During the rebase, the commit which you've instructed to be squashed will be squashed on top of the previous commit; turning them into a single commit instead.

# Rebase down to the initial commit

Since Git 1.7.12 (opens new window) it is possible to rebase down to the root commit. The root commit is the first commit ever made in a repository, and normally cannot be edited. Use the following command:

git rebase -i --root

# Rebasing before a code review

# Summary

This goal is to reorganize all of your scattered commits into more meaningful commits for easier code reviews. If there are too many layers of changes across too many files at once, it is harder to do a code review. If you can reorganize your chronologically created commits into topical commits, then the code review process is easier (and possibly less bugs slip through the code review process).

This overly-simplified example is not the only strategy for using git to do better code reviews. It is the way I do it, and it's something to inspire others to consider how to make code reviews and git history easier/better.

This also pedagogically demonstrates the power of rebase in general.

This example assumes you know about interactive rebasing.

# Assuming:

  • you're working on a feature branch off of master
  • your feature has three main layers: front-end, back-end, DB
  • you have made a lot of commits while working on a feature branch. Each commit touches multiple layers at once
  • you want (in the end) only three commits in your branch
    • one containing all front end changes
    • one containing all back end changes
    • one containing all DB changes

    # Strategy:

    • we are going to change our chronological commits into "topical" commits.
    • first, split all commits into multiple, smaller commits -- each containing only one topic at a time (in our example, the topics are front end, back end, DB changes)
    • Then reorder our topical commits together and 'squash' them into single topical commits

    # Example:

    $ git log --oneline master..
    975430b db adding works: db.sql logic.rb
    3702650 trying to allow adding todo items: page.html logic.rb
    43b075a first draft: page.html and db.sql
    $ git rebase -i master
    
    

    This will be shown in text editor:

    pick 43b075a first draft: page.html and db.sql
    pick 3702650 trying to allow adding todo items: page.html logic.rb
    pick 975430b db adding works: db.sql logic.rb
    
    

    Change it to this:

    e 43b075a first draft: page.html and db.sql
    e 3702650 trying to allow adding todo items: page.html logic.rb
    e 975430b db adding works: db.sql logic.rb
    
    

    Then git will apply one commit at a time. After each commit, it will show a prompt, and then you can do the following:

    Stopped at 43b075a92a952faf999e76c4e4d7fa0f44576579... first draft: page.html and db.sql
    You can amend the commit now, with
    
            git commit --amend
    
    Once you are satisfied with your changes, run
    
            git rebase --continue
    
    $ git status
    rebase in progress; onto 4975ae9
    You are currently editing a commit while rebasing branch 'feature' on '4975ae9'.
      (use "git commit --amend" to amend the current commit)
      (use "git rebase --continue" once you are satisfied with your changes)
    
    nothing to commit, working directory clean
    $ git reset HEAD^ #This 'uncommits' all the changes in this commit.
    $ git status -s
     M db.sql
     M page.html
    $ git add db.sql  #now we will create the smaller topical commits
    $ git commit -m "first draft: db.sql"
    $ git add page.html
    $ git commit -m "first draft: page.html"
    $ git rebase --continue
    
    

    Then you will repeat those steps for every commit. In the end, you have this:

    $ git log --oneline
    0309336 db adding works: logic.rb
    06f81c9 db adding works: db.sql
    3264de2 adding todo items: page.html
    675a02b adding todo items: logic.rb
    272c674 first draft: page.html
    08c275d first draft: db.sql
    
    

    Now we run rebase one more time to reorder and squash:

    $ git rebase -i master
    
    

    This will be shown in text editor:

    pick 08c275d first draft: db.sql
    pick 272c674 first draft: page.html
    pick 675a02b adding todo items: logic.rb
    pick 3264de2 adding todo items: page.html
    pick 06f81c9 db adding works: db.sql
    pick 0309336 db adding works: logic.rb
    
    

    Change it to this:

    pick 08c275d first draft: db.sql
    s 06f81c9 db adding works: db.sql
    pick 675a02b adding todo items: logic.rb
    s 0309336 db adding works: logic.rb
    pick 272c674 first draft: page.html
    s  3264de2 adding todo items: page.html
    
    

    NOTICE: make sure that you tell git rebase to apply/squash the smaller topical commits in the order they were chronologically commited. Otherwise you might have false, needless merge conflicts to deal with.

    When that interactive rebase is all said and done, you get this:

    $ git log --oneline master..
    74bdd5f adding todos: GUI layer
    e8d8f7e adding todos: business logic layer
    121c578 adding todos: DB layer
    
    

    # Recap

    You have now rebased your chronological commits into topical commits. In real life, you may not need to do this every single time, but when you do want or need to do this, now you can. Plus, hopefully you learned more about git rebase.

    # Testing all commits during rebase

    Before making a pull request, it is useful to make sure that compile is successful and tests are passing for each commit in the branch. We can do that automatically using -x parameter.

    For example:

    git rebase -i -x make

    will perform the interactive rebase and stop after each commit to execute make. In case make fails, git will stop to give you an opportunity to fix the issues and amend the commit before proceeding with picking the next one.

    # Configuring autostash

    Autostash is a very useful configuration option when using rebase for local changes. Oftentimes, you may need to bring in commits from the upstream branch, but are not ready to commit just yet.

    However, Git does not allow a rebase to start if the working directory is not clean. Autostash to the rescue:

    git config --global rebase.autostash    # one time configuration
    git rebase @{u}                         # example rebase on upstream branch
    
    

    The autostash will be applied whenever the rebase is finished. It does not matter whether the rebase finishes successfully, or if it is aborted. Either way, the autostash will be applied. If the rebase was successful, and the base commit therefore changed, then there may be a conflict between the autostash and the new commits. In this case, you will have to resolve the conflicts before committing. This is no different than if you would have manually stashed, and then applied, so there is no downside to doing it automatically.

    # Aborting an Interactive Rebase

    You have started an interactive rebase. In the editor where you pick your commits, you decide that something is going wrong (for example a commit is missing, or you chose the wrong rebase destination), and you want to abort the rebase.

    To do this, simply delete all commits and actions (i.e. all lines not starting with the # sign) and the rebase will be aborted!

    The help text in the editor actually provides this hint:

    # Rebase 36d15de..612f2f7 onto 36d15de (3 command(s))
    #
    # 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
    #
    # These lines can be re-ordered; they are executed from top to bottom.
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    #
    # However, if you remove everything, the rebase will be aborted.
    #          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    # Note that empty commits are commented out
    
    

    # Setup git-pull for automatically perform a rebase instead of a merge

    If your team is following a rebase-based workflow, it may be a advantageous to setup git so that each newly created branch will perform a rebase operation, instead of a merge operation, during a git pull.

    To setup every new branch to automatically rebase, add the following to your .gitconfig or .git/config:

    [branch]
    autosetuprebase = always
    
    

    Command line: git config [--global] branch.autosetuprebase always

    Alternatively, you can setup the git pull command to always behave as if the option --rebase was passed:

    [pull]
    rebase = true
    
    

    Command line: git config [--global] pull.rebase true

    # Pushing after a rebase

    Sometimes you need rewrite history with a rebase, but git push complains about doing so because you rewrote history.

    This can be solved with a git push --force, but consider git push --force-with-lease, indicating that you want the push to fail if the local remote-tracking branch differs from the branch on the remote, e.g., someone else pushed to the remote after the last fetch. This avoids inadvertently overwriting someone else's recent push.

    Note: git push --force - and even --force-with-lease for that matter - can be a dangerous command because it rewrites the history of the branch. If another person had pulled the branch before the forced push, his/her git pull or git fetch will have errors because the local history and the remote history are diverged. This may cause the person to have unexpected errors. With enough looking at the reflogs the other user's work can be recovered, but it can lead to a lot of wasted time. If you must do a forced push to a branch with other contributors, try to coordinate with them so that they do not have to deal with errors.

    # Syntax

    • git rebase [-i | --interactive] [options] [--exec <cmd>] [--onto <newbase>] [<upstream>] [<branch>]
    • git rebase [-i | --interactive] [options] [--exec <cmd>] [--onto <newbase>] --root [<branch>]
    • git rebase --continue | --skip | --abort | --edit-todo

    # Parameters

    Parameter Details
    --continue Restart the rebasing process after having resolved a merge conflict.
    --abort Abort the rebase operation and reset HEAD to the original branch. If branch was provided when the rebase operation was started, then HEAD will be reset to branch. Otherwise HEAD will be reset to where it was when the rebase operation was started.
    --keep-empty Keep the commits that do not change anything from its parents in the result.
    --skip Restart the rebasing process by skipping the current patch.
    -m, --merge Use merging strategies to rebase. When the recursive (default) merge strategy is used, this allows rebase to be aware of renames on the upstream side. Note that a rebase merge works by replaying each commit from the working branch on top of the upstream branch. Because of this, when a merge conflict happens, the side reported as ours is the so-far rebased series, starting with upstream, and theirs is the working branch. In other words, the sides are swapped.
    --stat Show a diffstat of what changed upstream since the last rebase. The diffstat is also controlled by the configuration option rebase.stat.
    -x, --exec command Perform interactive rebase, stopping between each commit and executing command

    # Remarks

    Please keep in mind that rebase effectively rewrites the repository history.

    Rebasing commits that exists in the remote repository could rewrite repository nodes used by other developers as base node for their developments. Unless you really know what you are doing, it is a best practice to rebase before pushing your changes.