To git or not to git? That is no question. But how?
Up until recently my git knowledge has been limited to simple commands like
git push, maybe an occasional
git commit --amend. These covered most scenarios for our single-branch git setup.
Something changed though. Two things actually.
First of all, we set up continuous integration for our projects (yay fastlane!). Every revision of the app is now build and tested externally. This made me wonder. Do I really need to waste my precious time looking at my computer screen and waiting for all test targets to finish before I push my changes? If I don’t, there’s a chance my commit breaks a build and I risk getting shamed by the rest of my team. That’s when I’ve decided to create a new branch for every feature I develop. My house, my rules. I can now push my changes freely and if there’s any problem CI notifies me without disrupting my teammates work. Sweet 🍭
Secondly, project that we were working on for the past couple months, RocketLuncher, has moved out of the development stage and become a live product. This means that App Store release stopped being a distant future thing but rather an everyday routine. New feature, update, two features and a small bug fix, update. This means that we should always have an app version ready to deploy. That’s when we decided to say goodbye to a single-branch setup and…
Introduced a new workflow!
Basic idea of the new workflow is to have two main git branches:
develop along with multiple feature branches.
master holds app version that’s currently in production and
develop is a development branch where new feature branches are merged, after they are thoroughly tested and reviewed.
develop must always be ready to be deployed to production.
How to start?
First thing to do after assigning oneself to a task is to create a new branch from an up-to-date
develop. You can do that in Terminal with a simple command:
git checkout -b feature/cool_feature. We agreed to name branches with a prefix and a short description:
feature/cool_feature. When we checkout the new branch, we can pretty much unleash our creative developer self and code. The best workflow is to commit every small progress so you have
git commit -m "This" and
git commit -m "That" and push them frequently to a remote repository with continuous integration. That way your whole project is built and tested and you receive immediate feedback if something breaks. You can push a new branch to remote repository with a command:
git push -u origin feature/cool_feature.
Merge requests are best for code review!
When you finish your task it’s time to prepare a merge request. You didn’t think about merging your changes to develop without a code review, right? Right? Good, because merge request is the best way to manage code review. To create it you need to open your git repository website and look for Merge Request button near your feature branch. Various git repository systems handle it slightly different but the idea is similar. This is how we do it in Gitlab:
Once created, merge request looks like this:
Now it’s time for your teammates to step in. You assign one of them to do a code review. They can see your changes either commit by commit in Commits tab or see everything at once in Changes. While they browse through your code they might find something they don’t like. In that case they can leave a comment in that very line describing the problem.
Don’t like the history? Change it!
Once code review is finished and you have fixed all problems pointed out by your reviewer you need to commit and push your code review changes. You end up with a branch with not-so-beautiful commit history:
"Fix X implementation",
"Add Z (work in progress)",
"Implement Z" and finally
"Code review fixes". Normally you wouldn’t be able to change commit’s history once you pushed your changes but it’s your branch, remember? Let’s remake all these commits into one!
In my opinion the easiest way to squash all your commits is to reset your commit’s history back to the last commit from
develop branch. To do this execute
git reset --soft <last_develop_commit_hash>. You end up with all files that you added or modified during development. Make sure all of them are staged with
git add and then commit them with an official commit message like
"Implement cool feature".
Merge or rebase?
You are almost ready to merge your feature branch to
develop. Your code is reviewed, all fixes have been made and it’s even nicely wrapped in one single commit. However if you merge it now you will end up with an ugly merge commit that messes your perfect linear commits history:
Which do you like more? Fortunately there’s a solution to that and it’s called rebasing! Atlassian has a great tutorial regarding difference between merge and rebase:
The first thing to understand about
git rebaseis that it solves the same problem as
git merge. Both of these commands are designed to integrate changes from one branch into another branch—they just do it in very different ways. [Rebase] moves the entire
featurebranch to begin on the tip of the
masterbranch, effectively incorporating all of the new commits in
master. But, instead of using a merge commit, rebasing re-writes the project history by creating brand new commits for each commit in the original branch.
To sum up,
git rebase merges two branches by “moving” all commits that are ahead of the rebased branch on top of that branch.
Let’s try rebase then!
First, checkout branch that you want to rebase to. In our case it’s
develop so call
git checkout develop. You need to make sure you’re up to date with remote repository using
git pull. You’re ready to do some rebasing now. Checkout to your feature branch:
git checkout feature/cool_feature, buckle up run
git rebase develop. You have two options now: either you have no conflicts and rebase succeedes immediately or there are some conflicts. In case of conflicts you will need to resolve them, add modified files to staging with
git add and continue rebasing with
git rebase --continue. When rebase finishes you end up with a branch that has all
develop commits with one additional commit:
"Implement cool feature"! Remember to never pull from remote repository to your rebased branch! You’ll end up with an ugly merge commit that merges your changes with your changes 🙄
So I can’t pull. What about push?
When you try to push your feature branch to remote repository you get an error. Why? Commit tree in git repository is made of parent-child relationships where children point to parents. By rebasing commits in local history you messed with this relationship. How to push your squashed commits to remote repository then? Use the force! Like this:
git push --force. Remember to use
--force only when you’re sure nobody else has pulled your branch before. Remote branch history will change and if they would try to pull again, mystery things might happen 😮
All that’s left to do is merge your feature branch into
git checkout develop and run
git merge feature/cool_feature. Your branch will merge seamlessly and without merge commit!
Is it worth it?
With our new git workflow we managed to accomplish a few goals. The most important is that we separated ready-for-production code from still-in-development code. Also, quality of the code improved because it’s not possible to commit to any official branches without a code review. Code review itself became way more convenient with Gitlab’s merge requests system. Lastly, we don’t have to wait for all test targets to complete locally before push. Continuous integration does it for us and if something fails, only our branch is affected.
I highly recommend you to try different approaches to git. It might improve your workflow and even if you decide to go back then hey, at least you’ve learned some git 😉