Skip to content

Instantly share code, notes, and snippets.

@swayson
Created July 27, 2026 08:04
Show Gist options
  • Select an option

  • Save swayson/1f41e97e3cd56a55d99f85088c9b171f to your computer and use it in GitHub Desktop.

Select an option

Save swayson/1f41e97e3cd56a55d99f85088c9b171f to your computer and use it in GitHub Desktop.
branch-provenance flow model

● Spec: branch-provenance flow model

  1. Scope

Given a repo and a target branch (qa), produce:

  • A — transition matrix over branch classes: P(source_class | merged_into=qa)
  • B — per-change promotion paths with per-hop lag
  • C — absorption stats: given a change reaches qa, does it reach main, stall, or get reverted

A is a rollup of B. Build B first; A falls out of it.

  1. Inputs

┌────────────────┬─────────────────────────────────────────────────────────┐ │ Param │ Notes │ ├────────────────┼─────────────────────────────────────────────────────────┤ │ repo │ org/name, plus a local clone (needed for --contains / │ │ │ patch-id) │ ├────────────────┼─────────────────────────────────────────────────────────┤ │ target │ default qa │ ├────────────────┼─────────────────────────────────────────────────────────┤ │ stages │ ordered list, e.g. ["develop","qa","main"] — defines │ │ │ what counts as a skip │ ├────────────────┼─────────────────────────────────────────────────────────┤ │ since │ ISO date; cap history, avoids old-process noise │ ├────────────────┼─────────────────────────────────────────────────────────┤ │ merge_strategy │ merge | squash | rebase | mixed — selects the identity │ │ │ key (§4) │ └────────────────┴─────────────────────────────────────────────────────────┘

  1. Ingestion

Two sources, merged, with provenance recorded per record.

Primary — GitHub PRs. Authoritative for head.ref/base.ref; survives squash. gh pr list --state merged --limit N --json
number,headRefName,baseRefName,mergedAt,mergeCommit,author,labels,additions, deletions Paginate; --limit caps silently. Persist raw JSON to disk — this is the slow step and you'll re-run the analysis many times.

Secondary — merge commits. Covers non-PR merges and pre-GitHub history. git log --merges --first-parent --pretty='%H%x09%P%x09%ct%x09%s' --since= Parse subjects:

  • Merge pull request #(\d+) from ([^/\s]+)/(.+) → PR number + head ref
  • Merge branch '(.+?)'(?: into (.+))? → head ref, base defaults to the branch being walked
  • anything else → source_class = "unknown", keep it (the unknown rate is a quality metric, don't drop it)

Reconciliation: PR record wins on conflict. Join on PR number when present, else merge commit SHA.

  1. Change identity

The key that lets you follow one change across branches. Pick by merge_strategy:

  • merge → merge commit SHA; git branch -a --contains works directly
  • squash → PR number, parsed from squash subject (#123); this is the most reliable key on GitHub
  • rebase / mixed → git patch-id --stable over the diff, as fallback when PR number is absent

Emit identity_method per record. Any change whose identity can't be established goes to an unresolved bucket, counted and reported, not silently dropped.

  1. Branch-class normalization

Raw refs are near-unique, so the matrix is meaningless without bucketing. Ordered rule list, first match wins, config-driven so it's tunable per repo:

^(main|master)$ -> main ^(develop|dev)$ -> develop ^qa$ -> qa ^release/ -> release ^hotfix/ -> hotfix ^revert- -> revert ^dependabot/|^renovate/-> bot ^(feature|feat)/ -> feature ^(bugfix|fix)/ -> bugfix .* -> other

Report the other share. If it's above ~10%, the ruleset is wrong for this repo — surface that as a warning rather than shipping a misleading matrix.

  1. Data model

Change { key # identity per §4 pr_number? # int merge_sha? # str identity_method source: {ref, class} target: {ref, class} merged_at # UTC author size # additions+deletions, for weighting provenance # "gh_pr" | "merge_commit" }

PathObservation { key hops: [ {branch, first_seen_at, method} ] # ordered by first_seen_at reached_main: bool reverted: bool abandoned: bool # in qa, not in main, older than stale_days }

first_seen_at per branch: the merge time of the PR whose base.ref is that branch. Only fall back to git branch --contains + first-parent walk when PR data is missing — --contains gets expensive on large repos and breaks under squash.

  1. Derived outputs

A — transition matrix. Counts (source_class, target_class), row-normalized per target. Also produce a size-weighted variant (weight by size) — a 3-line hotfix and a 2000-line feature are not the same event, and the two matrices diverging is itself a finding.

B — path distribution. Canonical hop sequences with counts and median per-hop lag: feature -> develop -> qa -> main 412 lag: 1.2d / 3.1d / 0.8d feature -> qa -> main 63 SKIP: develop hotfix -> main -> qa 29 BACKFLOW Flag any path violating stages order as skip or backflow. These are usually the whole point of the exercise.

C — absorption. From qa: P(reach main), P(revert), P(stall > stale_days). Break down by source class.

Emit as JSON (machine-readable, feeds a dashboard) plus a text summary. A Sankey over the path distribution is the one visual worth building.

  1. Caveats to bake in as warnings, not footnotes
  • Not actually Markovian. Promotion is policy, not a memoryless process. Label the output "flow frequencies," not "transition probabilities," or someone will compose the matrix with itself and get nonsense.
  • Force-pushes and deleted branches are invisible; PR data survives them, git alone doesn't.
  • Small counts. Any row with n < ~20 shouldn't be reported as a percentage. Show raw counts alongside every rate.
  • Time-varying process. A branch policy change mid-window makes the pooled matrix an average of two different regimes. Support a --split-at date, or at minimum plot the source mix monthly so a regime change is visible.
  1. Build order

  2. Ingest + cache PR JSON → Change records. Report coverage: % of merges into qa with known source.

  3. Class normalization + matrix A. This alone answers the original question.

  4. Path reconstruction B. The bulk of the complexity is here.

  5. Absorption C, monthly trend, Sankey.

Stop after 2 if the coverage number turns out to be poor — no point building path analysis on a stream with 40% unknowns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment