Git Tricks: Keeping branches even
In one of our projects, we use git with 3 main branches: master
, staging
and production
.
master
has all the code that has already been reviewed and approved, staging
has the code on our staging machine and production
has what is currently running for the client. They all follow the same timeline: from master
to staging
, then to production
, so we have something like this:
---------o---o---o->
P S M
Then when we need to merge master
into staging
or staging
into production
we simply need to go to the target branch and merge it with the branch that is ahead of it:
$ git checkout production
$ git merge staging
Using this approach, no merge commit will be created, because this merge is Fast Forward (FF). The commits from the ahead branch are just put after the last commit of the target branch. After that, we should have:
-------------o---o->
P,S M
But yesterday, one thing happened: staging
diverged from master
.
One of my coworkers merged a PR on master, that was not synced with remote, merged it to staging
and pushed it. So we ended up with something like this:
--a-----b---d--> master
\------c----> staging
We just noticed it afterwards, when we tried to merge master
into staging
again and received a prompt to edit the message of the merge commit. An easy fix for it would be to just merge it, but if we did that, all our merges would stop being FF and would generate merge commits, something we don’t want.
Two options were available to solve this problem. I'll walk through both of them now.
Using reset and force push
This approach keeps history clean by rewriting it but you'll need to push to remote using the force option. This can cause some problems when working with other people. Be careful when using this approach.
First of all, cherry-pick the diverged commit from staging
to master
:
$ git checkout master
$ git cherry-pick <commit-C-from-staging-branch>
$ git push origin master
Which leads us to:
--a-----b---d--c*-> master
\------c-------> staging
Notice that a new commit was created on master
. This happened because cherry-pick creates a new commit with the same changes as the selected one.
Now we need to return staging
to the point where it diverged. We can use reset
here to remove the last commit from staging's history.
$ git checkout staging
$ git reset --hard HEAD~1
If you try to push staging
now you won't be able to do it. At this point, your local staging
has diverged from remote staging
. Keep in mind that we're using --force
. This will rewrite history for any person that is dealing with this branch, so sync with them before doing so.
$ git push --force origin staging
Which leads us to the same timeline:
--a-----b---d--c*-> master
|staging
At this time, staging
doesn't have any different commits from master
and is 3 commits behind. In this situation we can just run git merge master
on staging
. It will run smoothly, with FF and no merge commits, to the result of:
--a-----b---d--c*-> master, staging
Using merge commits to avoid force push
This approach avoids the use of the --force
option, so it's safer. But it will add one merge commit to your history line. This is not a big deal but can pollute your history if used frequently.
Start this approach by going to master
and merge staging
into it.
$ git checkout master
$ git merge staging
This will create a merge commit, so you'll be prompted to confirm the commit message. Do it and push staging
to origin, which will lead us to this, with a new merge commit:
--a-----b---d---m-> master
\------c----/--> staging
Notice that staging
is still split from master
. To fix this we need to merge master
into staging
. Since both branches are synced, the merge will be FF, so you can just push it after the merge.
$ git checkout staging
$ git merge master
$ git push origin staging
Now, our two branches will be synced and all our merges from now on will be FF, but notice that our history will stay polluted by the time staging
diverged from master
:
--a-----b---d---m-> master, staging
\------c----/
Git is extremely versatile and flexible so it usually offers different ways to accomplish the same goal. It's up to you to decide which one is best for your git flow. If you use another approach to keep your branches even, feel free to use the comments to share it with us.