My Uncomplicated Git Workflow
Written on November 12, 2015
I'm writing a book about building and deploying web applications with Haskell and Yesod. Want to know when it's released? Click here.
Recently I have been surprised with how often I see developers struggle with their version control system. Given that using version control is such a central part of any serious software endeavour, struggling with it must be a great way to kill developer productivity and potentially lose work.
I don’t find it valuable to spend brain cycles context switching between thinking about what my production code should look like and trying to remember which Git command and with which flags is appropriate in any given context. For this reason, my Git workflow is about as simple as I can make it.
My workflow exists exclusively in the terminal, and leans heavily on Git aliases and a Ruby gem (don’t laugh) called git-smart. The gem seems not to be actively maintained any more, but it still works regardless. I don’t use git-flow, and I advise against using it because it is needlessly complex.
Reasons for only using Git in the terminal:
- I want to protect my arms, wrists, hands, and fingers by not having to shuffle a cursor around the screen with a mouse or trackpad. Typing only.
- My terminal is easy on the eyes with its dark theme so I suffer from fewer headaches when working.
- I am unable to fathom how anyone would find a GUI application like SourceTree less intimidating than Git’s command-line interface.
A typical working session for me begins with pulling the latest changes from the remote repository. I use the
git smart-pull command provided by the git-smart gem for this, and I have the command aliased in my global
.gitconfig so I only need to type
git sp. The git-smart gem will first run
git fetch origin, and then figure out the best way to pull changes from remote, rebasing if necessary. It’s also nice that it tells you which commands it’s running and when, so you can learn more about Git while it makes decisions for you.
After I have spent time working on a task, I will check the status of the working directory with the standard
git status command, which I have aliased to
git st. I use the standard output because I can scan it more quickly with my eyes. In most cases, I already know what the status of the working directory will be before checking the status, but I run the command anyway as a simple sanity check. I am usually able to scan the status in a fraction of a second.
Assuming checking the status didn’t give me any surprises, I inspect the work I did in greater detail with
git diff. I don’t alias this command, because I find it comfortable enough to type at speed. I do make an effort to keep my commits small, so I am usually able to scan the diff in about a second.
Now I am ready to begin staging my changes. In the cases when I want to stage everything — and this is the most common case — I use the
git add --all command, which I have aliased to
git aa. If on the other hand I feel my work should be split into multiple commits, I’ll stage hunks of changes with
git add -p. I use this command often, but not often enough to justify creating an alias for it. Using the
-p flag when staging puts you in an interactive patch mode that asks you which hunks to stage. Answer
y for yes,
n for no, and
q to quit patching. Sometimes the hunk it presents to you contains more code than you wish to stage, and at that point you can answer
s for split.
Sometimes I feel the need to check once again which changes are staged, but
git diff alone won’t work here. For staged changes, you need to run
git diff --cached, which I have aliased to
When I’m ready to commit my changes, I run
git commit, which I have aliased to
git ci. I almost never use the
-m flag when committing. Instead, Git opens up my text editor so I can comfortably write a proper commit message there.
If I forget to add some changes to the commit and/or I want to change the commit message, I use
git ci --amend. If I want to undo the commit entirely, I use
git reset HEAD~1.
If there are no other commits to make, I will run
git sp again to ensure I have the latest changes locally, and then I run
git push to share my work with my team.
Working with branches
I’m generally not a fan of the pull-request model of collaboration, and I don’t like the idea that a team of developers can’t trust one another enough to commit directly to master. The pull-request model usually involves a synchronous code-review step, where the PR can not be merged into master until it has been approved by other members of the team. It’s unfortunate that we have tools that cater to an asynchronous workflow, and yet use them synchronously anyway. The PR model can be a necessary evil however if a project doesn’t have any tests.
When I do need to use branches, I make liberal use of
git checkout, which I have aliased to
git co. If I want to create a new branch, I use
git co -b <branchname>. If I want to switch back to the previous branch I was on (I sometimes shuffle quickly between a feature branch and master), I use
git co -. The checkout command is also handy for clearing away unstaged changes that I don’t care for anymore, with
git co -- ..
If I need to merge from a feature branch back into master, I use the
git smart-merge command from git-smart. I don’t use this often enough to bother creating an alias. This is another one of those commands that decides the best strategy for merging given some context, and it’s always just done the right thing for me.
The view from 10,000 feet
From time to time, I like to briefly check how the project has been progressing, and again git-smart pulls through for us here providing the
git smart-log command, which I have aliased to
Sometimes I’ll be half-way through working on a task and I will have to context-switch to another task. I don’t want to commit my work in an unfinished state, but I do want a clean working directory. In this case, I use
git stash. When I want to retrieve my unfinished work, I use
git stash apply, followed by
git stash drop assuming nothing went wrong when applying the latest stash. This is safer than using
git stash pop directly.
Problems in the wild
There is a caveat to using git-smart, and that is it doesn’t play nicely with Git submodules. What happens is, a submodule that is yet to be updated will make the working directory appear dirty, when in reality it isn’t. When git-smart sees a dirty working directory, it’ll stash your changes before pulling, and then pop them after pulling, which will pop the wrong stash which could potentially cause problems. In practice, this shouldn’t be an issue because submodules should be avoided anyway.
Learning to harness the power of Git properly is a key factor in communicating with colleagues effectively, and it also makes projects far easier to maintain.
Uninstall SourceTree. Abandon git-flow.