Keeping generated files out of merge conflicts Jump to heading
A conflict in a generated file has no manual resolution that is defensible. Picking lines from two versions of a resolved dependency graph, a compiled schema or a generated client produces an artefact that corresponds to no input — it may parse, it may even build, and it describes a state nobody chose. The correct resolution is always the same: take either side, then re-run the generator against the merged inputs. A merge driver makes that automatic. This recipe builds one, within merge strategies and gitattributes.
When to use this approach Jump to heading
- The same generated file conflicts on most merges.
- Someone has resolved a generated file by hand and something broke afterwards.
- Your repository commits lockfiles, protocol stubs, API clients or compiled schemas.
- The generator is deterministic and available on every developer machine.
- If the generator needs credentials or a service that is not always reachable, a driver will fail at awkward moments; prefer regenerating in CI and marking the file generated.
Step 1 — Confirm the generator is deterministic Jump to heading
A driver that regenerates non-deterministically converts a merge conflict into a permanent diff.
npm run codegen && git diff --exit-code src/api/generated/ \
&& echo "deterministic — safe to drive" \
|| echo "NOT deterministic: output differs from what is committed" # Run it twice and compare, to rule out timestamps and ordering
npm run codegen && sha256sum src/api/generated/client.ts
npm run codegen && sha256sum src/api/generated/client.ts # Verification: both hashes match Non-determinism is usually a timestamp, a map iteration order, or an embedded version string. All three are worth fixing regardless of merging, because they also make every unrelated change look like a change to the generated file.
Step 2 — Write the driver Jump to heading
Git passes three temporary files: %O the common ancestor, %A our version, %B theirs. The driver must leave the result in %A and exit zero.
# The general shape: take one side, then regenerate from the merged inputs
git config merge.generated.name 'regenerate from source instead of merging'
git config merge.generated.driver 'npm run --silent codegen && cp "%A" "%A"' # For a lockfile, where the inputs are the merged manifests
git config merge.lockfile.name 'regenerate the lockfile from the manifest'
git config merge.lockfile.driver \
'cp "%B" "%A" && npm install --package-lock-only --silent --ignore-scripts' # Verification: the driver is registered and its command is what you expect
git config --get merge.lockfile.driver The cp "%B" "%A" step matters even though the content is about to be overwritten: some generators read the existing file to preserve ordering or comments, and starting from a valid file rather than a conflicted one avoids confusing them.
Step 3 — Point the attributes at it Jump to heading
cat >> .gitattributes <<'ATTR'
package-lock.json merge=lockfile linguist-generated=true
pnpm-lock.yaml merge=lockfile linguist-generated=true
src/api/generated/** merge=generated linguist-generated=true
schema.graphql merge=generated linguist-generated=true
ATTR
git add .gitattributes && git commit -m 'chore: regenerate rather than merge generated files' # Verification: paths resolve to the intended driver
git check-attr merge linguist-generated -- package-lock.json src/api/generated/client.ts Step 4 — Distribute the driver to every clone Jump to heading
Attributes are committed; drivers are not, because a driver is a command. Every clone needs the definition installed.
# In the committed team configuration, included by each clone once
cat >> .gitconfig-team <<'INI'
[merge "lockfile"]
name = regenerate the lockfile from the manifest
driver = cp \"%B\" \"%A\" && npm install --package-lock-only --silent --ignore-scripts
[merge "generated"]
name = regenerate from source instead of merging
driver = npm run --silent codegen
INI git config --local include.path ../.gitconfig-team # Verification: the driver resolves in a fresh clone after the include
git clone "$(git remote get-url origin)" /tmp/fresh
(cd /tmp/fresh && git config --local include.path ../.gitconfig-team && git config --get merge.lockfile.driver) SAFETY WARNING — a clone without the driver installed falls back to a textual merge of a generated file, silently producing exactly the corrupt artefact the driver exists to prevent. The failure is invisible in the merge and appears later as an install that resolves unexpected versions. Check for the driver in the configuration audit, and have CI verify that the committed artefact matches its inputs on every pull request.
Step 5 — Verify the result in CI Jump to heading
The driver makes merges deterministic; CI is what proves they were also correct.
- run: npm ci --ignore-scripts
- name: Generated output matches its inputs
run: |
npm run codegen
npm install --package-lock-only --ignore-scripts
git diff --exit-code -- 'src/api/generated/**' package-lock.json # Verification locally before pushing
npm run codegen && npm install --package-lock-only && git diff --exit-code && echo "in sync" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if the driver fails during a merge? Jump to heading
Exiting non-zero leaves the path conflicted, which is correct: the merge stops and a person decides. That is much better than a driver that swallows errors, because a silent failure here produces a plausible-looking artefact nobody will question.
Should generated files be committed at all? Jump to heading
It depends on whether a fresh clone must build without running the generator. Committing them makes the repository self-contained at the cost of churn; generating on demand keeps the repository small but makes the generator a hard build dependency. The trade is laid out in keeping generated files out of review diffs.
Does the driver run during a rebase or a cherry-pick? Jump to heading
Yes — all three use the same merge machinery, so the attribute and the driver apply identically. That is usually welcome, though it means a long rebase will run the generator once per conflicting commit, which is worth knowing when a replay seems unexpectedly slow.
Related Jump to heading
- Merge Strategies & gitattributes — the parent topic and the three levels of control.
- A Custom Merge Driver for Lockfile Conflicts — the lockfile case in full.
- Keeping Lockfiles Conflict-Free During Bulk Updates — where these conflicts arrive in volume.