Measuring branch age against conflict risk Jump to heading

“Keep branches short-lived” is standard advice, but it lands as an opinion until someone shows the numbers for their own repository. How much more likely is a conflict for a branch open a week versus a day? How far behind main can a branch drift before merging becomes painful? Git’s history contains the answer: every merge records how long the branch existed and how much main moved in the meantime, and re-running merges shows which ones conflicted. This page measures that relationship for your repository, turns it into a simple limit, and puts the live numbers where people see them, within conflict prevention by design.

When to use this approach Jump to heading

  • The team debates how long branches should live, without data.
  • Merges are frequently painful, and you suspect long-lived branches are the cause.
  • You want an evidence-based limit for branch age or drift, for limiting feature branch lifetime.
  • History uses merge commits, or pull-request metadata records branch open and merge times.

Step 1 — Collect age and drift for past merges Jump to heading

For each merge commit on main, compute two numbers: the branch’s age (time from its first commit to the merge) and its drift (commits main gained between the branch’s base and the merge).

#!/bin/sh
# merge-stats.sh — age (hours), drift (commits) per merge on main
git rev-list --merges --first-parent --since=6.months main | while read -r m; do
  base=$(git merge-base "$m^1" "$m^2")
  first=$(git rev-list --reverse "$base..$m^2" | head -1)
  [ -n "$first" ] || continue
  age=$(( ($(git log -1 --format=%ct "$m") - $(git log -1 --format=%at "$first")) / 3600 ))
  drift=$(git rev-list --count "$base..$m^1")
  echo "$m $age $drift"
done > merge-stats.txt
wc -l merge-stats.txt

For squash-merged repositories, use pull-request data instead: creation time, merge time, and the base commit the forge recorded.

Step 2 — Re-run each merge to see whether it conflicted Jump to heading

The merge commit does not record whether a conflict happened. Re-running the merge with git merge-tree does, without touching any branch or working tree.

while read -r m age drift; do
  if git merge-tree --write-tree "$m^1" "$m^2" >/dev/null 2>&1; then c=0; else c=1; fi
  echo "$m $age $drift $c"
done < merge-stats.txt > merge-conflicts.txt
awk '{n++; c+=$4} END {printf "conflict rate: %.0f%% of %d merges\n", 100*c/n, n}' merge-conflicts.txt

git merge-tree --write-tree exits non-zero when the merge has conflicts; it needs Git 2.38 or newer. The technique is covered in depth in previewing merges with git merge-tree.

From history to a conflict-risk numberFor each merge on main, find the branch base, the branch first commit and the merge time to compute age and drift, then re-run the merge with merge-tree to learn whether it conflicted. Bucketing those results gives a conflict rate per age or drift range.Merge on mainrev-list --mergesAge + driftfirst commit, baseRe-run mergemerge-tree --write-treeBucketrate per rangeno branch or working tree is touched — every step reads history

Step 3 — Bucket and compare Jump to heading

Group merges by age and by drift and compute the conflict rate in each bucket. The shape matters more than the exact numbers: most repositories show a sharp rise after a few days or a few dozen commits of drift.

awk '{ b = ($2<24)?"<1d":($2<72)?"1-3d":($2<168)?"3-7d":">7d"; n[b]++; c[b]+=$4 }
     END { for (k in n) printf "%-5s %4d merges  %3.0f%% conflicted\n", k, n[k], 100*c[k]/n[k] }' \
  merge-conflicts.txt | sort
Conflict rate by branch ageAn illustrative six months of merges in one repository. Branches merged within a day rarely conflicted. The rate roughly tripled for branches open three to seven days, and branches open more than a week conflicted in nearly half of merges.share of merges with conflicts, by branch age (illustrative)< 1 day6%1–3 days11%3–7 days19%> 7 days44%run the script on your own history — the knee of this curve is your limit
# The same by drift: commits main gained while the branch was open
awk '{ b = ($3<20)?"<20":($3<60)?"20-59":($3<150)?"60-149":"150+"; n[b]++; c[b]+=$4 }
     END { for (k in n) printf "%-7s %4d merges  %3.0f%% conflicted\n", k, n[k], 100*c[k]/n[k] }' \
  merge-conflicts.txt | sort -n

Drift is usually the better predictor, because a branch can be old in a quiet week or young in a busy one. Use whichever shows the clearer step.

Step 4 — Turn the knee into a visible limit Jump to heading

Pick the point where the rate climbs — say, drift above 60 commits or age above 3 days — and show it on open pull requests. A warning is enough; a hard block tends to produce worse behaviour, like rushed merges.

# Warn on pull requests whose branch has drifted past the limit
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: |
          base=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
          drift=$(git rev-list --count "$base..origin/${{ github.base_ref }}")
          echo "Branch is $drift commits behind ${{ github.base_ref }}"
          [ "$drift" -le 60 ] || echo "::warning::$drift commits behind — merges past 60 conflict often here. Update the branch."
What to do when a branch passes the limitIf the branch is nearly done, finish and merge it quickly. If it still has work left, update it from main now to reset the drift. If it is blocked or abandoned, close it or park it explicitly so it stops accumulating risk.Branch is past the limit — what state is it in?nearly doneFinish + mergetodaywork remainingUpdate from mainreset driftblocked / staleClose or parkstop the driftthe warning is a prompt to decide, not a penalty

Step 5 — Publish the trend Jump to heading

Re-run the measurement monthly and publish one chart: conflict rate by bucket, and the median drift at merge. If the median drifts upward, review practice is slowing down or branches are getting larger, and the conversation can start from numbers.

awk '{print $3}' merge-conflicts.txt | sort -n | awk '{a[NR]=$1} END {print "median drift at merge:", a[int(NR/2)+1]}'

The related measure — how long pull requests wait for review — often explains why branches age, and is covered in measuring review latency from Git history.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why not just block merges of old branches? Jump to heading

A block encourages people to merge quickly to beat the clock, or to recreate branches to reset their age. A visible warning plus a clear number tends to change behaviour without those side effects.

Does rebasing reset a branch’s age? Jump to heading

It resets the drift, which is the part that drives conflicts, but not the age of the work. That is why drift is usually the better metric to warn on.

What if our history is squash-merged? Jump to heading

Use the forge’s pull request data: created-at, merged-at, and the base commit. Re-run each merge as the merge of the base branch at merge time with the pull request’s head, which the forge also records.