Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Rebasing Revisited

2011, git, gtotw, tip

This week's Git Tip of the Week is about using rebase to move between branches. You can subscribe to the feed if you want to receive new instalments automatically.


Interactive Rebasing

In my last rebasing post, I discussed the concept of rebasing, the ability to create new history by replaying past commits in a different order. In fact, most of that post turned out to be discussing interactive rebasing, which allows you to change the order of commits, squash two (or more) commits into one and even remove commits from the history.

However, rebasing also plays another key part in the way developers frequently interact with git, by moving a branch in its entirety forward to a new point.

Rebasing branches

Let's say you've been working on a feature branched off a known point, and you now want to commit it to the repository. Let's assume the history looks like:

A → B → C → 1 → 2 → 3

where C was the point of master at the time of starting the feature branch. If the remote branch has moved on since then (say, to “F”), we have the option of creating a merge node “G”, or moving our feature branch forwards, based on where the remote is now:

A → B → C → D → E → F → G // Merge node
         \→ 1 → 2 → 3 →/

A → B → C → D → E → F ⇒ 1 → 2 → 3

Some developers or large development teams have a preference to create merge nodes, not only as a way of avoiding problems (the merge node can be tested) but also as a way of documenting where it came from in the first place. Some teams even create merge nodes when they're not needed (such as the git pull --no-ff option).

Other developers like trying to keep the number of merge nodes to a minimum, to try (as far as is possible) to have a linear history in the repository.

Either way, although there's not a right answer, Git allows you to do both depending on what the right answer is for you, at that particular point.

Rebase example

If we wanted to achieve the second history, we could write an interactive rebase script which picked first changes D, E and F, followed by 1, 2, and 3. However, doing this manually would be error prone, particularly if the branch has moved on more than a few commits.

That's where git rebase onto comes into play.

If the above branches were named master (for A..G) and feature (for 1..3), we can transplant feature forwards with:


$ git rebase master feature
First, rewinding head to replay your work on top of it...
Applying: 1
Applying: 2
Applying: 3

This takes the set of changes in feature that aren't in master, applies them to where master is now, and then calls that the new feature branch.

The second argument can be optimised away if we're already on the feature branch:


$ git branch
* feature
  master
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: 1
Applying: 2
Applying: 3
72366d5 3
8da923e 2
a4fe060 1
65521f9 F
62f8b88 E
e210d31 D
94c037d C
668b955 B
a34bd14 A

One nice property of git rebase is that it won't duplicate deltas that are the same. So if we need a particular change for a bugfix, it won't re-apply that:


$ git checkout master
Switched to branch 'master'
$ git cherry-pick 8da923e
[master cf6d845] 2
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 2
$ git log --oneline
cf6d845 2
65521f9 F
62f8b88 E
...
$ git rebase master feature
First, rewinding head to replay your work on top of it...
Applying: 1
Applying: 3
$ git log --oneline
ba0ce72 3
6979b71 1
cf6d845 2
65521f9 F
62f8b88 E
...

In this case, we cherry-picked 2 from the branch, and then applied the remaining feature changes afterwards. This is a quick way of doing an interactive rebase if you know you just want to pull a single change but don't want to fire up an editor.

Rebasing onto

So where does --onto come into play? This is used for some serious tree surgery; it basically allows you to take a set of commits, and transplant them onto a different node.

In our example, let's say we had created two feature branches, feature1 and feature2, which were logically independent, but we'd ended up developing it so that feature2 branched off feature1. We'd have something that looked like:


Ma → Mb → Mc → Md ← Master
           \ → F1a → F1b ← Feature1
                       \ → F2a → F2b ← Feature2

Now let's say that we want to push Feature2 to Master, but we don't want to push Feature1 to Master yet (perhaps because it's not ready). We can perform the transplant with a git rebase --onto as follows:


$ git branch
  feature1
* feature2
  master
$ git log --oneline feature2
986d8ac F2b
625bcde F2a
fb24802 F1b
d8f7e48 F1a
3cd0af7 Mc
addd99a Mb
7ad7ead Ma
$ git rebase --onto master feature1 feature2
First, rewinding head to replay your work on top of it...
Applying: F2a
Applying: F2b
$ git log --oneline feature2
b3e017a F2b
ac65d2e F2a
205af73 Md
3cd0af7 Mc
addd99a Mb
7ad7ead Ma
$ git log --oneline feature1
fb24802 F1b
d8f7e48 F1a
3cd0af7 Mc
addd99a Mb
7ad7ead Ma
$ git log --oneline master
205af73 Md
3cd0af7 Mc
addd99a Mb
7ad7ead Ma

We've now transplanted the bit between feature1 and feature2 onto the current master branch. In effect, we're doing a git cherry-pick of all the changes between feature1 and feature2 onto the current point of master, then reseting the feature2 branch (that we're currently on) to point to the new location. Our git repository now looks like:


                 / → F2a → F2b ← Feature2
Ma → Mb → Mc → Md ← Master
           \ → F1a → F1b ← Feature1

You can see this graphically from the terminal using git log:


$ git log --decorate --graph --oneline --all
* b3e017a (HEAD, feature2) F2b
* ac65d2e F2a
* 205af73 (master) Md
| * fb24802 (feature1) F1b
| * d8f7e48 F1a
|/  
* 3cd0af7 Mc
* addd99a Mb
* 7ad7ead Ma

Summary

Rebasing is an incredibly convenient way of updating your local repository to the current version of the branch on a remote server, and is used frequently. It can also be used to perform multiple cherry-pick operations and reorder (local) history in order to create new versions of that history.

As with any power tool, care must be taken not to reorder changes which have previously been made available (e.g. via GitHub) as creating new history causes a divergence in the timeline which causes eddies in the space time continuum. Or at least annoys people.


Come back next week for another instalment in the Git Tip of the Week series.