Skip to content

fix(scripts): branch ownership comes from the PR author, never the tip commit (backend#2365) - #323

Merged
LukasWodka merged 5 commits into
developfrom
fix/2365-branch-owner
Aug 24, 2026
Merged

fix(scripts): branch ownership comes from the PR author, never the tip commit (backend#2365)#323
LukasWodka merged 5 commits into
developfrom
fix/2365-branch-owner

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes tracebloc/backend#2365.

The five-minute check first, because half the ticket is retracted by it

backend#2365's first task was to establish how backend#2001's per-person branch
table
was derived, on the suspicion that it used the tip-commit author and was
therefore aiming each engineer's --delete at other people's branches.

It did not. That table is attributed by PR author, and needs no rebuilding.

The two generators are recoverable from the 2026-08-14 session that produced them
(inventory.py, then perperson.py). The decisive line is inventory.py's,
comment and all:

# attribution: merged/closed PR author wins, else the tip's git identitypr=openprormergedprorclosedprlogin= ((pr.get("author") or {}).get("login", "") ifprelse"")

and perperson.py groups on r["login"] or r["email"] or r["aname"] — the PR
author when there is a PR, a git identity only when there is none.

That is also visible in the published artifacts without reading the code, which is
how it was found: in stale-remote-branches.tsv, all 1,165 rows with a PR number
carry a GitHub login and all 28 rows without one carry an email address.
A
perfect split on "does a PR exist", not on "is this identity resolvable" — and
design-system Fl-app-page-header, whose committer email is the perfectly
resolvable …+waqaskhanroghani@users.noreply.github.com, still shows as the raw
email because it has no PR. So the fallback keys on PR existence, exactly as the
code says. stale-remote-branches.md even labels its own summary "By author
(branch tip / merged-PR author)".

So the destructive-policy half of the ticket is demoted, with evidence: no row of
#2001's table belongs to the wrong person, and nobody was sent to reap somebody
else's branches by it.
The "never had a PR" column in that table is exactly the
set where the weaker signal was used, and it is 23 branches.

What is real, and what this PR fixes

The reporter's own incident stands, and reproduces today:

branch (client)PRPR authortip-commit author
fix/bugbot-tier0-helm-prereqs#395shujaatTraceblocLukasWodka / lukas@tracebloc.io
fix/583-wire-ca-proxy#592shujaatTraceblocLukasWodka / lukas@tracebloc.io

Both are Shujaat's; a review fixup pushed onto each rewrote the tip author. The
list that nearly deleted them was a fresh ad-hoc query, not #2001's pipeline — and
that is the actual defect: the rule was got right once, in a throwaway script,
and nothing carries it forward.
The next for-each-ref one-liner re-derives it,
and the obvious re-derivation is the wrong one.

So this adds the one place that answers it.

scripts/branch_owner.py

./scripts/branch_owner.py every origin/* branch, TSV
./scripts/branch_owner.py fix/583-wire-ca-proxy
from branch_owner import attribute, UNATTRIBUTABLE

Signals, in priority order, each named in the output so a caller can see which one
decided:

  1. pr-exact — the PR author, where the PR's head oid is still the branch tip.
  2. pr — the PR author, where later pushes have overtaken the PR head.
  3. first-commit — no PR: the author of the oldest commit not on the default
    branch. (On fix/583-wire-ca-proxy that is Shujaat while the tip is Lukas, so
    even the fallback beats the tip.)
  4. unattributable — said out loud, with the reason.

The tip author is not merely deprioritised, it is unreachable:attribute()
takes no tip-author parameter, so no code path in the module can return one. A
selftest case asserts the signature, so re-adding the parameter reddens before any
behaviour test has to.

Fails closed, in four places. "No PR exists" is only a fact when the PR list
was read, completely — so an absent, unauthenticated or failing gh, an
unparseable answer, or a list that hit its own --limit cap makes every branch
unattributable rather than falling through to a commit author. That fall-through
is the same misattribution, arrived at from a clean read of the wrong thing.
Ambiguity is a refusal too: two PRs on one reused head name by two different
people, or a PR whose author is a deleted account, name the candidates instead of
picking one.

origin/HEAD is read but never trusted, for the reason git-reap's header
already documents (stale on 9 of this org's 19 clones). It cannot affect signal 1 —
the PR query is not filtered by base — but it corrupts signal 3 exactly, because
main..branch then includes commits that are on develop, whose oldest belongs to
whoever landed them. So the remote is asked, and when it cannot be, signal 3 is
withheld rather than used.

Failure modes are stated in the module, not implied

Co-authored work (a PR has one author; Co-Authored-By: is invisible here),
cherry-picks and rebases (git preserves the original author, so an imported first
commit attributes to its origin), handovers (the PR author proposed it; the board
assignee owns it now, RFC-BACKEND-0008 D31), and bot/agent identities (returned
verbatim — mapping identities to people is a roster, and a hand-written roster is
what this module refuses to be).

CreateEvent is not signal 2, though the ticket proposed it

Measured on tracebloc/backend, 2026-08-24: /repos/{repo}/events caps at 300
events
(page 4 → HTTP 422), that whole window spanned 07:30Z–11:24Z, and it
held 2 branch CreateEvents. An active repo pushes branch creations out of the
window in hours; these branches are months old. A signal that answers for almost
nothing looks like coverage, so the module records why it is not used.

Eight defects found before review finished, all fixed and pinned

Three from the first live run, four from Bugbot, one from an audit Saqlain's
review prompted (below). Every one is the same
shape: a message asserting a check that never ran.

  1. origin/HEAD's git short name is bare origin, so stripping the remote
    prefix leaves the remote itself and it appeared in the output as a branch called
    origin. Not hypothetical: that is the same bug that put one bogus row per repo
    into the 2026-08-14 inventory (1212 rows vs 1193 real branches), and a sweep
    built on it would try to delete a branch named after the remote.
  2. A missing binary crashed instead of refusing.subprocess.run raises when
    gh is not on PATH — a traceback out of the seam, on the machine most likely to
    lack gh, is not the documented refusal. It is an exit code now.
  3. "The tip has moved past the PR head" was printed when there was no tip at
    all
    (a branch named on the command line that this clone never fetched). A
    claim about the branch, made where there was nothing to compare. The two now
    read differently, and a note on stderr names any branch that is not in the
    clone's refs.

Verification

  • scripts/tests/branch-owner-selftest.py95 assertions, 0 failed. No
    network, no git, no gh: the rule is pure and both seams are stubbed. Case 1 is
    the ticket's own measured pair, by name.

  • scripts/tests/branch-owner-mutations.py36 mutations, 0 stale, 0
    uncaught.
    Every anchor is asserted to match exactly once, --dry resolves them
    in make lint, and the module is restored on every path including a crash.
    Three mutations were uncaught on the first run and the tests were strengthened
    rather than the mutations deleted:

    • both no-author refusals said "deleted account", so deleting the exact-tip arm
      let control fall to the name-reuse arm and refuse for the wrong reason. The
      arms are now asserted apart — a refusal test that cannot say which refusal is
      a coin toss.
    • every owner assertion compared against the imported UNATTRIBUTABLE, which
      agrees with itself whatever the module sets it to, so changing it to
      "unknown" passed the whole suite. It is now written down independently.
    • the default-branch cases only checked what the stub returned, never which
      question was asked, so the remote query could be replaced by any command at
      all. The command is asserted now.
  • Saqlain's review + Bugbot round 3 (the fourth and fifth defects). Saqlain
    verified Cursor's Medium on remote_branches — the fix had already landed in
    7a87b83, so his line numbers were one commit stale, but his ask was right and
    it generalised. Auditing all four reads (derived from the call sites, not from
    memory) found first_commit_author with the same defect and worse: if rc != 0 or not out made a failed git log and a genuinely empty range the same
    return value, so attribute reported "no commit not already on the default
    branch" for both. It returns (author, problem) now. Bugbot found the same seam
    independently, plus a root cause behind it: default_branch trusted gh for
    the name and never checked this clone has the ref, so an unfetched default
    produced one failed range per branch — blaming N ranges for one missing ref. It
    is rev-parse --verify'd and reported once now.

  • main() is tested as a result, which is the part worth flagging. Every case
    before this exercised attribute or a seam in isolation, so dropping the single
    argument that carries a seam's problem into the rule reverted the whole fix while
    the suite stayed green — the mutation run reported it UNCAUGHT. The entry point
    is now driven directly for all four outcomes.

  • Both empties are pinned apart in both directions, for both reads that have
    them
    : collapse the failure into the success and report the genuine empty as
    a failure — each reddens. Asserting only "something empty came back" would have
    passed in a world where both refuse identically.

  • Bugbot round 2 found the sharpest one, fixed in the third commit:
    remote_branches collapsed a failed git for-each-ref into [] with no
    problem string, and main printed that as 0 branch(es), exit 0 — a clean bill
    of health from a read that never happened, while the module's other two seams
    refuse explicitly. It returns (refs, problem) now and main refuses with exit
    2; a genuinely empty remote still reports no problem, pinned by its own case so
    the two cannot collapse. (Bugbot's second comment on that head was stale — it
    restated the absent-tip finding already fixed, and its own footer names the
    earlier commit. Verified by construction against the current tree.)

  • Bugbot round 1 found 2 more of the same class, both real, both fixed in
    9102520
    — the multi-author reuse refusal claimed "none is at the current tip"
    with no tip in hand, and a withheld first-commit signal was reported as "no
    commit not already on the default branch", a fact about the branch from a check
    nobody ran. attribute takes first_commit_problem now, so a withheld signal
    says it was not measured; the measured-and-empty wording is pinned apart from it.
    Three further mutations cover them.

  • Wired into make selftests and make mutations, so both tiers run inside the
    already-required selftests context — no branch-protection edit, and nothing
    lands as a red gate. The wiring is itself mutation-proved: unwiring
    selftest-branch-owner makes selftests-cover fail with
    branch-owner-selftest.py is not run by any target in this Makefile.

  • make lint green: ruff 0.15.20 at CI's exact --isolated --select E4,E7,E9,F,
    house-rules --all (no findings), action-pins, mint-scope, actionlint,
    mutations-dry. make selftests green (all 17 suites).

  • End-to-end on the real repo, which is where the three defects above came from:

    $ branch_owner.py fix/583-wire-ca-proxy fix/bugbot-tier0-helm-prereqs # in client/
    fix/583-wire-ca-proxy shujaatTracebloc pr-exact PR #592 author, head oid matches the tip
    fix/bugbot-tier0-helm-prereqs shujaatTracebloc pr-exact PR #395 author, head oid matches the tip
    

    and across all of client: 32 pr-exact, 1 pr, 1 first-commit, 0
    unattributable.

org-standards.md

One line, next to the existing "your branches are yours to clean up" rule, saying
what "yours" means and pointing at the tool. It reaches every repo's CLAUDE.md
through the existing sync, so humans and AI sessions read the same rule — which
matters here, because the wrong rule is the one that is easier to type.

Not done, deliberately

  • No branches were deleted, and nothing here deletes.branch_owner.py only
    reads.
  • git-reap is untouched. It reaps the caller's own local branches, so
    ownership never arises there; a helper it does not call would be dead code in a
    shipping tool. Its header is where the fail-closed posture used here came from.
  • #2001's artifacts are not rebuilt. They are correct; see the top.
  • A house-rules grep for %(authorname) was considered and skipped: zero current
    hits, and %(authorname) is legitimate for "who touched this last". The rule
    lives in the module and in org-standards.md instead.

🤖 Generated with Claude Code


Note

Medium Risk
New ownership helper that other cleanup/sweep scripts are expected to call; a wrong attribution could target another engineer’s unlanded branch. Mitigated by fail-closed refusals, no delete path, and a mutation-proved selftest.

Overview
Defines one ownership rule so a “my branches” list cannot aim --delete at someone else’s work after a review fixup rewrote the tip author (backend#2365).

branch_owner.py attributes in order: PR author at the current tip (pr-exact), PR author after later pushes (pr), oldest commit not on the default branch (first-commit), or unattributable with a reason. The tip author is unreachable by construction (attribute() has no such parameter). Unreadable or truncated gh output, reused head names with different authors, deleted PR authors, and untrusted default-branch caches refuse rather than guess.

Wired into make selftests / make mutations with a stubbed selftest and mutation harness. org-standards.md now states that “yours” means the PR you opened and points callers at this script. Read-only; git-reap is unchanged.

Reviewed by Cursor Bugbot for commit ba2dada. Bugbot is set up for automated code reviews on this repo. Configure here.

…p commit (backend#2365)
A "my branches" list built from `%(authorname)` on the ref answers "who touched
it last", not "whose work is this". Pushing a review fixup onto someone else's
branch rewrites the tip author and changes nothing about who owns it -- and the
next command in that workflow is `push origin --delete`. Measured: `client`
PRs #395 and #592 are @shujaatTracebloc's and both showed Lukas as tip author.
backend#2001's per-person table is NOT affected -- checked first, and it
attributed by PR author already (`inventory.py`: "merged/closed PR author wins,
else the tip's git identity"). The defect is that the rule lived in a throwaway
script, so every later query re-derives it and the obvious derivation is wrong.
`branch_owner.py` is the one place that answers it: PR author first (exact head
oid, then head name), the oldest commit not on the default branch for a branch
with no PR, and `unattributable` otherwise. `attribute()` takes no tip-author
parameter, so no path can return one. Fails closed -- an unread or truncated PR
list, an ambiguous head name and a deleted PR author all refuse rather than
guess -- and `origin/HEAD` is verified against the remote before the
first-commit signal is used, because a stale default corrupts that signal
exactly.
`CreateEvent` is not used for the no-PR case despite the ticket proposing it:
/repos/{repo}/events caps at 300 events, which on backend spans under four hours
and holds two branch creations.
71 assertions and 26 mutations, wired into the already-required `selftests`
context; three mutations were uncaught on the first run and the tests were
strengthened rather than the mutations dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 24, 2026
Comment threadscripts/branch_owner.py
Comment threadscripts/branch_owner.py Outdated
… not a mismatch (backend#2365)
Both of Bugbot's findings on #323, and both are the same shape as the defect
already fixed in the first commit: a message asserting a comparison that never
ran. Reproduced by construction before changing anything.
1. The multi-author reuse refusal ended "and none is at the current tip" even
with no tip in hand -- reachable for a branch named on the command line that
this clone has not fetched, whose head name two people have used. The single
-author `pr` arm already split those two sentences; this arm now does too.
2. When the default branch could not be confirmed, `main` withheld the
first-commit signal by passing "" and `attribute` then reported "no commit on
this branch that is not already on the default branch" -- a fact about the
branch, from a check nobody ran. Appending the reason did not retract it.
`attribute` takes `first_commit_problem` now, so a withheld signal is reported
as unmeasured with its reason, and the measured-and-empty wording is pinned
apart from it.
77 assertions, 29 mutations, 0 stale, 0 uncaught.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Both Bugbot findings were real and are fixed in 9102520. Reproduced by construction first — they are the same shape as the defect the first commit already fixed (a message asserting a comparison that never ran), so the class is now pinned in three places rather than two.

1. Absent tip claimed as unmatched — correct. Reproduced: attribute("fix/typo", "", [two PRs by two authors]) returned "…and none is at the current tip" with no tip in hand. The single-author pr arm already split "moved past the PR head" from "no tip was supplied"; the multi-author arm now does the same.

2. Withheld commit check misreported — correct, and the more useful of the two. With the default branch unconfirmed, main withheld the first-commit signal by passing "", and attribute then reported "no commit on this branch that is not already on the default branch" — a fact about the branch, from a check nobody ran. You are right that appending ; also, … did not retract it. attribute now takes first_commit_problem, so a withheld signal is reported as unmeasured with its reason, and main passes the reason instead of patching the string afterwards. The measured-and-empty wording is pinned apart from it by its own case, so the two cannot collapse back into one.

Three new mutations cover both (the reuse tail unconditional, the withheld arm disabled, and the two wordings collapsed into one) — all three redden. Suite now 77 assertions, 29 mutations, 0 stale, 0 uncaught; make selftests, ruff and house-rules green locally.

Comment threadscripts/branch_owner.py Outdated

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the core attribute() logic and it's excellent — fail-closed at every ambiguity, never returns the tip author. Requesting changes on @cursor's Medium, which I verified and which is a real inconsistency with this module's own contract:

remote_branches fails open (scripts/branch_owner.py:357). A failed git for-each-ref does return [] with no problem signal, so main renders it as a successful empty inventory — header, 0 branch(es), exit 0. Every other seam (pull_requests, default_branch) refuses with an explicit problem string, and the module's own docstring is emphatic that a silent empty "looks like coverage" and that "cannot tell" must be a finding, not a pass. This one read violates that: an enumeration that couldn't run is indistinguishable from "no branches to attribute."

Make it signal the failure the way the other seams do — raise (or return a sentinel main turns into a non-zero exit with the git error) on rc != 0, rather than []. Everything else is good to go.

…one (backend#2365)
Bugbot's medium-severity finding on 9102520, and it is the sharpest one yet
because it is the fail-closed rule turned on this module's own third seam.
`remote_branches` collapsed a failed `git for-each-ref` into `[]` with no problem
string, and `main` printed that as `0 branch(es)`, exit 0 -- a clean bill of
health from a read that never happened, while the other two seams
(`pull_requests`, `default_branch`) refuse explicitly. "I could not read the
branch list" and "this clone has no branches" are different answers and only one
of them is a fact about the clone.
It returns `(refs, problem)` now and `main` refuses with exit 2, because without
the ref list there is no tip for any branch and no way to tell whether a named
one exists -- so every verdict below it would be weaker than it looked. A
genuinely empty remote still reports no problem, pinned by its own case so the
two cannot collapse back together.
Bugbot's other comment on that head is stale: it restates the absent-tip finding
already fixed in 9102520 and its own footer names commit 6f45cdc. Verified by
construction on the current tree -- the refusal reads "and no tip was supplied to
break the tie with".
80 assertions, 30 mutations, 0 stale, 0 uncaught.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Round 2: one real finding, fixed in 7a87b83; the other comment is stale.

Failed ref list looks empty (medium) — correct, and the sharpest one so far, because it is this module's own fail-closed rule turned on its third seam. Reproduced: with for-each-ref returning 128, remote_branches gave [] and main printed the header, 0 branch(es), exit 0. A clean report from a read that never happened, while pull_requests and default_branch both refuse explicitly — exactly the asymmetry you named.

It returns (refs, problem) now, and main refuses with exit 2 rather than reporting a zero: without the ref list there is no tip for any branch and no way to tell whether a CLI-named one exists, so every verdict below would be weaker than it looked. Verified end-to-end — the failing-read path prints the reason and returns 2. A genuinely empty remote still reports no problem, pinned by its own case, so "could not read" and "nothing there" cannot collapse back into one answer. Two mutations cover it (the problem string emptied; the guard disabled).

Absent tip claimed as unmatched — stale. That is the finding already fixed in 9102520, and the comment's own footer names commit 6f45cdc. Verified by construction against the current tree: with two authors and no tip, the refusal reads …(aptracebloc, waqaskhanroghani) and no tip was supplied to break the tie with. No change needed.

Suite now 80 assertions, 30 mutations, 0 stale, 0 uncaught; ruff, house-rules and make selftests green locally, and the live run over client is unchanged (32 pr-exact, 1 pr, 1 first-commit).

That is 6 defects this review has caught between us, and every one was the same shape: a message asserting a check that never ran. Worth a line in .cursor/BUGBOT.md if it shows up once more.

…e seam and in main (backend#2365)
Saqlain's requested change on #323 landed in 7a87b83, before his review arrived.
This is the rest of what he asked for, and it found a fourth instance he had not
seen.
AUDITED ALL FOUR READS rather than fixing only the reported one. `pull_requests`
and `default_branch` already refuse with a problem string. `remote_branches` was
fixed in 7a87b83. `first_commit_author` had the SAME defect and worse: its
condition was `if rc != 0 or not out`, conflating a FAILED `git log` with a
branch that genuinely has no commits off the default branch, and `attribute` then
reported the second one -- "no commit on this branch that is not already on the
default branch" -- for both. A fact about the branch, from a read that never ran.
It returns `(author, problem)` now, and `main` carries that reason through.
THE TWO EMPTIES ARE PINNED APART IN BOTH DIRECTIONS, for both reads: collapse the
failure into the success and the mutation reddens; report the genuine empty AS a
failure and it reddens too. Asserting only "something empty came back" would have
passed in a world where both refuse identically.
AND `main` IS NOW TESTED, which is what made this worth doing. Dropping the one
argument that carries a seam's problem into the rule reverted the whole fix while
every isolated case stayed green -- the mutation run caught it as UNCAUGHT, so
the entry point gets driven directly: a failed history read renders as
unmeasured, a genuine empty renders as no-unique-commits, a failed enumeration
exits 2 with no rows, and a genuinely empty remote exits 0.
92 assertions, 35 mutations, 0 stale, 0 uncaught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7a87b83. Configure here.

Comment threadscripts/branch_owner.py Outdated
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

@saqlainsyed007 — you were right, and the fix for the exact thing you flagged had already landed in 7a87b83 about twenty minutes before your review arrived, so the line numbers you were reading were one commit stale. Sorry for the crossed wires. remote_branches returns (refs, problem) and main turns a rc != 0 into exit 2 carrying the git error, which is the sentinel option you described.

Your finding generalised, and caught a fourth seam you hadn't seen. Rather than fix only the reported read I audited all four (derived from the call sites, not from memory — awk '/^def /{fn=$2} /_run\(/{print fn}'):

readbeforenow
pull_requestsrefuses with a problemunchanged
default_branchrefuses with a problemunchanged
remote_branchesfailed open[](refs, problem), main exits 2 (7a87b83)
first_commit_authorfailed open, and worse(author, problem) (6665d13)

first_commit_author had the same defect with an extra twist: its condition was if rc != 0 or not out, so a failed git log and a branch that genuinely has no commits off the default branch were the same return value — and attribute then reported the second one, "no commit on this branch that is not already on the default branch", for both. Same shape as yours: a fact about the branch asserted from a read that never ran.

Both empties are now pinned apart in both directions, for both reads. You'd get away with a test that only asserted "something empty came back", so each pair is mutated twice: collapse the failure into the success (reddens) and report the genuine empty as a failure (reddens). Same trap as the two indistinguishable no-author refusal arms earlier in this PR.

And main is tested now — that's the part worth flagging. Every case up to this point exercised attribute or a seam in isolation, so dropping the single argument that carries a seam's problem into the rule reverted the whole fix while the suite stayed green. My mutation run reported it UNCAUGHT, so the entry point is driven directly now: failed history read → "was not measured" naming the command; genuine empty → "no commit on this branch"; failed enumeration → exit 2, zero rows printed; genuinely empty remote → exit 0. That gap existed before your review and your finding is what surfaced it.

92 assertions, 35 mutations, 0 stale, 0 uncaught. ruff, house-rules --all and make selftests green locally; live run over client unchanged (32 pr-exact, 1 pr, 1 first-commit).

That's 7 defects this review has caught between the three of us, every one the same shape: a message asserting a check that never ran. If it recurs once more it belongs in .cursor/BUGBOT.md as a house rule rather than being re-argued per PR — happy to file that separately if you agree.

Re-requesting your review; not merging.

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

…ot N failed ranges (backend#2365)
Bugbot's `Failed first-commit read looks empty` names the seam already fixed in
6665d13, and two sub-cases with it. Both were verified by construction: an
unfetched default branch and a CLI-named branch absent from the clone now report
"the oldest-commit signal was not measured", naming the failed command, rather
than "no commit not already on the default branch".
The root cause behind the first one was still open, though. `default_branch`
trusted `gh` for the NAME and never asked whether this clone has the ref, so a
checkout that never fetched the remote's default produced one failed
`default..branch` range per branch -- honest since 6665d13, but blaming N ranges
for one missing ref, and unreadable on a repo with hundreds of branches. It is
verified with `rev-parse --verify` now and reported once, as its own fact, with
the fetch that fixes it.
95 assertions, 36 mutations, 0 stale, 0 uncaught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Failed first-commit read looks empty — correct on all three counts, and independently found: I was mid-audit of the other seams when this landed. The seam itself is fixed in 6665d13, the root cause behind your first sub-case in ba2dada. Thread resolved.

The seam.first_commit_author had if rc != 0 or not out, so a failed git log and a genuinely empty range were the same return value and attribute reported the second for both. (author, problem) now.

Your two sub-cases, verified by construction rather than assumed — both were already covered by that fix, and I checked rather than claiming it:

unfetched default branch -> "...was not measured: `git log origin/develop..origin/feat/x` failed"
CLI-named branch not in clone -> "...was not measured: `git log origin/develop..origin/never/fetched` failed"

Neither says "no unique commits" any more.

But your first sub-case had a root cause still open, and that is the better catch.default_branch trusted gh for the name and never asked whether this clone has the ref — so a checkout that never fetched the remote's default produced one failed range per branch. Honest since 6665d13, but it blames N ranges for one missing ref, and on a repo with hundreds of branches the real fault is invisible in the noise. It is rev-parse --verify'd now and reported once, with the fetch that fixes it. Diagnosed at the right level rather than merely reported truthfully.

Also worth recording because it is the most useful thing this round produced: chasing your finding is what got main() under test. Everything before it exercised attribute or a seam in isolation, so dropping the one argument that carries a seam's problem into the rule reverted the entire fix while the suite stayed green — my mutation harness flagged it UNCAUGHT. The entry point is driven directly now for all four outcomes.

95 assertions, 36 mutations, 0 stale, 0 uncaught. Both empties pinned apart in both directions for both reads, so "could not read" and "nothing there" cannot collapse.

bugbot run

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The fail-open I flagged (and @cursor did) is fixed and verified: remote_branches now returns (list, problem) and signals a failed git for-each-ref instead of collapsing it to [], so main can no longer render an unreadable enumeration as a clean "0 branch(es) / exit 0". That brings the last seam in line with the module's own fail-closed contract, which the core attribute() logic already followed rigorously (PR-author precedence, never the tip author, refuse at every ambiguity). CI green, thread resolved, no open threads.

@LukasWodka
LukasWodka merged commit 9c993f7 into developAug 24, 2026
13 checks passed
@LukasWodka
LukasWodka deleted the fix/2365-branch-owner branch August 24, 2026 12:26
Sign up for freeto 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.

2 participants

@LukasWodka@saqlainsyed007