Thoughts on git commits, branches, PRs and stacking
At work, we’ve been trying out Graphite for the past year or so. It’s a complement to our GitHub account which helps us manage stacked PRs.
After trying the “stacking” workflow in the context of our small team, I wrote up some thoughts internally on how we use git. Hopefully, this will help us think more clearly about how to use our version control system to maximum effect. I figured I’d post my reflection publicly, too.
Primitives in version control
- Commits are mandatory; we can’t use git without commits.
- Branches can let us have multiple “latest” commits in the repo. We use this usually to separate “work in progress” from “work that is integrated”. Anither use of branches is to distinguish “latest changes from the dev team” from “latest deployment”. Using more than one branch is not mandatory!
- Pull requests are a social technology built on top of branches. They are a request for social interaction before a branch is merged with the trunk.
- Stacking is another social technology which makes it convenient to maintain multiple related pull requests. (Maybe it’s a stretch to call stacks a “primitive”!)
Commits
A commit is an atomic change to the codebase. It can touch multiple files, if a single logical change is physically distributed across the filesystem.
- For the benefit of future investigation using
git blame
, make sure a commit is coherent. It should have a single purpose. One change should be completely implemented in one commit, and each commit should implement only one change. - For the benefit of future debugging using
git bisect
, make sure a commit doesn’t break the app. If the app is in a broken state after a commit, then we will not be able to run the app to determine if the commit introduced a bug. - Conventional commits introduces the idea that you can use the commit message for more things in the future: e.g. automating a changelog. This adds more meaning to the commit message itself, which is why it must follow a specific format.
Branches
A branch refers to the “tip” of a linked list of commits. Merge commits join two branches into one, so that commits form an acyclic graph.
- Branching away from the trunk allows us to sequester away “work in progress” so that developers can avoid stepping on each others’ toes. We prefer to do this rather than to do all work in the main branch, though some teams have the opposite preference.
- Branches can be a point of integration with other tools; for example, we use Linear which can automatically link issues to branches with particular naming conventions.
- “Work in progress” branches often end up looking like a raw history of ad-hoc editd, rather than a curated sequence of atomic and coherent commits. This is because the branch is “hidden away” and the consequences of messy commits in a branch are zero… until it is merged.
Pull requests
A pull request is a social construct built atop branches. It is a “request” from one developer to merge one branch into another. (And yes, I prefer GitLab’s “merge request” terminology, but unfortunately it’s the less used term.)
- It is assumed that the requester and the merger will be different people, though solo developers may also use a PR based workflow too…
- Because PRs provide another point of integration for things like:
- Code review, these days including code review performed by tools ranging from linters to security scanners to LLMs.
- Automated testing, especially when the full test suite is too heavy to run on every single commit pushed to the VCS
- Preview deployments (though we are not doing this)
- PRs provide an opportunity to add more human-focused details to a branch, e.g. screenshots of what the changes look like, discussion threads, approval processes, including e.g. for certifications like SOC2.
- Because of these social benefits, a PR might only contain a single commit. But our PRs usually come from a branch with many commits over multiple days of work. As noted above, our branches are often full of “work in progress” commits which aren’t cohesive.
- However, a PR can be a great opportunity to review your own work and rebase the commits to become more coherent once the work is “complete” enough to be merged. Rebasing a series of 10 wip commits into 4 coherent changes is a gift to the future of the codebase.
Stacks
Stacks are a collection of related PRs, often with sequential dependency between them. E.g. the PR that implements a frontend feature depends on the PR that implements the backend for the feature. These two PRs could be managed using a stacking tool like Graphite.
The stated purposes of stacking workflows are:
- Preventing developers from being “blocked on review”, allowing them to keep working on top of a pull request while it’s still not merged. This is a technical solution to the social problem of slow/asynchronous PR reviews.
- Make it easier to work on related branches while keeping them all up-to-date with each other. This is a technical improvement to git - the same approach could be done manually with git commands, it would just be more annoying.
Outcomes
- We have noticed a tendency for stacks to grow and accumulate more and more unmerged related PRs. This may be because we often work on fairly large features which need to be broken up into many parts. Our PR reviews being slow due to the team being small and busy also make it attractive to “move on” to another PR on the stack.
- We don’t often rebase or clean up our branches when creating a PR. This can exacerbate problems further downstream, e.g. making it harder to create cleanly-separated stacks.
- We’ve adopted the “conventional commits” format in our commit messages, but we aren’t yet doing anything with the commit messages. This has led to some looseness in the way we use our commit tags, as we can select any commit type and it doesn’t affect anything.
Thinking this through reminded me of the importance of commits themselves. I’d like to try harder to create exemplary commits for my team, which should flow on into better PRs, more helpful git blame
, and hopefully a reduced need for stacking tools.