Detecting conflict-prone files from history Jump to heading

Git does not record conflicts. A merge commit looks the same whether it resolved cleanly or cost someone an hour, so the obvious query is unavailable and teams fall back on impressions β€” which are dominated by the most recent painful merge rather than by the pattern. There are good proxies, all computable from history alone, and together they identify collision points reliably enough to act on. This recipe builds the measurement, within conflict prevention by design.

When to use this approach Jump to heading

  • You suspect a few files cause most of the pain but cannot name them.
  • Before restructuring anything, so the change targets something real.
  • After a remedy, to check whether it worked.
  • When deciding where to apply attributes or a merge driver.
  • If your repository is a few months old, there is not enough history for the signal to be meaningful.

Step 1 β€” Count appearances in merge commits Jump to heading

The bluntest proxy: files listed in the diff of a merge commit are files the merge had to reconcile.

git log --merges --since='1 year ago' --name-only --format='' \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -20
# Restrict to non-trivial merges, which is a better signal
git log --merges --since='1 year ago' --format='%H' | while read -r m; do
  n=$(git show --name-only --format='' "$m" | grep -c .)
  [ "$n" -gt 0 ] && git show --name-only --format='' "$m"
done | sort | uniq -c | sort -rn | head -20
# Verification: the top entries should be recognisable, not random

A merge commit’s diff is empty when the merge was a clean fast-forward-style combination, so files appearing there are files where the merge machinery had to do work β€” not proof of a conflict, but strongly correlated.

Step 2 β€” Count distinct authors per file Jump to heading

The best single predictor. A file many people edit conflicts; a file one person edits does not, however large it is.

git log --since='1 year ago' --format='@%an' --name-only \
  | awk '/^@/{a=substr($0,2); next} NF{print a "\t" $0}' \
  | sort -u | cut -f2 | sort | uniq -c | sort -rn | head -20
# Combine with size, since a small file with many authors is the worst case
git log --since='1 year ago' --format='@%an' --name-only \
  | awk '/^@/{a=substr($0,2); next} NF{print a "\t" $0}' | sort -u | cut -f2 \
  | sort | uniq -c | sort -rn | head -10 | while read -r n f; do
      [ -f "$f" ] && printf '%2s authors  %5s lines  %s\n' "$n" "$(wc -l < "$f")" "$f"
    done
Authors per file, across a yearMost files are edited by one or two people and rarely conflict. A small tail is edited by many, and that tail contains almost every file people complain about. Combining author count with file size sharpens it further: small files with many authors are the worst case.files by distinct author count1 author4122-3 authors1864-6 authors447 or more9the nine files on the right are where the measurement should point you

Step 3 β€” Find files that change together Jump to heading

Co-change reveals coupling that the directory structure hides, and coupled files conflict as a group.

git log --since='1 year ago' --format='---' --name-only \
  | awk '/^---/{if (n>1 && n<12) for (i=1;i<=n;i++) for (j=i+1;j<=n;j++) print f[i] "\t" f[j];
         n=0; next}
        NF{f[++n]=$0}' \
  | sort | uniq -c | sort -rn | head -15
# Verification: the top pairs should make architectural sense

Pairs that change together in most commits are effectively one unit. If they are also edited by many people, the pair is a collision point even though neither file looks remarkable alone.

Step 4 β€” Separate structural from circumstantial Jump to heading

A file that has always conflicted is structural. One that conflicted last quarter is a busy quarter.

# Split the window in half and compare the rankings
for window in '2 years ago::1 year ago' '1 year ago::now'; do
  since=${window%%::*}; until=${window##*::}
  echo "--- $since to $until"
  git log --merges --since="$since" --until="$until" --name-only --format='' \
    | grep -v '^$' | sort | uniq -c | sort -rn | head -5
done
# Persistent across both windows AND across changing authors β†’ structural
git log --since='2 years ago' --format='@%an' --name-only -- src/routes/index.ts \
  | awk '/^@/{print substr($0,2)}' | sort -u | wc -l
Reading the measurementA file at the top of both windows, edited by a changing set of people, is a structural collision point. One that appeared recently is circumstantial. One that appeared in a single burst is usually a refactor that has already landed.Is the file near the top of both time windows?yes, and authors changedStructuralreshape itonly the recent windowCircumstantialwait and re-measureone concentrated burstA past refactornothing to doacting on the middle or right branch wastes effort and usually makes the code worse

Step 5 β€” Turn it into a scheduled report Jump to heading

A measurement run once produces a conversation; run quarterly it produces a trend.

#!/usr/bin/env sh
# scripts/conflict-report β€” the three proxies, in one output.
set -eu
since=${1:-'1 year ago'}

echo "=== files in merge commits ==="
git log --merges --since="$since" --name-only --format='' \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -10

echo; echo "=== distinct authors per file ==="
git log --since="$since" --format='@%an' --name-only \
  | awk '/^@/{a=substr($0,2); next} NF{print a "\t" $0}' \
  | sort -u | cut -f2 | sort | uniq -c | sort -rn | head -10

echo; echo "=== attributes already in place ==="
git check-attr merge -- $(git log --merges --since="$since" --name-only --format='' \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -5 | awk '{print $2}') 2>/dev/null
# Verification: the report runs in seconds and fits on a screen
sh scripts/conflict-report '6 months ago' | head -30
Using the measurement over timeMeasure to find the collision points, apply one remedy, wait a quarter for enough new history, and measure again. Changing several things at once produces a second measurement nobody can attribute.Baselinethree proxies recordedweek 0One remedythe top file onlyweek 1Enough historynew merges accumulatedweek 13Re-measureattributable resultweek 14one remedy per cycle is what makes the comparison mean anything

The last section of the report matters as much as the first two: a file at the top that already has a merge attribute is telling you the attribute is not covering the case, which is a different problem from a file nobody has addressed.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why does Git not record conflicts? Jump to heading

Because a merge commit records a result, not the process that produced it β€” the same commit could have been created cleanly, by hand, or by a tool. The only durable trace is rerere’s cache, which exists solely on the machine that resolved the conflict and only when rerere is enabled.

Can rerere’s cache be used as a data source? Jump to heading

On one machine, yes: the number of entries in .git/rr-cache is a direct count of distinct conflicts that person resolved. It is not shared by default and does not aggregate across a team, though sharing rerere resolutions across a team describes a setup where it could be.

What about very large repositories where these commands are slow? Jump to heading

Restrict by path and by window, and run the report on a schedule rather than interactively. The author-count query is the expensive one; limiting it to the top twenty files from the merge query gives nearly the same answer for a fraction of the work.