February 23, 2021 • Reading time: 12 minutes
At this point, most developers use Git as a tool for collaboration. We have our
rote-learned commands to pull, commit, and push. And of course, there’s that
one coworker who knows a bit more about Git than
everyone else, who helps get us back on track whenever our local repos end up in
a strange state.
But what if I told you that Git can be a valuable tool without ever setting up a
remote repository? I’m not just talking about having a working version of your
code base to roll back to if you mess something up, although there’s that too.
Used correctly, Git can help to structure your work, identifying gaps in your
test coverage and minimizing dead code.
There are two subjects I’m going to avoid for the purposes of this blog post:
other developers, who are the most compelling but least interesting argument
for keeping your commit history clean, and git bisect
, which does factor
heavily into my workflow but deserves its own blog post.
As with any ubiquitous developer tool, the Git user base has a lot of strong and
conflicting opinions about the one “correct” way to use it. My goal is simply to
introduce a workflow that I’ve been using and refining for much of my career;
take from it what you will. And, importantly, it’s a workflow that has become a
vital part not just of my collaboration process, but of the way I write code.
Ultimately, these principles serve two purposes: they focus my work onto a
particular bugfix, feature, or goal, and they ensure that my Git history isn’t
set in stone. With proper hygiene, commits can be dropped, rearranged, and split
off into other branches painlessly and without merge conflicts.
Principle 1: A branch must do one useful thing
When I’m managing my own projects, I have a lot of ideas that I want to see
happen. If I’m just throwing one commit after another into main
, I’ll get
halfway through implementing one feature and then jump off to hacking on
another. If any of the features get completed, it will be at the expense of a
wasteland of half-completed features that are now taking up space in my code
base.
In a brand-new project, sure, I’ll throw a bunch of garbage commits into main
.
My rule of thumb for when to stop this is when I can write my first effective
integration test. If there is something useful to test, there is now enough
substance to my project that I can have distinct tasks on the go. Trying to
break into branches too early just results in me throwing my garbage commits
into a branch instead of main.
In the early stages of a project, articulating the purpose of a branch can be as
simple as giving it a descriptive name. If a commit isn’t moving the code base
in that direction, it can always get cherry-picked into a different branch.
As the project matures, I’ll start using some sort of issue or bug tracking
software to flesh out what I’m trying to accomplish in more detail and
coordinate the branches for multiple related useful things.
I find that descriptive branch names also help to refocus my attention on what
I’m trying to accomplish. For instance, my command prompt currently looks like
this:
10:02:19 max ~/Projects/mikkel.ca blog-post-git-as-a-solo-developer| R%
Principle 2: Every commit must be independent
So much for branches, let’s zoom into a commit level. I’ve articulated what
concrete thing I want my branch to add, now how do I add it? Usually, there’s
some poking around my code base involved in figuring that out. Sometimes I take
a wrong turn, sometimes I just