Git Rebase

Git rebase is one of those commands that allows you to rewrite history and tidy up your branch history.

Published on April 4, 2021

In my earlier tutorial I wrote about squashing commits. I made use of git rebase -i for the purpose of removing commit messages that I didn't want to put into the development branch or master branch. One of those commit messages were like the following:

$ git commit -m "Got it working. Need to clean things up :D"

I typically use git rebase to avoid adding merge commits when I'm going to merge back into a main line of development. I touch on this subject in my tutorial about practical merging.

Doing this comes down to preference. Some people like to avoid merge commits whenever they can. Some like to only have merge commits into their main line of development. Finally, some like to use it when it seems sensible as defined by them.

Regardless, being able to rebase with confidence is good to know. In this tutorial, I demonstrate how to rebase and what to watch out for when working with another developer on the same branch.

Git rebase

Let's say I discovered a bug in production and I need to patch it. It's a tricky one, so I might need to spend a bit of time on it.

git rebase login-bugfix

I have my branch and I start debugging. As I'm working away, the master branch starts moving forward.

git rebase master branch progress

I figured out what I need to do, so I start fixing the bug. In the meantime the master branch progressed again.

git rebase master branch progress 2

I've finished fixing the bug and I tested my changes. The only thing is that I need to get the latest code and test my changes with that. The other dilemma I have is that the change consists of one commit. I don't want to create a merge commit for one commit, which would look something like this:

git rebase master bugfix merge commit

I would prefer having one commit appear in the master branch, which would look something like this:

git rebase master bugfix rebase

Nice, clean, and straightforward. That's how the master branch will look like after rebasing. To rebase, I can do the following, assuming that I am in the login-bugfix branch:

$ git checkout master
$ git pull
$ git checkout login-bugfix
$ git rebase master

Once I do this, Git will change the base of my currently checked out branch, login-bugfix to what is currently in the master branch. It will look like I branched off of master when it had the most recent changes.

There is one problem. I won't be able to simply push this, because now the local history of login-bugfix is different than what's in the remote history. I will have to force the push by doing the following:

$ git push -f

Now my login-bugfix branch is ready to merge with master and I will get a nice fast-forward merge just like in the diagram from before.

git rebase master bugfix merge after rebase

Caveats

Rebasing is straight forward and what I would consider without risk if I'm working alone or on a branch in isolation. It gets a little tricky when I'm working on a branch with someone else.

Rebasing rewrites history. When I rebase locally and then push it to remote, I'm changing the remote history.

Rebasing when working with another developer

I most likely wouldn't do it, unless I really cared about how clean I wanted the branch to look. Let's say I absolutely need to rebase even though I'm working with a fellow 100x developer, I'll call her Sam.

I have a couple of scenarios:

Sam hasn't made any changes that I don't have

She had to step away to pair program with a 1x developer and then I decided to rebase. Now, Sam has the old version of the branch and it no longer matches remote. She will need to do the following to get the new branch history:

  1. Checkout the login-bugfix branch
  2. Pull the latest changes
$ git checkout login-bugfix
$ git pull

Now, she has the correct history from remote and we can continue to harmoniously collaborate as 100x developers. Pretty straight forward.

Sam has some changes that she's working on, that she hasn't pushed to remote

Sam made some commits here and there to her local copy of login-bugfix and she's ready to push to remote. Pushing to remote will fail and Git will suggest she pull in the latest changes. She can do this, but then she'll have to bring in a dirty merge commit in order to add her changes. In order to keep the remote branch clean she can rebase onto the new remote branch by doing the following:

  1. After git push fails, run git fetch to get the latest copy of origin/login-bugfix from remote
  2. Rebase onto origin/login-bugfix
  3. Resolve any merge conflicts
  4. Push changes to remote: git push
$ git fetch
$ git rebase origin/login-bugfix
$ git push

The funny thing is that if I just merged in master without rebasing, I wouldn't have to get Sam to do any of that stuff. She could just keep committing and pulling to her heart's content, but then we wouldn't have that nice squeaky clean history.

Sam pushed new changes to remote that I don't have

Let's say I finished rebasing login-bugfix onto master and Sam pushed new changes after I did that. I was completely unaware and then I ran git push -f to force the update. In this scenario, all her changes would be gone from remote.

To avoid that situation, I could have ran git push --force-with-lease and my attempt to force an update would have failed, because Git would have detected that my local branch doesn't match remote anymore.

--force-with-lease is another whole topic on it's own, so I'm not going to get talk about that here. I'm just saying that you can give yourself a little more protection by using git push --force-with-lease instead of just git push -f.

That's it for rebasing with Git. Rebasing is much cleaner, but requires a bit more work and understanding for the developers involved, compared to simply merging. It's always good to keep the skill level of the developers you're working with in mind and what they might require to follow standards that are set out by the leadership.

References

  1. Good SO answer on --force-with-lease
  2. Post explaining force-with-lease thoroughly