Git is a tool, and like any tool, there is a big difference between using it and using it properly. I will describe in this article the workflow I’m using in my day-to-day developer’s job.
Starting to Work on a Ticket
When I start working on a Bug, on a User Story, on a Task or on whatever, I always start by checkout a branch containing my name, a short description of the issue and ticket number. It looks like :
git checkout -b tan/fix-event-crash-MIC-1831 origin/master
Saving my work
After a few minutes or a few hours, depending on the complexity of the task, I end up to commit my modifications. I rarely use command line for commit, but use git gui instead:
And I systematically push my branch, also with git gui push button. This has 2 advantages:
- If I screw up my repos, I can still reset on the last time I pushed by branch.
- If my computer crash for any reason, I have a backup of my work.
I also use gitk a lot to see current status of my branch:
Here I can see that I’m based on master, thanks to the
remote/origin/master label. I have 3 commits in my branch and my branch is currently pushed, thanks to the
remotes/origin/tan/feat2 label being the same as local
tan/feat2. So my work is saved.
I really commit/push A LOT. I push several times a day, and sometime up to several times per minutes for very short modifications.
Retrieving Others’ Work
I regularly rebase my branch on current master to retrieve other’s work and address conflicts as soon as possible.
git fetch git rebase origin/master
Note that I rebase, and not merge master into my branch. I want my history to be linear, and the only merge points that appear in history are merged of feature branches into the master branch.
Always Push at the End of the Day
Whatever the status of my code, I always commit my work at the end of the day and push it. I never know what can happen. If I’m in a draft state, I just put everything in a commit and prefix commit message with “WIP”:
Modifying the Yesterday’s WIP Commit
After a good night, I wake up and wonder what the hell I was doing yesterday. I clean up my mess and want to do a clean commit. But I commit an ugly “WIP” commit yesterday. Here comes the awesome “Amend Last Commit” git gui checkbox:
This checkbox let you modify the last commit, rewrite commit message, stage/unstage files etc… So I really have no reasons to not commit a WIP work.
I often do intermediates WIP commit, to explore a path until it leads to something concrete. I also frequently do small dumb fixes or typo correction that should go on older commit. To avoid having a lot of intermediate commits that goes back and forth, I practice interactive rebase. Interactive rebase is a wide topic. You can have a look here to have more details.
In the example below, while fixing a crash, I fix a typo in documentation I previously wrote. To indicate it, I prefix the commit message with an
fixup. I would like this commit to be squashed with the 2nd
add doc commit, and then I would like to squash the
WIP commits with the
fix feat 1 commit.
To do so, I do an interactive rebase:
git rebase -i origin/master
And go from this:
Now my branch is clean, I can publish the merge request and ask for review.
This workflow is well adapted to Trunk based development, but can also be used for any git branching models. The most important points are:
- Rebase branch on master instead of merging master in branch.
- Always push branches, even draft and work in progress branches.
- Learn interactive rebase.
(Bonus) Retrieving Temporary Work of Others
Ideally, development branches should not live more than a few days, or even a few hours, so you should never have to use the work of other developers before it has been integrated in master. But in complex monolithic legacy projects, integration may take days or weeks… In such a case, your work may depend on someone else work that is still not integrated. There are many ways to do this: Rebasing branch on each other, cherry-picking commits, merging branches, … I will describe the method I use.
Assuming I am in this situation. I already have some commit, starting from master:
But I some
bob/fix-db branch to continue my work. This branch is in the integration pipeline, but won’t be available before next week. In this case, what I do is:
# ensure everything is up to date git fetch # save my work. # If I screw up, I can still reset to what I just pushed git push origin HEAD:tan/feat2 # get an overview of my work, and keep it on the side gitk& # now serous things begins. Reset my branch to common trunk git reset --hard origin/master # I start by merging the branch I need # --no-ff force git to create an explicite merge commit # and not flatten the history in case of fast forward. git merge origin/bob/fix-db --no-ff # Maybe I need several branches to continue my work. # I do this for each branches. git merge origin/ben/fix-build --no-ff # now, I retrieve my commits from the opened gitk git cherry-pick ... git cherry-pick ... git cherry-pick ... # using range git cherry-pick sha1...sha2 # And finally, check my branch looks good gitk
History should look like this:
I can see that:
- My branch
tan/feat2start from master
- It first merges the
bob/fix-db. I see this thanks to commit
Merge branch 'bob/fix-db' into tan/feat2.
- The label
remote/origin/bob/fix-dbis just before merge point, so I’m up-to-date with Bob’s work.
- I’ve added 4 commits on top of Bob’s work.
The main advantage of merging dependencies and cherry-picking your own commits, is that you and your colleagues can clearly see in history what is your own work and which branches your work depend on.
There is also another big advantage. Imagine the branch
bob/fix-db broke something during integration and Bob had to fix its branch. By cherry-pick its commits or by basing your branch on its branch, you cannot see this. With the merge technique, you can directly see it:
git fetch gitk
You notice that the
remote/origin/bob/fix-db has disappeared, indicating bob did something to its branch. In this case, just repeat the procedure described previously.
This procedure can also handle case when
pbr/fix-db has finally been integrated in master. In this case, the
git merge origin/pbr/fix-db will tell you this branch is already merge and there are nothing to merge.
Ideally, you should repeat this procedure until all dependent branches has been merged, and send your merge request only when you have only your modifications in your branch.
As said at the beginning, this technique is really a workaround to handle very long integration pipeline. If your work depend on other’s one, it is always better to wait their work to be integrated than doing high-flying gitology.