Proper Git Rebasing

on git

Rebasing is an other way of merging in changes from an other branch into your own. It's similar to git merge but the big difference is it keeps a clean history between commits by avoiding the useless merge commits.

posts/proper-git-rebasing/messy_history.png Messy history

posts/proper-git-rebasing/clean_history.png Clean history, only merge pull merges should be present

git merge is still a useful command for putting commits into master or dev. git rebase should only be used for grabbing commits from dev and putting them into a feature branch, these kinds of Merge branch 'something' commits are just noise.

So Why Rebase?

  • Keeping a clean history is better for everyones sanity
  • Removes the useless merge branch commits
  • Reverting commits is easier
  • It makes squashing--combining commits--much much easier

Have you ever tried squashing a merge commit? It'll bring you through hell and back. Although it can still be done easily through git reset lets not go there right now.

You Might Lose Commits

Yes, git rebase is a more advanced git command and if used incorrectly can lose commits but if you have a deeper knowledge of git rebase and practice a bit, it'll quickly become a valuable tool in your arsenal.

I wrote lose commits because although commits can disappear, if you know how to use git reflog, another advanced command, you can get back any lost commit. Remember git never deletes commits, every commit is always kept.

Simple Rebasing Example

You just got into work and like a good dev, the first thing you do is update your feature branch with dev.

So you do a git pull and see some new commits. The difference between the two branches ends up looking like this:

             +-----+    +-----+
Dev  +-------+  B  +----+  C  |
     |       +-----+    +-----+
  +--+--+                      
  |  A  |                      
  +--+--+                      
     |       +-----+           

Feature +-------+ D |
+-----+

You decide to do a git merge dev and although your feature branch is up to speed, it ended up with this nasty new merge commit.

             +-----+    +-----+
Dev  +-------+  B  +----+  C  |
     |       +-----+    +--+--+
  +--+--+                  |   
  |  A  |                  |   
  +--+--+                  v   
     |       +-----+    +-----+

Feature +-------+ D +----+Merge| +-----+ +-----+

There must be a better way and that is rebasing. git rebase dev.

             +-----+    +-----+           
Dev  +-------+  B  +----+  C  +--+        
     |       +-----+    +-----+  |        
  +--+--+                        |        
  |  A  |                        |        
  +-----+                        |        
                                 | +-----+

Feature +-+ 'D | +-----+

git rebase works a bit differently, instead of creating a merge commit it will checkout dev in a temporary branch and cherry pick all the commits in the feature branch (only D in this case) into the temp branch. Once every thing is done, the original feature branch is overwritten and you end up with a linear history.

So now it appears as if you wrote your new feature on top of B and C. Remember because D was cherry picked, it is now a new commit, with a new hash, and it's authored date would be now.

Git Status Is Messed Up

Now running git status --short, will give you this weird output.

## feature...origin/feature [ahead 2, behind 1]

This is the most confusing part of rebasing. When you do a git status it compares your local branch changes to what is on the remote branch, obivously remote doesn't have the rebase you just did because you never pushed, so a difference is expected.

          +-----+                                        

Remote | A |
+--+--+
| +-----+
Feature +-------+ D |
+-----+

+----------------------------------------------------------+

                     +-----+    +-----+                  
        Dev  +-------+  B  +----+  C  +--+               
             |       +-----+    +-----+  |               

Local +--+--+ |
| A | |
+-----+ |
| +-----+
Feature +-+ 'D |
+-----+

This is the current state of your local branch and the same branch on remote. When a git status is done, it actually counts the differences in commits, your local branch has two commits remote does not--ahead by 2--and you do not have one commit from remote--behind by 1. Remember 'D and D these are actually different commits now but contain the same changes.

Force Pushing

Hopefully we understand what's going on with git status. So we can now push our changes, but because we significantly change it by removing D we have to force push it.

This is the part where you could lose data, by force pushing you are telling remote to accept whatever you are giving it and it could be anything.

Before you run the command ensure you have this setting in your ~/.gitconfig.

[push]
    default = simple

This will stop git from force pushing all your branches to remote and potentially destroying other peoples work.

Safely push it with, git push --force.

 + 1d88fab...6612ec4 feature -> feature (forced update) 

At this point you're done and remote has your changes but there are a couple caveats to look out for.

Pulling a Rebased Branch

Pulling from remote and seeing either merge conflicts or a merge prompt window. That means you just pulled a rebased branch and should stop everything, but do close the merge prompt window.

By default git will try to make things right by merging remote changes into your branch but don't do it. You'll end up with this hydra merge and will make squashing a nightmare. It's much better to just reset your working directory.

git reset --hard @{u}

This says, throw away everything I have on my local and match the remote branch exactly. I do this so often I made it a git alias.

If you happened to have a commit you were trying to push you'll now have to do a cherry pick.

Handling Merge Conflicts

An other ceveat is rebasing and encountering merge conflicts. This is handled differently from git merge but it's still simple enough.

Resolve the conficts as normal and when finished just add the files to staging.

git add .
git rebase --continue

Or if you were not brave enough and want to stop rebasing.

git rebase --abort

Rebasing brings you to a temporary branch and you can make whatever changes you want. This makes it easy to abort at any time.

For more information on rebasing checkout the git docs.