Keeping generated files out of review diffs Jump to heading
A pull request showing four thousand changed lines, of which eleven were written by a person, is not a review — it is a scrolling exercise with an approval at the end. Generated output causes it: lockfiles, protocol buffer stubs, API clients, snapshot tests, compiled assets. The files usually belong in the repository for good reasons, and none of those reasons requires a human to read them line by line. This recipe separates the two questions, within code review workflow engineering.
When to use this approach Jump to heading
- Diffs are routinely dominated by files nobody reads.
- Reviewers have started approving without opening the changed-files view.
- A size rule counts generated lines and is therefore ignored.
- Merge conflicts in generated files are a recurring cost.
- If your repository contains no generated artefacts, there is nothing to mark here.
Step 1 — Inventory what is actually generated Jump to heading
Be precise: a file is generated if a command reproduces it exactly from inputs already in the repository.
# Candidates by size and churn
git log --since='6 months ago' --name-only --format='' \
| sort | uniq -c | sort -rn | head -20 # The decisive test: can it be reproduced byte for byte?
npm run codegen && git diff --exit-code src/api/generated/ \
&& echo "reproducible — genuinely generated" \
|| echo "NOT reproducible — someone has edited it by hand" Step 2 — Mark it so review collapses it Jump to heading
cat >> .gitattributes <<'ATTR'
# Reproducible output: visible in the tree, collapsed in review.
package-lock.json linguist-generated=true
pnpm-lock.yaml linguist-generated=true
**/generated/** linguist-generated=true
*.pb.go linguist-generated=true -diff
*.snap linguist-generated=true
schema.graphql linguist-generated=true
ATTR # Verification: attributes resolve as intended
git check-attr linguist-generated -diff -- \
package-lock.json src/api/generated/client.ts internal/proto/user.pb.go linguist-generated=true collapses the file in the review interface while leaving it diffable locally. Adding -diff goes further: Git treats it as binary and stops producing a textual diff at all, which is right for files that are genuinely unreadable and wrong for ones you occasionally need to inspect.
Step 3 — Prove it stays generated Jump to heading
A marked file that someone hand-edits is worse than an unmarked one, because the edit is now hidden from review as well as from the generator.
# .github/workflows/codegen.yml
name: codegen
on: [pull_request]
jobs:
reproducible:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run codegen
- name: Generated output must match its inputs
run: git diff --exit-code -- 'src/api/generated/**' 'schema.graphql' # Verification locally, before pushing
npm run codegen && git diff --exit-code -- src/api/generated/ \
&& echo "in sync with its inputs" SAFETY WARNING — marking a file generated without a check that regenerates it creates a blind spot: a change to that file is invisible in review and unverified by anything else. For files holding executable code — generated clients, protocol stubs, compiled assets — that blind spot is a genuine supply-chain gap. Never mark a file generated unless a pipeline job reproduces it.
Step 4 — Keep them from conflicting as well as from being read Jump to heading
Generated files conflict constantly, because two branches regenerate them from different inputs. Resolution by hand is always wrong; regeneration is always right.
# Treat it as output during a merge as well as during review
git config merge.generated.name 'regenerate from inputs'
git config merge.generated.driver 'npm run codegen --silent && cp %A %A'
echo 'src/api/generated/** merge=generated' >> .gitattributes # Verification: force a conflict and confirm it resolves without intervention
git merge other-branch 2>&1 | grep -i 'generated' || echo "no manual conflict raised" The same pattern applied to lockfiles specifically is in keeping lockfiles conflict-free during bulk updates, and the general mechanism is in a custom merge driver for lockfile conflicts.
Step 5 — Ask whether it should be committed at all Jump to heading
Marking is a mitigation. For some artefacts the better answer is not to store them.
# What does committing it actually buy?
# - builds work without running the generator → keep
# - the generator needs credentials or a service → keep
# - it is large, churns constantly and is rebuilt anyway → consider removing # If removing: check the size it has been costing
git rev-list --objects --all -- 'dist/**' \
| git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' \
| awk '$1=="blob" {s+=$2} END {printf "%.1f MB of history\n", s/1048576}' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does marking a file generated hide it from reviewers entirely? Jump to heading
No — the file is collapsed by default and can be expanded. The change is to the default, which is what matters: a reviewer who wants to see the regenerated client can, and one who does not is no longer scrolling past it to reach the code.
What about snapshot tests, which are generated but meaningful? Jump to heading
They are the interesting boundary case. A snapshot diff genuinely carries information — it says the rendered output changed — so collapsing it can hide a real regression. Mark them generated but keep them diffable, and make reviewing snapshot changes an explicit step rather than an accident of scrolling.
Should the marking be per file or per directory? Jump to heading
Per directory wherever the generator owns the directory, because a per-file list goes stale the moment the generator emits a new file. Reserve per-file entries for artefacts that live among hand-written code, and prefer moving those into their own directory instead.
Related Jump to heading
- Code Review Workflow Engineering — the parent topic and the other levers on review time.
- Keeping Generated Files Out of Merge Conflicts — the conflict half of the same problem.
- Enforcing Pull Request Size Limits — the rule that depends on this measurement being honest.