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"
Three questions that decide how a file should be treatedA file that a command reproduces exactly is generated and should be marked. One that is generated but has been hand-edited is a source file with a misleading name. One that cannot be reproduced at all should probably not be in the repository.Does a command reproduce this file byte for byte?yes, exactlyMark it generatedcollapsed in reviewyes, but it has editsIt is source nowrename or stop generatingno command existsReconsider committing itor document how it was madethe middle answer is the common one, and the most expensive to leave unresolved

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.

A typical pull request, before and after markingThe raw diff is dominated by a regenerated API client and a lockfile. Once both are marked as generated, the review interface shows only the lines a person wrote, and any size rule counts the same number.changed lines in one pull requestraw diff4140minus the lockfile1180minus generated client96the same change, three ways of counting it

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}'
Committing generated output against generating on demandCommitting it makes a fresh clone immediately buildable and keeps the generator out of the critical path, at the cost of repository size and constant churn. Generating on demand keeps the repository small but makes the generator a hard dependency of every build.Commit the outputGenerate on demandfresh clone buildsimmediatelyafter running the generatorrepository sizegrows with every changeunaffectedreview noiseneeds markingnonegenerator availabilitynot required to builda hard dependencythere is no universal answer — but there is a right answer per artefact

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.