Git Rebasing - make your commit history pretty

20 november 2017 - Sergei - Development

Rebasing!

  • scares a lot of people…

  • …but the benefit is a pretty commit history!

  • … but only if everyone does it :(

What is it?

In short: rewriting commit history.

Longer explanation:

  1. your own commits from a branch are temporarily rolled back

  2. the commits of the rebase target (=develop) that you didn’t have
    yet are applied

  3. your own commits are replayed on top

Why do it?

  • Cleaning up your commit history in feature branches provides a clear overview of what has been done for which feature

  • Because we’re human and make mistakes. This is fine on feature branches, however there’s no need to bother your colleagues with them

Before rebase

Let’s say - a feature branch is started.

develop 
A -- B -- C -- D
                \
                 E // start of feature

So, you happily implement features and commit without worry…

develop
A -- B -- C -- D
                \
                 E -- F -- G -- ... // much fun is had

But, so did others, and they had easy features that were simple to review and merge, causing develop to have moved forward, while you were doing all the hard stuff!

develop
A -- B -- C -- D -- H -- I
                \
                 E -- F -- G -- ... // much fun is had

So, you finish your feature, the build passes, code is the prettiest thing ever to be seen by mankind… but the commit history is a bit ugly:

  • 3 commits? this can actually be condensed into a single commit, because of raisins.

  • there are a couple of merge conflicts because of commit H…

Getting to rebase…

We want to solve two problems:

  • our feature should merge cleanly, and after commit I.

  • we want to combine commits E, F and G.

So, when we start the rebase, our commits are rewinded:

develop
A -- B -- C -- D -- H -- I
                \
                 // we're here, sort of

And then, the other commits are applied:

develop
A -- B -- C -- D -- H -- I
                     \
                      // now we're here

 

develop
A -- B -- C -- D -- H -- I
                          \
                           // and now we're at the HEAD of develop!

Then, we apply our own commits:

develop
A -- B -- C -- D -- H -- I
                          \
                           E // still fine

 

develop
A -- B -- C -- D -- H -- I
                          \
                           E -- F // oops, this one has a merge conflict!

… you resolve the merge conflict, and continue

develop
A -- B -- C -- D -- H -- I
                          \
                           E -- F -- G

Bam, rebase done, conflicts resolved.

Now, we’ll clean up our stuff - squash E, F and G together:

develop
A -- B -- C -- D -- H -- I
                          \
                           J

There, single commit for our feature, that references the develop HEAD, without merge conflicts. PR ready to be merged!

How do it?

  • Make sure your local develop is up to date:
    git checkout develop && git pull origin develop

  • Start the rebase: git rebase -i develop, which prompts you with a ‘commit plan’:

    pick 34836972 Start automating production dumps for testing/development purposes
    pick 5abe11b0 wip
    # Rebase 4aa66062..5abe11b0 onto 4aa66062 (2 commands)
    #
    # Commands:
    # p, pick = use commit
    # r, reword = use commit, but edit the commit message
    # e, edit = use commit, but stop for amending
    # s, squash = use commit, but meld into previous commit
    # f, fixup = like "squash", but discard this commit's log message
    # x, exec = run command (the rest of the line) using shell
    # d, drop = remove commit
    #
    # 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.
    #
    # Note that empty commits are commented out
  • the WIP commit is fine, I change pick into fixup, :wq and rebase is ‘done’

  • output of git status:

    git:(feature/dev-db-dump) git status
    On branch feature/dev-db-dump
    Your branch and 'origin/feature/dev-db-dump' have diverged,
    and have 17 and 2 different commits each, respectively.
      (use "git pull" to merge the remote branch into yours)
    nothing to commit, working tree clean
  • now push to the remote, with force, since we rewrote history:
    git push --force-with-lease

  • done, feel good about yourself

Common issues

  • maaaaaassive PR, with a lot of merge conflicts -> takes a long time to resolve them commit by commit, and it’s possible ‘in-between’ commits cancel each other out.

    Solution: rebase on current branch: git rebase -i HEAD~4 and squash commits before rebasing on develop.

    Or, if that also complicates things too much, git merge develop --no-ff

  • conflict resolution results into ‘empty commits’ later on. You can skip these with git rebase --skip.

  • git says branches have diverted -> this is expected, you should push with --force-with-lease. If you pull now (as it suggests), you’ll introduce nasty merge commits and duplicate commits. Ew.

Tips

  • when in doubt, create a backup branch from your feature branch: git branch backup, so you can recover using that branch if you mess up.

  • configure git globally to pull with rebase (= git pull --rebase):

    # ~/.gitconfig
    [pull]
        rebase = true  # <-- THIS

 



Latest Tweets