Practical Git Workflows #5: Running Pull Requests — Review Size, Commit Messages, and Draft PRs
We covered the flow of creating and merging your first PR in Git Basics #6: GitHub and Your First Pull Request. Knowing how to open a PR and making PRs run well as a team are different problems. Teams where reviews sit for two days, teams where a 500-line PR gets a single “LGTM”, teams where every commit message just says “fix” — what they have in common is not the tooling but the operations. This post looks at PRs from the operations point of view.
It runs in seven parts.
- #1 Branching strategies — GitHub Flow and trunk-based
- #2 rebase vs merge — decision criteria and the golden rule
- #3 Interactive rebase — cleaning up commits with squash and fixup
- #4 Resolving conflicts — structure, mergetool, and rerere
- #5 Running Pull Requests — review size, commit messages, and draft PRs ← this post
- #6 stash, cherry-pick, bisect — everyday tools
- #7 Monorepos and Git — sparse-checkout, submodules, and LFS
This post walks through the criteria for designing review units, commit message conventions, draft PRs and self-review, and finally the team-level mechanisms: PR templates and branch protection.
Small PRs — designing the unit of review #
If you pick the single most effective principle in PR operations, it is keeping PRs small. There are two criteria.
- One purpose — a PR should be explainable in a single sentence. When the purpose starts reading like a list (“fix login bug + change button color + bump dependencies”), you have already passed the point where it should have been split.
- Reviewable size — once a change goes past a few hundred lines, review quality drops sharply. The moment a reviewer gives up reading the diff to the end and clicks approve, the review has become a formality.
Small PRs get reviewed quickly, receive specific comments, and keep the revert scope narrow when something goes wrong. Large PRs, in contrast, wait longer for review, accumulate conflicts as main moves ahead in the meantime, and eventually drift toward “let’s merge it now and fix it later.”
How to split large work #
Even when you know the principle, some work is simply big. In that case, split PRs by dependency order, not by chronological order.
- Separate the preparatory refactoring. Send the structural changes a feature needs (extracting functions, renames, file moves) as a separate PR with no behavior change. The reviewer only has to confirm that behavior is identical, so even a large diff reviews quickly. The feature PR that lands on top then contains nothing but the new behavior.
- Cut features vertically. Instead of building a whole screen at once, divide it into stackable units: a data model PR, an API PR, a UI PR.
- Stacking is also an option. Create the next branch from the previous PR’s branch, chaining the PRs. When the front PR merges, retarget the next PR’s base to main and let them flow through in order. It works without extra tools, but the management burden grows as the stack deepens, so two or three levels is the realistic limit.
Commit messages — Conventional Commits #
The basics of commit messages (a one-line subject, one change per commit) were covered in the Git Basics series. Teams often add one more layer of structure on top. The most widely used format is Conventional Commits.
type(scope): subject
feat(auth): add social login buttons
fix(cart): correct the total when quantity is zero
docs(readme): update local setup stepsThe type declares the nature of the change, and the scope in parentheses narrows down the affected area. The common types are as follows.
| type | Use |
|---|---|
feat | A new user-visible feature |
fix | A bug fix |
refactor | Structural improvement with no behavior change |
docs | Documentation-only changes |
test | Adding or updating tests |
chore | Build, configuration, dependencies, and other auxiliary work |
The value of this format goes beyond readability. Because it is machine-parsable, it connects directly to automatic changelog generation and automatic version bumps (feat is minor, fix is patch). Commit messages become the input to release automation.
If the subject carries the “what”, the body is where you write the “why”. Background that the code does not reveal, alternatives you considered and discarded, warnings about side effects — put them in the body. For the person digging through git log six months later, the most valuable information is usually there.
Draft PRs — when it is not ready for review yet #
The moment you open a PR does not have to be after the work is done. GitHub’s draft PR is a PR with a “not ready for review” marker attached. It is useful in two ways.
- Sharing direction — open a draft once the structure is in place early in the implementation, and the team can raise objections to the direction while looking at the diff. That costs far less than hearing “the structure is wrong” after everything is built.
- Checking CI first — CI runs on draft PRs too, so you can get tests and linting green before spending a reviewer’s time.
The moment to switch to Ready for review is clear: when you would be willing to merge it yourself. The “open it now and keep building while being reviewed” approach keeps the reviewer’s comments and your changes permanently out of sync, so it is best avoided.
Self-review — read the diff before your reviewer does #
There is one final habit before clicking Ready for review: read your own PR’s Files changed tab from start to finish. What you notice in an editor and what you notice in a diff are different. The usual suspects caught in self-review are predictable.
- Debug output and commented-out code you forgot to remove
- Files that slipped in unintentionally (local configuration, experiment scripts)
- Small edits unrelated to the purpose of this PR
If your reviewer has to point these out, the energy that should go into design review gets spent elsewhere. The healthy division of labor is that self-review and CI filter the mechanical problems, and humans focus on the problems that require judgment.
Review comments — the minimum cultural agreement #
Review culture varies by team, but the minimum agreements that reduce friction are universal.
- Include the intent. “There could be a race when concurrent requests overlap, so I suggest switching this counter to an atomic operation” lands far better than “change this.”
- Use suggested changes. With GitHub’s suggestion feature in comments, the author can commit the fix with a single click. It finishes typos and one-line changes without a round trip.
- Agree on the approval bar. Decide as a team whether approval means “after all comments are resolved” or “approve now, address minor comments after” so the bar does not shift from PR to PR.
Team mechanisms — PR templates and branch protection #
There are two mechanisms you can enforce at the repository level instead of relying on individual habits.
A PR template is a scaffold that automatically fills every PR description when you place a .github/pull_request_template.md file in the repository. With just sections for the summary, the reason, and the verification steps, PRs with empty descriptions disappear.
## What changed
## Why
## How it was verified
- [ ] Tests added or updated
- [ ] Verified locallyBranch protection is the set of rules on the main branch. In the repository settings you can enforce the following.
- No direct pushes to main without a PR
- At least one approving review
- The merge button stays disabled until the designated CI checks (tests, lint) pass
Once the rules stand in for human memory, the “it was urgent so I just pushed” incident becomes impossible by construction.
Merge policy and its relationship to commit cleanup #
The three merge buttons from Git Basics #6 (merge commit, squash, rebase) are where team policy and commit-cleanup effort intersect.
If your team’s policy is Squash and merge, the commits inside a PR are collapsed into one at merge time, which greatly reduces the need to clean up “address review” and “fix typo” commits with the interactive rebase covered in the previous post. In exchange, one PR becomes one commit, so the larger the PR, the lower the resolution of your history. The small-PR principle and the squash policy work best as a set — that is when history reads best.
Conversely, if your team keeps individual commits in history (merge commit or rebase and merge), the habit of organizing commits into meaningful units before opening a PR translates directly into history quality.
Wrap-up #
The takeaways from this post are four.
- Keep PRs to one purpose and a reviewable size, and split large work by dependency order.
- Conventional Commits goes beyond a readable format — it is the input to changelog and release automation.
- Use draft PRs to check direction and CI first, and self-review to filter mechanical problems before Ready.
- PR templates and branch protection turn habits into repository-level rules.
The next post, “Practical Git Workflows #6: stash, cherry-pick, bisect — Everyday Tools”, covers three tools that are hard to do without at the moment you need them: parking work for an urgent switch, moving a single commit to another branch, and finding the commit that introduced a bug.