Martin Pickering

The Cardinal Rule about Git Rebase

This Post is part of a series about Git, Git concepts, commands and usage patterns to remind me and to help me learn. The first post of the series is Git - A New Years Resolution.

“Never git rebase a Shared Branch” is the often quoted rebasing guidance

But what does it mean and why is this a consideration?

What is a Shared Branch?

A Shared Branch is a branch that exists on a remote repository that is accessible to others and as such could be ‘pulled’ and used to base further work upon.

A quick review of git rebase

For many (and for me), a git rebase looks like the diagram below.

Conceptually, it appears as though the whole feature branch has been unplugged from its original position on develop and moved to be based upon the head of develop; or, to put it another way that all the commits from the feature branch have been re-applied to the tip of the develop branch.

Note my use of the word ‘reapply’, not ‘move’ or ‘cut & paste’. Git has taken each of the original commits on the feature branch in sequence, and has re-applied them to the destination [of the rebase command]. The implications of which are:

  1. These are new commits. They are not the original commits simply moved elsewhere
  2. The original commits still exist [somewhere in Git] and have not been destroyed; although they are now quite hard to find

The following diagram is perhaps a more accurate picture of what has actually happened due to a git rebase command, where commits E and F still exist but are all but hidden from view:

The changes represented by commits E and F have been re-applied to create the new E’ and F’ commits.

Guidance is great, but knowledge is better

Let’s illustrate why the Rebase Cardinal Rule exists by exploring some of the consequences of ignoring it.

Richard and Martin are working on the same Product and have both just resynchronised their local workspaces with the Azure DevOps hosted Repository. Their stuff looks similar to:

Martin rebases the F100 branch on develop; thereby, breaking the Cardinal Rule. Meanwhile Richard continues his work on F100 and adds a new commit.

Martin, proud of what he has achieved, decides that he should share the fruits of his labours, but is disappointed to receive a rejection from Git.

Martin (F100)$ git push
To https://dev.azure.com/bedroom-software/mega-product.git
 ! [rejected]    F100 -> F100 (non-fast-forward)
error: failed to push some refs to 'https://dev.azure.com/bedroom-software/mega-product.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push -- help' for details.

Usually when Martin pushes his work back to the remote, Git performs a fast-forward merge of his local changes into the remote’s copy. As Git cannot perform a fast-forward merge under these circumstances Git refuses to comply with Martin’s wishes. Git is trying its hardest to protect Martin from himself.

So, Martin hits the Git Documentation and finds that a solution to his dilemma is to use git push --force. By forcing the situation, Martin has instructed the remote to not attempt a fast-forward merge. Instead the remote will erase its version of F100 in favour of Martin’s version, to end up with…

Now, the unfortunate Richard also wishes to share his work on F100 by pushing back to the remote.

Richard (F100)$ git push
To https://dev.azure.com/bedroom-software/mega-product.git
 ! [rejected]    F100 -> F100 (non-fast-forward)
error: failed to push some refs to 'https://dev.azure.com/bedroom-software/mega-product.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push -- help' for details.

Richard’s attempt to git push has been rejected. However, the message justifying why the refusal occurred is not all that alarming. It has a fairly straight forward explanantion. Richard’s version of F100 is not in sync with the remote because it has been updated by somebody else. Git tries to help Richard by advising him to re-synchronise by using a git pull. This causes a merge to occur in Richard’s workspace of his version of F100 with the remote’s version. The outcome of the merge for Richard’s local copy is:

Having achieved a successful merge, Richard is now able to push his work back to the remote and sign-off for the day, happy with his achievements.

However, come the next team meeting and a quick review of the Azure Devops Repository and everybody is alarmed and confused as to what has happened and how such a messy history has been created. That’s when Martin remembers the Cardinal Rule for Git Rebase, makes his confession, puts on the dunce’s hat and buys everybody conciliatory doughnuts.

At least only two people’s work was involved in the SNAFU. Breaking the Cardinal Rule when more parties are involved gets exponentially worse and the mess becomes a real horror show. When that happens, doughnuts are not nearly enough to restore team love.


Last modified on 2020-05-11