A case for 1 commit per pull request
Tags: git, workflow, github, codereview
Published: 2023-03-09 04:02:20

Recently I had a coworker introduce the mental model of one pull request containing a single commit. At first I immediately rejected it as heretical. Such an idea seemed to be in opposition to many "best practices" we're taught in learning git. Nonetheless, I heard him out and there are some interesting advantages.

## Address future maintainers while fresh

It's worth asking: what is the purpose of a commit? Here are some real life commit messages from a PR of mine:

```
- Introduce tooltip for Change Lead Time
- Apply text changes proposed by product
- Include small typo fix in admin
```

While these commits may communicate something to an immediate observer of a PR, they may be useless if not confusing months down the road when someone looks at a particular line's commit message with [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#current-line-blame-).

Compare that to the following (shortened significantly for brevity)

```
Force a browser hard navigation if the app is stale

Since we introduced code splitting via splitVendorChunkPlugin() 
with Vite (see PR 7420 19c7355), now Vue SFCs are lazily loaded.

Each module is hashed `[name]-[hash].js` according...
```

Writing a good commit message answers two important questions:
1. What is the overall objective of this PR?
2. How should a developer months/years from now interpret the 'why' of these code changes?

Arguably, these might often be the same question. Nonetheless for me both are easiest to articulate after the initial major code change (i.e., first commit). Waiting until approval to introduce a comprehensive squash commit message often means I may have to re-orient to context and recall the motivations for changes. Context switching is a heavy cost.

Switching to a model of one commit per PR forces one to think long term earlier and to provide helpful context in commit messages that will be available to developers even if version control cloud provider, issue tracker, etc. are changed.

## Encourage better collaboration

One compelling argument for smaller commits is to be able to walk my peers through my process of code changes. This can be very useful for learning no doubt. But arguably it can be accomplished much better with other strategies:

1. Smaller pull requests
2. Comments (PR or in code)
3. Pair programming

Enabling your reviewer with the knowledge to comprehend and adequately review your code changes is important. Enabling your reviewer _and_ your future maintainers is even better. Pairing, github commenting, and smaller PRs help in that they ideally improve your code coherence. But at the end of the day your maintainers (including you) are left with only one resource. The code commit. Why not treat the code commit as a first class equal partner to the code changes it describes?

If developers rely on the suggested tools rather than commit walkthroughs, they will become better collaborators and enablers on their teams. 

## My current git workflow
```zsh
# zshrc
alias gcf='git commit --amend --reuse-message HEAD'
alias gpf='git push --force-with-lease'
```
- branch from master and make a significant change
- `git commit -m 'long commit message'`
- make a change
- `gcf` `gpf`
- make a change that merits explanation to future maintainers:
- `git commit --amend` `gpf`

[Here](https://app.sleuth.io/sleuth/deployments/application/d709c8f-1678297901160-production/timeline) is an example of how I am trying to write commits for better code maintenance. You can even see some of the earlier versions of that same commit in the timeline.

## Potential Exceptions

**Configuration migration**

You're moving an entire configuration from one file to another but you also want to introduce some changes. This happened for my team when we wanted to move eslint config from package.json. Putting these in the same file  first in one commit then noting the changes in another is preferable. See also [git blame --ignore-rev](https://www.michaelheap.com/git-ignore-rev/)

**Branch off of non-trunk branch**

Sometimes it makes sense to branch off of a non-trunk branch. For example, I work with a distributed team that sometimes doesn't get to my PRs for 12+ hours. Sometimes I want to introduce multiple PRs that will depend on changes in a previous. In this case, a tool like GitHub will intelligently watch and once a target branch is merged into main, it will update target of PR to main. Rewriting git history with something like [git commit --amend](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---amend) can break this and will likely require some conflict resolution and manually updating the merge target.