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.
I have my branch and I start debugging. As I'm working away, the master branch starts moving forward.
I figured out what I need to do, so I start fixing the bug. In the meantime the master branch progressed again.
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:
I would prefer having one commit appear in the master branch, which would look something like this:
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.
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:
- Checkout the
login-bugfix
branch - 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:
- After
git push
fails, rungit fetch
to get the latest copy oforigin/login-bugfix
from remote - Rebase onto
origin/login-bugfix
- Resolve any merge conflicts
- 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.