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.

What union merge does to two kinds of fileIn an append-only list, keeping both sides preserves every entry and the order is arbitrary but harmless. In a file where lines are edited in place, keeping both sides produces two versions of the same line, which is syntactically valid and semantically wrong.Edited in placeAppend-only listresult of keeping bothtwo versions of one lineevery entry preserveddetectable by testssometimesnot neededcorrect ordermattersarbitrary is finesafe to enablenoyesthe left column is how union merge earns its reputation for being dangerous

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
Two branches appending to one fileEach branch adds a line at the end of the same file. A textual three-way merge sees two different insertions at one position and conflicts. Union merge keeps both insertions, in an order determined by the merge, and produces no conflict.both entries survive the mergemainBMentry ABA1entry BBB1the order of A1 and B1 in the result is arbitrary, which is why the file must be append-only

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
Union merge, with the guard that makes it safeThe attribute removes the conflict, the merge keeps both entries, and a check in the pipeline catches the duplication that would appear if the file stopped being append-only. Without the last step, a changed file structure degrades silently.merge=unionattribute, committedNo conflictboth sides keptDuplication checkheadings, repeatsReview the attributeif the file changed shapethe third box is what turns a silent failure into a build failure

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.