Skip to content

Say where each branch stands, and whose rebase fixes a failure - #11

Draft
chris-peterson wants to merge 1 commit into
mainfrom
merge-readiness
Draft

Say where each branch stands, and whose rebase fixes a failure#11
chris-peterson wants to merge 1 commit into
mainfrom
merge-readiness

Conversation

@chris-peterson

@chris-peterson chris-peterson commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Context

git-fi maintains a shared fi branch that merges everyone's in-flight feature branches together, so conflicts between them surface while both branches are still small instead of at release time. When that combined merge fails, though, it prints the whole set it was merging — git can't attribute a conflict inside one octopus merge to a single branch — and the usual response on a team is git fi -f <my-branch>: replace fi with just your branch and move on, discarding everyone else's integration. The conflict is still there the next time someone adds their branch back. This makes fi say which branch actually failed, who owns fixing it, and how — usually one or two rebases rather than a reset.

Review guide

Start here — the attribution itself

  • src/readiness.ts attributeConflicts — replays the branch list one at a time onto origin/main, then probes each failure against origin/main alone to split "needs a rebase" from "collides with a peer", and sweeps pairwise to name which peer. A failing branch is left out of the accumulation, so one bad branch doesn't condemn everything after it.
  • src/readiness.ts mergeTree — the whole thing runs on git merge-tree --write-tree, which merges in the object database and exits 1 on conflict. No ref, no index, no working tree, so it runs after the failed merge is cleaned up and disturbs nothing.
  • src/merge.ts — where it hooks into the MERGE-11 failure path.

The report a user actually sees

  • src/readiness.ts renderConflicts — each branch carries its tip author's email, so the line says who owns the fix; the report closes with a git fi -r line marked temporary. --force is offered nowhere.
Failed trying to merge branch(es):

 * feature-a (bob@example.com)  conflicts with main
     * src/config.ts
     git checkout feature-a && git rebase origin/main && git push --force-with-lease
 * feature-c (cara@example.com)  conflicts with feature-b (bob@example.com)
     * src/router.ts
     rebase feature-c onto feature-b (or the reverse) and settle the overlap there

Or temporarily remove them from fi — the conflict comes back when they do:
  git fi -r feature-a feature-c

One listing answers three questions

  • src/git.ts the --format string%(ahead-behind:) and %(authoremail:trim) ride on the git branch -r listing that already runs, so the behind count, merged-ness, and the author cost no extra invocation.
  • src/git.ts mergedRemoteBranches — a branch with nothing ahead of main is exactly what git branch -r --merged reports, so the ahead half replaces that second invocation. Worth a look: this changes the mechanism behind MERGE-07's existing prune.
  • src/style.ts↓12 for a branch trailing main, a struck-through name plus merged for one that's landed. The word rides with the strikethrough because not every terminal draws SGR 9, and merged supersedes the behind count rather than stacking with it.

Skim

  • src/readiness.ts branchJson--json now nests everything under the branch; the top-level ci array is gone. Parallel arrays keyed by name made a consumer join to answer "what's the state of this branch", and could disagree about which branches exist. Nothing consumes this output yet.
  • SPEC.md Merge ReadinessREADY-01..READY-07, plus FUT-01 capturing --check (answer "will my add land?" before attempting it) as deferred.
  • test/readiness.test.ts — the discriminating cases: a branch clean against main but colliding with a peer is blamed on the peer, and a clean branch merged earlier in the same run isn't blamed at all.

Approach & trade-offs

The git floor moves from 2.13.0 to 2.41.0 (PRE-02, src/git.ts). That's the price of a commit count rather than a bare behind/not-behind flag: %(ahead-behind:) landed in 2.41.0, released 2023-06-01. git merge-tree --write-tree needs only 2.38.0, so attribution isn't what sets the floor.

Who that excludes: Ubuntu 22.04 ships git 2.34.1. macOS is fine either way — Apple's CLT git and Homebrew's are both well past it. The cheaper alternative is git branch -r --contains, which keeps the 2.13.0 floor and gives a boolean instead of ↓12.

git-fi maintains a shared `fi` branch that merges everyone's in-flight
feature branches together, so conflicts between them surface while both
branches are still small. When that combined merge fails it prints the
whole set it was merging, because git cannot attribute a conflict inside
an octopus merge to a single branch, and the usual response on a team is
`git fi -f <my-branch>`: replace fi with just your branch and move on,
discarding everyone else's integration. The conflict is still there the
next time someone adds their branch back. This makes fi say which branch
actually failed, who owns fixing it, and how, which is usually one or
two rebases rather than a reset.

Attribution replays the branch list one at a time onto `origin/main`
with `git merge-tree --write-tree`, which merges in the object database
and needs no ref, index, or working tree, so it runs after the failed
merge is cleaned up and disturbs nothing. A failing branch is probed
against `origin/main` alone to separate "needs a rebase" from "collides
with a peer", then swept pairwise to name which peer, and is left out of
the accumulation so one bad branch does not condemn everything after it.

Two things the replay cannot say, it says explicitly rather than
implying. `git merge-tree` exits 1 for an unresolvable ref and a shallow
clone's unrelated histories as well as for a conflict, so a probe that
wrote no tree OID is treated as unattributable and abandons the pass —
filing that branch as conflicting with main would propagate, since it is
then left out of the set every later branch is measured against. And the
combined merge uses the octopus strategy, which has no rename detection
where merge-tree's engine has it, so a rename against a concurrent edit
fails the merge and comes back clean from every probe; the report names
the combination instead of naming nobody.

The remedies are lines a person pastes into a shell, so branch names in
them are single-quoted: a ref name may hold backticks, `;`, `&&` or a
leading `-`, and inside double quotes a backtick still expands. Author
emails get the same treatment from the other direction — git accepts
ANSI escapes in one and `fsck --strict` passes them, so control
characters are stripped as listing fields are read, before anything can
repaint text already written to the terminal. The `git fi -r` escape
hatch is offered only for branches fi actually holds; one that failed on
the way in was never added.

One `git branch -r` listing now carries the behind count, merged-ness
and the tip author alongside the name and date, so those cost no extra
invocation and `git branch -r --merged` goes away. Counts git cannot
produce are null rather than zero, which is a real position: no
`origin/<default>` to compare against, a branch since deleted, or — for
the behind count alone — a shallow clone, whose walk stops at the graft
and would otherwise report the size of the fetched window. The ahead
count survives truncation because the only thing derived from it is
merged-ness, and a truncated window makes a branch look more ahead, so
pruning can miss a landed branch but never drops a live one.

The git floor moves from 2.13.0 to 2.41.0 for `%(ahead-behind:)`. The
cost is real but smaller than it looks: `merge-tree` alone requires
2.38.0, which already excludes Ubuntu 22.04's 2.34.1, so dropping the
count would move the floor by three releases rather than back to 2.13.0.

`--json` nests everything under the branch instead of spreading it
across arrays keyed by name, which made a consumer join to answer "what
is the state of this branch" and let the arrays disagree about which
branches exist. On a failed merge `branches` is fi as it still stands,
since nothing was pushed, and the set that was tried is named
`attempted`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant