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.
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 →// 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.
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.
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
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.
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,
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
feature2 onto the current
master branch. In effect, we’re doing a
git cherry-pick of all the changes between
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 --decorate --graph --oneline --all * b3e017a (HEAD, feature2) F2b * ac65d2e F2a * 205af73 (master) Md | * fb24802 (feature1) F1b | * d8f7e48 F1a |/ * 3cd0af7 Mc * addd99a Mb * 7ad7ead Ma
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.