Git is a massively popular Version Control System (VCS), and should need no introduction. I've been using it for several years now, and have found it to be an invaluable tool. I am not going to promote it as the "best" VCS as it is the only one I have used, but it is good at what it does.
Git is not without it's shortcomings however, the biggest probably being the user interface - it's not particularly intuitive to use. Even with an understanding of the fundamental commands, it can be difficult to know which to use when, since there are often multiple approaches to achieve the same end.
In this post I outline the git workflow I use to manage my contributions to a git repository, regardless of its size. The workflow is rebase heavy, which will perhaps make it unattractive to some. As to why I use this workflow, its mostly due to the influence of a relatively small number of posts and articles found via HN, /r/git or Google. I have included some of these below, and they elaborate more on the rationale than I do.
I always start work on a new branch (
git checkout -b). If a higher priority task arises then I find coming back to a named branch with a semi-sensible commit message is more helpful than a coming back to a stash.
I Commit Often, Perfect Later, Publish Once. Whenever I have made notable progress on a task it will get committed. By keeping the changeset of each commit relevant to one issue it is easier to clean up local history later.
Occasionally I will find myself in the position where an upstream fix is needed or I have been working independently from the collaborative branch for too long. Here I agree with the author of Why You Should Use a Rebase Workflow - rebasing helps keep history linear and easier to follow.
So, I avoid using the standard pull command when possible (the underlying operations of which are a fetch and a merge). By using
git pull --rebaseor a
git fetchfollowed by
git rebaseit is possible to prevent merge commits by allowing git to rewrite local history.
While the author of Commit Often doesn't personally "hide the sausage making" I agree with the points he makes in its favour. So, when work is completed and ready to be shared, I run an interactive rebase with
git rebase -i. This allows for all them small commits made along the way to be squashed, rearranged and reworded into something clearer to follow.
Something that perhaps gets neglected too often is the importance of the commit message, so I try to follow the seven steps of How to Write a Git Commit Message. If a commit has to be linked to a third party system (Jira or Trac) I consider it a waste of space to add this ID to the subject line. This information can be added to the end of the commit body keeping the subject line free to explain the changes. If you can convince your team to start consistently using a format similar to Conventional Commits' preceding
fix(parser):) then even better. A style like this may even help keep the changesets more focused on a single issue.
With history now presentable, I rebase my branch one final time to bring in any additional upstream changes, run sanity tests, then issue
git pushto share my changes with the public upstream branch.
The Dangers of this Workflow
It is worth mentioning some of the issues that can arise with using a rebase heavy workflow, and at the very least how you can run away from trouble should it find you.
You Bork an Interactive Rebase
You might fix-up the wrong two commits, encroach on someone else's commit or even rebase over the wrong branch. The way to get out of this requires knowing the commit id of your branch prior to the rebase. This is found by either having the foresight to take note of the commit your branch is on before issuing the rebase command, with a
git log /
git rev-parse HEAD or by exploring the
git reflog after the fact. With this information you can undo all the breakages by putting your branch back in the pre-rebase state with a
git reset --hard.
After a Branch is Rebased Tests Fail...
.. and It's Not Your Fault
You brought someone else's shoddy code in and now it's all broken. Worse, since this was a rebase git history now suggests that it was always this way.
Even with a merge however, the problem will still occur and someone will have to fix it. The only difference is with a merge you still have a commit sitting in history which you can use to prove that your changeset once worked (albeit on a now antiquated source tree).
If the code can be proven to be broken on the upstream branch without your changes then this is someone else's doing (or you introduced a bug a long time ago). There are several options open to you, and best choice depends on your experience in and around the area of the bug:
Create a new branch from the upstream and fix the issue yourself. Push the fix up when you are happy that it works as expected. Then rebase your feature branch again so your feature commits sit on top of the fix.
Hold off pushing and notify the relevant party (found, of course, with a
git blame). Some things you just don't have time for, or cannot be fixed properly by you - for instance someone updating a submodule reference but not the submodule itself. Hard reset your branch back to the previously working state. Once the issue has been fixed in the upstream rebase your branch to pull it, and all other changes in.
Just push your rebased branch and assume it'll start working when the guilty party eventually fixes their bug. Obviously, this is not recommended.
... and It's All Your Fault
It is also possible that something went awry when you were rebasing your work onto the newer code base e.g. a conflict was not correctly resolved. The same problem would have happened during a branch merge, though the merge commit itself may have made spotting the incorrect conflict resolution easier.
This can be a problematic issue to correct - at least partly due to the fact there's no one to point the finger of blame at. Here you can either:
Investigate the failing tests/build errors and see what code has been changed in that area. It may be a trival fix, in which case it can be committed and then squashed with an interactive rebase into the appropriate commit.
If the rebase was fairly trivial and there weren't too many conflicts during the process, you could hard reset the branch to the pre-rebase state and try the rebase again. This time with knowledge of roughly what was broken, more care can be taken with conflicts in the relevant areas.
Say to hell with it and hard reset back to the working state and merge instead. This won't magically fix anything in itself, but if it's broken badly enough you have to go asking for advice then git history in this form will be clearer for others to follow.
Git Get Started
Above and Beyond
- Git Internals A look into what git is doing in the background.