Skip to main content
Code and think

GIT Rebase

A common workflow in git is to create a feature or bugfix branch for your work before you start. In case you are using rebase workflow, or just want to keep a clean commit log, you should rebase your branch to the target branch.

Shortcut #

A quick way to do rebase is to use the -r switch in the pull command.
For example: you are working on your branch and want to merge to master. To rebase you can check out your branch and use the following command: git pull -r origin master

Reference

What is rebase #

Rebase reverts all commits from a branch, removes a current branch, creates a new one from the target branch and replies the commits. This means that the commit history is modified. It is important to never rebase public branch or a branch that we shared with someone. We can only rebase a branch that is our private. If not, we can cause a lot of troubles to our fellow contributors.

What if we pushed the branch before rebasing #

Let's imagine we have a branch named main. There is only one file with the content:

# LearnGit

## First chapter

TODO

## Second chapter

TODO

## Third chapter

TODO

## Fourth chapter

TODO

Now, we create a second branch named second. After that, we add another commit to the main branch, so the file looks like:

# LearnGit

## First chapter

Description of the first chapter

## Second chapter

TODO

## Third chapter

TODO

## Fourth chapter

TODO

We switch to the second branch and add description of the second chapter and commit it. The file content is:

# LearnGit

## First chapter

TODO

## Second chapter

Description of the second chapter

## Third chapter

TODO

## Fourth chapter

TODO

After we add commit on second branch we check the git log and see:

bdd614a Add desc 2
3aab9bb Initial commit

We show output of git log --oneline command. The first column is commit SHA and the second one is commit message. The HEAD info is omitted for clarity. This structure is used in all further examples below.

Here is the graph of the log above:

gitGraph LR:
  commit id: "3aab9bb"
  branch second
  commit id: "bdd614a"
  checkout main
  commit id: "88c96b4"

Let's push the current state to the remote and then rebase the second branch. After we rebase we see the following in the log:

e6ef94e Add desc 2
88c96b4 Add desc 1
3aab9bb Initial commit

Here is the graph of the log above:

gitGraph LR:
  commit id: "3aab9bb"
  commit id: "88c96b4"
  branch second
  commit id: "e6ef94e"

We received a commit 88c96b4 with description 1. The original commit that was committed to second branch with id bdd614a is no longer there. The reason is that git reverted that commit, took all commits from the main branch and replayed a content of the bdd614a as a new commit. This commit is different, because it has different parent. If it was modifying the same line that was modified in the any of the newly added commits, then we would have a conflict. But this is for another time.

Let's push our changes to remote #

Normally, after we are happy with our commits we want tp push our changes. In case we try to push after rebase we would be met with the following error:

 ! [rejected]        second -> second (non-fast-forward)
error: failed to push some refs to 'https://github.com/bojanpikl/LearnGit.git'
hint: Updates were rejected because the tip of your current branch is behind  
hint: its remote counterpart. If you want to integrate the remote changes,    
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.    

Because we pushed the second branch to the remote before rebasing, we now have different commits on local second branch and remote second branch. Eventually, they should be synchronized. From what I have seen, often times we can do a mistake on this step and take the wrong direction.

The wrong step after rebase - pull #

Since the branches locally and on remote are different, we can pull changes from remote. This is usually what we do, since we want to receive other's changes before contributing our own. However, as we have seen, during rebasing the history changed. So, if we pull from remote, we get:

1a3ebe9 Merge branch 'second' of https://github.com/bojanpikl/LearnGit into second
e6ef94e Add desc 2
bdd614a Add desc 2
88c96b4 Add desc 1
3aab9bb Initial commit

Here is the graph of the log above:

gitGraph LR:
  commit id: "3aab9bb"
  commit id: "88c96b4"
  branch second
  commit id: "bdd614a"
  commit id: "e6ef94e"
  commit id: "1a3ebe9" type:HIGHLIGHT

Reading the output we see that the last 3 commits are strange. We have two commits with the same message Add desc 2, however, we only ever created one such commit. To understand what happened, we can check the SHA. One commit is from the original commit, and the second from the rebased commit. The last commit is a merge commit. We will ignore it in scope of this article, but by default git pull does merge operation, so we get merge commit.

What we see is that if we pull after rebase, our commits will duplicate. This happens because GIT doesn't know that the commits bdd614a and e6ef94e have the same content. To git these two commits are simply two different commits. But what should be done?

The correct step after rebase - force push #

After rebase, we know that our push will be rejected. Now, we have learned that pull is a bad option, because it makes ugly log history and can even lead to bugs (in case of merge conflicts). If the branch exists on the remote, it is important that we push our changes there. To do this, git offers a force and force-with-lease flags. In general, if you plan to use force be sure you know very well what are you doing, because this is a very powerful command that can lead to loose of data.

Much safer flag is force-with-lease. This flag allows us to push after modifying the history. It however, checks that the remote was not modified after we last pushed. This guarantees us that in case our fellow contributor pushed some changes we will be notified about this instead of simply loosing them. So, after rebase, make sure to git push --force-with-lease, so that the remote will receive a new log history as well.

What if you rebased on remote #

In case you rebased on the remote, I would advise to remove your local branch git branch -d second and then fetch changes. A new branch will be created for you. Of course, be careful not to have any commits locally that you haven't pushed before rebasing, or deleting branch.