How many long-lived maintenance branches should a project keep, and how do you bound the backport cost?
answer
- cost is per fix per live line
- drift makes old lines expensive
- a support promise sets the number
- deletion is cheap, tags keep history
- audit before every release cut
basics
~20 sAs few as the support promise requires — each extra live line multiplies every fix by another merge or cherry-pick, retest and tag. Bound the cost with a published end-of-life date, strictly ordered merge-up, and no refactoring on maintenance lines.
solid answer
~50 sCost scales with the number of live lines times the number of fixes, and it grows superlinearly because each line drifts further from the others over time. So the count should follow an explicit support promise — typically the current line plus one or two predecessors — with a published end-of-life for each, after which the branch is frozen and only its tags remain. Bound the per-fix cost with structure: keep the lines strictly ordered so a fix can be committed on the oldest affected one and merged forward rather than fanned out; forbid refactors, formatting sweeps and dependency upgrades on maintenance lines because textual drift is what makes a cherry-pick stop applying; tag every shipped build with an annotated tag so `git tag --contains <sha>` answers "which versions are affected" instantly; and make a `git cherry -v` audit part of cutting each release so nothing is silently missing.
code
bash · 6 linesgit tag -l 'v1.4.*' --sort=-v:refname # releases on one line, newest first
git log --oneline --cherry-pick --right-only main...release/1.4 # not yet upstream
git tag --contains 9fceb02 # which releases carry this fix
git branch -d release/1.3 # retire the line; tags keep history
git switch -c release/1.3 v1.3.9 # recreate it if ever neededgo deeper
Recall the core tradeoff: each supported release line means every fix must be applied and released again there, so the number of live branches is a cost, not a free safety net.
Explain the mechanisms that keep the cost down — ordered lines with forward merges, annotated tags on every shipped build, and maintenance branches that only ever receive fixes.
Show how you operate it: an audit before each release cut, reachability queries to answer which versions are affected, and a hard line against changes that widen the drift between branches.
Own the policy end. Derive the branch count from a published support promise with end-of-life dates set at cut time, and be explicit that fewer lines shift upgrade pressure onto customers — a product decision you make deliberately.
## The cost model The question is a budgeting question dressed as a Git question. Every additional supported line multiplies the work for *every* fix that applies to it: a port (merge or cherry-pick), a conflict resolution when the code has drifted, a full test run on that line's dependency set, a release build, an annotated tag, and a publication. Three lines is not 50% more work than two; it is more, because the third line is usually the oldest, therefore the most drifted, therefore the one where the patch does not apply cleanly. The second term is time. Drift accumulates: every refactor, rename, formatting change or dependency upgrade that lands on one line and not another widens the textual distance a future patch must cross. A branch that was trivially mergeable at month three requires hand-porting at month eighteen. So the cost of a line is not a constant per fix — it rises for as long as the line stays alive. ## Choosing the number Start from the promise you are willing to keep, not from what feels safe. In practice that is usually the current release line plus one predecessor, with a longer-lived line only where an explicit commitment exists — a contractual support window, a customer who cannot upgrade, a platform that pins you. Each line should have a **published end-of-life date** decided when the line is cut, not negotiated when it becomes inconvenient. Without that date, lines never die, because the cost of keeping one alive is diffuse while the cost of ending it is a conversation. Retiring a line in Git is cheap and reversible: stop merging into it, delete the branch, and keep the annotated tags. The tags keep every shipped commit reachable, so the history is preserved permanently and the branch can be recreated from a tag (`git switch -c release/1.4 v1.4.7`) if something forces you back. That asymmetry — deletion is cheap, resurrection is one command — is the argument for retiring aggressively. ## The mechanisms that bound the cost **Order the lines and merge forward.** If the live lines form a chain, a fix goes onto the oldest affected line and is merged forward through the others. One commit, N-1 merges, and reachability then proves coverage: `git branch --contains <sha>` lists every line and `git tag --contains <sha>` every release that carries the fix. Fanning independent cherry-picks out to each line produces unrelated commits, unprovable coverage, and conflicts whenever those lines are later merged. **Keep maintenance lines boring.** The single largest lever. A maintenance branch takes fixes and nothing else: no refactors, no reformatting, no opportunistic dependency bumps, no "while I was in there". Every such change is a permanent tax on every subsequent backport to that line. **Tag everything, annotated.** An annotated tag per shipped build turns incident triage into a query. `git tag --contains <sha>` answers "which released versions have this fix", `git tag --sort=-v:refname -l 'v1.4.*'` lists a line's releases in version order, and `git describe` on any build stamps it back to a commit. Without tags, the same questions become archaeology. **Audit, do not trust.** Before cutting a release, run `git cherry -v main release/1.4` and review every `+`: those are commits on the maintenance line with no equivalent upstream. Some are legitimately line-local (a version bump); the rest are fixes somebody forgot to propagate. Making that review a gate is how the classic "the hotfix never reached main" regression stops happening. **Reduce repeated conflict cost.** `rerere.enabled = true` records conflict resolutions and replays them when the same hunk conflicts again, which is common when the same forward merge runs every week. ## The tradeoff to articulate More supported lines buy customers time to upgrade and buy you goodwill; they cost engineering throughput and, more insidiously, they cost *attention* — a fix applied to four lines is four chances to make a mistake under incident pressure, on the lines you exercise least. Fewer lines force upgrade pressure onto customers, which is a product decision with real consequences and must be owned as one, not smuggled in as an engineering preference. The honest framing for an interview: name the support promise first, derive the branch count from it, then show the Git-level mechanisms — merge-up ordering, boring maintenance lines, annotated tags, `git cherry` audits — that keep the derived cost from compounding. A candidate who answers with a number and no promise behind it has missed the question; so has one who lists Git commands without acknowledging that the real variable is how long lines are allowed to live.
- What happens to shipped history when you delete a retired maintenance branch?Nothing, provided every release on that line carries a tag. Tags keep those commits reachable, so the history and the exact shipped trees survive branch deletion, and the line can be recreated with `git switch -c release/1.4 v1.4.7` if you ever need it. Deleting an untagged branch is what actually loses work.
- How would you answer "which released versions are affected by this bug?" quickly?Find the commit that introduced or fixed it, then `git tag --contains <sha>` to list every tag whose history includes it. That is exact reachability, so it only works when fixes propagated by merge; where they were cherry-picked, fall back to patch-id comparison with `git cherry -v` or the `(cherry picked from commit ...)` trailers.
- Why does forbidding refactors on maintenance lines matter more than any tooling you add?Because backport cost is driven by textual distance between lines, and refactors are what create it. Tooling can replay a resolution or find a missing fix, but nothing makes a patch apply to code that no longer looks like it. Keeping the lines close is the only intervention that reduces the work rather than automating it.
- How do you decide when a line's end-of-life should be announced?When the line is cut, not when it becomes inconvenient. A date set upfront is a planning input for everyone downstream; a date negotiated later always slips, because the cost of keeping a line alive is diffuse while ending it requires one uncomfortable conversation. Announce it with the release, and hold it.
saying these in an interview costs you the question
- Keeps every release line alive indefinitely with no end-of-life
- Treats branch count as a purely technical choice
- Allows refactors and dependency bumps on maintenance lines
- Fears deleting a retired branch will lose released history
- Relies on memory rather than an audit to confirm propagation