Union merge for append-only files Jump to heading
Two branches add an entry to the top of a changelog. Neither touched the other’s line, both added at the same position, and Git has no basis to order them — so it conflicts, and someone resolves it by hand for the fiftieth time. Union merge tells Git to keep both sides’ lines instead of conflicting, which is exactly right for a file whose semantics are “a list that grows” and dangerous for anything else. This recipe applies it safely, within merge strategies and gitattributes.
When to use this approach Jump to heading
- A changelog, decision log or similar list conflicts on most merges.
- Entries are independent: keeping both in either order is correct.
- The file is never edited in place, only added to.
- The conflicts are pure noise and always resolved the same way.
- If entries reference each other, or the file has a total ordering that matters, union merge will produce something plausible and wrong.
Step 1 — Prove the file is genuinely append-only Jump to heading
This is the entire safety argument, so check rather than assume.
# How often are existing lines modified rather than added?
git log --numstat --format='' -- CHANGELOG.md | awk '{add+=$1; del+=$2} END {
printf "%d added, %d deleted (%.0f%% deletions)\n", add, del, 100*del/(add+del)}' # A file with very few deletions relative to additions is append-only in practice
git log -p --follow -- CHANGELOG.md | grep -c '^-[^-]' # Verification: inspect a sample of the deletions that did happen
git log -p -- CHANGELOG.md | grep -B2 -A2 '^-[^-]' | head -20 A file with 5% deletions is append-only with occasional typo fixes, which is fine. One with 30% is edited in place and union merge will eventually produce a duplicated section nobody notices.
Step 2 — Enable it per path Jump to heading
cat >> .gitattributes <<'ATTR'
CHANGELOG.md merge=union
docs/decisions/index.md merge=union
.github/CODEOWNERS merge=union
ATTR
git add .gitattributes && git commit -m 'chore: union-merge the append-only lists' # Verification: the attribute resolves for the intended paths and no others
git check-attr merge -- CHANGELOG.md README.md docs/decisions/index.md union is built into Git, so unlike a custom driver it needs no local configuration and works for everyone on checkout. That makes it the cheapest conflict removal available when it applies.
Step 3 — Test it against a real conflict Jump to heading
git switch -c u-a main && printf -- '- fix: clamp the refund window\n' >> CHANGELOG.md \
&& git commit -am 'docs: changelog entry A'
git switch -c u-b main && printf -- '- feat: add retry metrics\n' >> CHANGELOG.md \
&& git commit -am 'docs: changelog entry B' git switch u-a && git merge u-b
tail -4 CHANGELOG.md # Verification: both entries are present and no conflict markers remain
grep -c '^<<<<<<<\|^=======\|^>>>>>>>' CHANGELOG.md # expect 0
git switch main && git branch -D u-a u-b Step 4 — Handle the ordering that union merge does not give you Jump to heading
Both entries survive; their order is whatever the merge produced. For a changelog that usually does not matter, and where it does, the fix is to generate the file rather than to merge it.
# Check the result is at least deterministic for a given pair of inputs
git merge u-b && sha256sum CHANGELOG.md # If order matters, generate the changelog from commit history instead
git log --format='- %s (%h)' "$(git describe --tags --abbrev=0)"..HEAD Generating is strictly better where it is possible, because the file stops being a source of conflicts entirely — the approach in automating changelog generation with semantic-release.
SAFETY WARNING — union merge silently keeps both sides, so a file that stops being append-only will start accumulating duplicated content with no conflict to alert anyone. Review the attribute when the file’s structure changes — a changelog that grows a “Unreleased” section people edit in place is no longer append-only, and the duplicate headings will appear weeks before anyone connects them to the attribute.
Step 5 — Add a check for the duplication union merge can cause Jump to heading
# Duplicate headings or repeated identical entries are the tell
awk '/^## /{if (seen[$0]++) print "duplicate heading: " $0}' CHANGELOG.md
sort CHANGELOG.md | uniq -d | grep -v '^$' | head # In CI, as a cheap guard
- name: Changelog has no duplicated sections
run: |
dupes=$(awk '/^## /{print}' CHANGELOG.md | sort | uniq -d)
[ -z "$dupes" ] || { echo "::error::duplicated changelog sections:"; echo "$dupes"; exit 1; } # Verification: introduce a duplicate deliberately and confirm the check fires
printf '## 2.4.0\n' >> CHANGELOG.md && awk '/^## /{if (seen[$0]++) print "caught: " $0}' CHANGELOG.md
git checkout -- CHANGELOG.md Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does union merge work for CODEOWNERS? Jump to heading
Usually yes, because it is a list of rules that grows. The caveat is that ownership resolution takes the last matching rule, so an arbitrary merge order can change which rule wins for a path. Keep patterns non-overlapping, or accept that the file needs a person when two branches add rules for the same path.
What about YAML or JSON lists? Jump to heading
Do not. Both formats have structure that union merge does not understand — it will produce two version: keys or a broken array, and the file will fail to parse rather than merging incorrectly, which is at least loud. For structured data, a merge driver that parses and merges properly is the right tool.
Can union merge lose anything? Jump to heading
Not lines: it keeps both sides. What it loses is the signal that two people changed the same area, which is sometimes worth knowing. For a changelog nobody wants that signal; for anything where concurrent edits might indicate a coordination problem, the conflict was doing useful work.
Related Jump to heading
- Merge Strategies & gitattributes — the parent topic and where union sits among the options.
- Keeping Generated Files Out of Merge Conflicts — the better answer when the file can be generated.
- Reducing Lockfile Churn in a Busy Repository — the same problem in a file where union merge would be catastrophic.