Git Rebase Git

Nov 27th, 2020 - written by Kimserey with .

Git Rebase is a command used to reapply commits on top of another base tip. The base tip could be either another branch or the current branch. Rebase is useful in scenarios where we need to keep branches up to date with long lived branches or scenarios where we want to rework the history of a feature branch. In this week blog post we will look at different scenarios where Git Rebase is useful and understand the usage of the command with its parameters.

Rebase on latest upstream commit

Let’s take the first example of a feature branch which was branched out of a long lived master branch:

1
2
3
     E---F---G feature-a
    /
A---B---C---D  master

If C or D are fixes that we want to have in feature-a, we have two options; either merge master into feature-a or rebase feature-a on latest master. In the case of changes completely unrelated to feature-a, we can choose to rebase which would change the base of feature-a to the tip of master:

1
2
❯ git rebase master feature-a
Successfully rebased and updated refs/heads/feature-a.

which is equivalent to:

1
2
❯ git checkout feature-a
❯ git rebase master

The graph will then be as such:

1
2
3
             E'---F'---G' feature-a
            /
A---B---C---D master

E, F and G and temporarly saved and feature-a is reset to the latest of master then E, F and G are reapplied. In case of merge conflict, we can resolve conflicts on each commits and use:

1
2
3
❯ git rebase --continue
❯ git rebase --abort
❯ git rebase --skip
  • --continue is used when we resolve the conflict and continue to the next commit.
  • --abort is used to completely abort the rebase.
  • --skip is used to completely skip a commit.

If commits are already present in the upstream branch, then the rebase will skip those commits. For example:

1
2
3
          D---E---F feature-a
         /
A---B---C---D'---E' master

Executing rebase:

1
❯ git rebase master feature-a

will result in the following:

1
2
3
                    F feature-a
                   /
A---B---C---D'---E' master

Rebase onto a different base

It’s also possible to rebase on another branch. A typical scenario is when we work with master and feature branch, we have feature-a-1 which contains the beginning steps for our feature which we want to submit a pull-request on master, and we then continue to work on feature-a-2 so that we can have a subsequent PR onnce feature-a-1 gets merged.

1
2
3
4
5
                 F---G feature-a-2
                /
           D---E feature-a-1
         /
A---B---C master

If we squash merge like PR usually do:

1
2
❯ git checkout master
❯ git merge --squash feature-a

We would end up with DE’ being the squashed commit on master:

1
2
3
4
5
                 F---G feature-a-2
                /
           D---E feature-a-1
         /
A---B---C---DE' master
1
❯ git diff master...feature-a-2

will still show all commits from D, E, F and G as that would be the difference between the common ancestor and the tip of feature-a-2 because we squashed D and E into DE’. So what we want to do is rebase feature-a-2 onto master:

1
❯ git rebase --onto master feature-a-1 feature-a-2

The arguments are: --onto [<new base>] [<current upstream>] [<branch>]. The current upstream identifies where to start to take the commit from, we could also provide the commit hash like git rebase --onto master E feature-a-2. By providing a commit we would also be able to rebase just a portion of feature-a-2, for example if we do git rebase --onto master G feature-a-2 we will only be taking G to rebase on master.

We then get the following:

1
2
3
4
5
           D---E feature-a-1
         /
A---B---C---DE' master
             \
              F'---G' feature-a-2

Now if we look again at the diff:

1
❯ git diff master...feature-a-2

we will only see the diff added by feature-a-2. We can safely discard feature-a-1 branch.

Rebase to rework history commits

Another useful feature of git rebase is the ability to rewrite history.

1
2
3
           D---E---F---G feature-a
         /
A---B---C master

Here we have feature-a with commits D, E, F and G. We can rewrite the history by using HEAD~n where n is the number of commits we want to ammend:

1
❯ git rebase -i HEAD~2

This will open the rebase in interactive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pick dd579e7 (F) Another change
pick b189c5c (G) Something else change

# Rebase 710be5d..b189c5c onto 710be5d (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# 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.
#

The commits are listed from oldest to latest. The comment explains the possible commands to provide, by default pick would just pick back all commits which result in no changes.

What we can do is edit a commit with edit:

1
2
pick dd579e7 (F) Another change
e b189c5c (G) Something else change

Once we quit vim, we will be in the process of rebasing on the second step:

1
2
3
4
5
6
7
8
9
10
11
❯ git status
interactive rebase in progress; onto 710be5d
Last commands done (2 commands done):
   pick dd579e7 Anotherchange
   edit b189c5c Something else change
No commands remaining.
You are currently editing a commit while rebasing branch 'feature-a' on '710be5d'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

nothing to commit, working tree clean

We can see that git will pause the rebase at the edit step to let us use --amend to amend the commit, giving the opportunity to add new changes in between commits and then to change the commit message:

1
❯ git commit --amend

then once we done we can complete the rebase:

1
2
❯ git rebase --continue
Successfully rebased and updated refs/heads/feature-a.

If we don’t need to amend the change and only need to change the commit message we can use reword.

Another great feature from rebase interactive is the ability to squash multiple commits, for example if we have:

1
D---E---F---G feature-a

and those were all small typo fixes, we can group all the commits into a single commit to clean up the history:

1
2
3
4
5
❯ git rebase -i HEAD~3

pick 82b3c49 (E) Adding extra changes
s cad3722 (F) Something else change
s 7711c6a (G) Making another change

This will squash all 3 commits together in a single commit and give us the ability to change the commit message. After that we will end up with:

1
D---EFG' feature-a

Never Rebase long lived branches

We’ve seen how rebase can be used to rewrite history by changing base or even just directly rewriting the commits or adding new commits in between existing commits. It’s a powerful functionality giving us ability to rectify errors made or just cleaning history prior persisting it into the main branche. But those actions have consequences that we should be aware of.

Rebase should never be applied shared branches (or long lived branches used by many developers). The problem caused by rebasing a long lived branch is that if merge conflicts appear, and are fixed during rebasing, every branches had the, now rebased, branch as root will need to also resolve all those conflicts.

For example if we work in a GitFlow scenario with master and develop:

1
2
3
4
5
              F feature-a
            /
       D---E develop
     /
A---B---C master

We have a feature-a which was branched from develop and master being ahead by one commit (this is an example). If we rebase develop on latest master:

1
❯ git rebase master develop

and for any reason, D or E conflict with C, we would need to merge those conflicts. Now what we have would be:

1
2
3
4
5
           D'---E' develop
         /
A---B---C master
     \
      D---E---F feature-a

The dangerous part of that is that we effectively changed the initial base of feature-a and it’s no longer based on develop but instead based on master. Our feature-a is no longer in sync with develop. What we would need to do is go through the same exercise and rebase onto develop and fix every conflicts, and that would be necessary for any branch that branched from develop, so as a general rule, never rebase a branch used by others.

And that concludes today’s post! See you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.