Skip to content

shape_materialize.py and bump-leg.py: the fetch for a branch's answer leaves out a pattern like refs/heads* that puts a branch under another name, its clauses move into _left_out, and the docstrings name the clashes left and the way back (#1100) - #1157

Merged
brettheap merged 3 commits into
mainfrom
fix/1100-refspec-followups
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Goal

#1100 holds five follow-ups from RV1087's review of #1087 (issuecomment-6314349196). This PR does all five:

  1. Take the clause-4 fix for a pattern such as refs/heads*, and name the clash that is left, +refs/pull/7/head:refs/remotes/origin/main, in the three places that list clashes.
  2. Add the WHICH_CLAUSE row that kills R16.
  3. In bump-leg.py, name the unforced exception and the way back, git fetch --force origin.
  4. Write down the accepted cost where the cost is described.
  5. Move origin_fetch_refspecs' four clauses into a helper, _left_out.

Fix round 1 (baa19972a) answers the independent review rv1157/rv1157b (issuecomment-6347113460: NOT READY, 2 SHOULD, 4 NIT, no BLOCKER). It does all six. See "Fix round 1" below.

The NIT round (368b87e9b) answers rc1157's re-check of fix round 1 (issuecomment-6352215076: READY, 3 NITs, all in this body). It adds one test row, which kills F2, the survivor of fix round 1, and corrects three statements in this body: the reason F2 was left, the longest line's length and the #1131 merge-tree line. The scripts are unchanged. See "NIT round, after the re-check of fix round 1" below.

What held on main (038dafba)

Measured by a probe on git 2.43 and git 2.47.1, with local bare remotes only. The two gits gave identical tables. For each case the probe ran:

  • the repository's own git fetch origin;
  • the tools' fetch, git fetch -q --prune --no-prune-tags --refmap= <origin|path> <refspecs>, with the refspecs from main's origin_fetch_refspecs, once through origin and once through a path;
  • the repository's own fetch again ("own-after").

Where own-after failed, it then ran git fetch --force origin, a plain fetch, and the tools' fetch followed by the repository's own fetch ("again"). Branches are main, x and release/x at three unrelated commits unless the case says otherwise.

case                                                    own  main o/p  own-after  force, plain, again
C25  +refs/heads*:refs/remotes/origin/h*, branch h/x    0    128/128   0/0        -
C18b +refs/pull/7/head:refs/remotes/origin/main alone   0    128/128   0/0        -
C02  release/* onto origin/*, unforced                  0    0/0       1/1        0 (ref back), 0, 1
C02c same, x descends from release/x                    0    0/0       1/1        0 (ref back), 0, 1
C05u release/x onto origin/x, unforced                  0    0/0       1/1        0 (ref back), 0, 1
C20u tags onto origin/*, unforced, branch and tag x     0    0/0       1/1        0 (ref back), 0, 1
C32  heads/* onto origin/*-a, branches q and q-a        0    0/0       0/0        -
C33  feat-* onto origin/feat-*-old, feat-1, feat-1-old  0    0/0       0/0        -

These match the issue's numbers. C32 and C33 exit 0 because #1087 leaves them out. The own-after failure is ! [rejected] release/x -> origin/x (non-fast-forward). C02b, where release/x descends from x, gives 0. So the condition is that the kept commit does not descend from the branch's, not that the two commits are unrelated. The old paragraph's words "not in line" were wrong for C02c, and the new text corrects them.

Main has since moved to 961640c6 (#1155, which touched only docs/release-notes/v1.2.0.md). The four files this PR changes are byte-identical there, so nothing above changes.

What changed

scripts/shape_materialize.py

  • Clause 4 (gives_a_branch_another_name) now reads a source that is not under refs/heads/ as naming branches when it is a pattern whose part before the * is a start of refs/heads/. This is how can_name_a_tag reads refs/* for tags:

    fixed, star, _ = source.partition("*")
    if not source.startswith(HEADS_PREFIX):
        return bool(star) and HEADS_PREFIX.startswith(fixed)
    

    Such a pattern's * also takes what the pattern leaves of refs/heads/. So no destination under refs/remotes/origin/ gives a branch its own name, and every such refspec is left out. The only pattern that keeps the names, refs/heads*:refs/remotes/origin*, writes outside origin/, and clause 1 leaves it out first. The shorter starts (refs/, *) can name a tag, and clause 2 leaves them out first.

  • The fix was taken only after measuring it. A pure-function differential of main against the branch ran over 39,385 configs: 43 sources x 31 destinations, forced and unforced, negatives, pairs, doubles, and each one beside the first refspec.

    • The branch carries nothing that main does not.
    • It newly leaves out 207 distinct refspecs. Every one has a pattern source whose part before the * starts refs/heads/ but neither refs/heads/ nor refs/tags/ (refs/h* to refs/heads*).
    • The helper alone (main plus _left_out) gives main's output on all 39,385 configs.
    • The fix alone and the fix plus the helper give the same output as each other on all 39,385.
  • The probe of all 53 cases, on both gits, changed no outcome outside that class: exits, own-after, force, plain and again are identical for the 46 cases outside the class, C03u, C17u and C32o included. The seven cases in the class changed:

    case                                                      own  main o/p  branch o/p  own-after (branch)
    C25   refs/heads* onto origin/h*, branch h/x              0    128/128   0/0         0/0
    C25u  same, unforced                                      0    128/128   0/0         1/1; --force 0, plain 0
    C25n  refs/heads* onto origin/h*, no clash                0    0/0       0/0         0/0 (pruned, written back)
    C25g  default glob + refs/heads* onto origin/h*           0    0/0       0/0         0/0 (pruned, written back)
    C25b  default glob + refs/he* onto origin/he*, heads/x    128  128/128   0/0         128/128
    C25c  refs/he* onto origin/he*, branch heads/x            0    128/128   0/0         0/0
    C25h  glob + refs/h* onto origin/hh/*, ref refs/hidden/1  0    0/0       0/0         0/0 (pruned, written back)
    

    Wherever a tools' fetch exited 0, every remote branch's refs/remotes/origin/<name> held that branch's commit (all cases, both rules, both ways, both gits). A fetch that exited 128 changed no ref.

  • _left_out(source, destination) holds the four clauses, in their order and with their comments. origin_fetch_refspecs now asks it once. The SonarQube analyzer (python:S3776, analyze_code_snippet) raises nothing on an excerpt of the four functions as committed, with their docstrings dropped. With four more if ...: continue in the loop it reports "from 17 to the 15 allowed", so origin_fetch_refspecs measures 9 (it was 15). By hand, _left_out is 3 and gives_a_branch_another_name is 2.

  • The docstrings:

    • gives_a_branch_another_name no longer ends "A source not under refs/heads/ names no branch". It names the pattern and points to WHAT CAN STILL CLASH.
    • The A BRANCH UNDER ANOTHER NAME paragraph names the pattern.
    • A new THE COST, ACCEPTED paragraph says that, after either tool runs, an unforced refspec's own fetch exits 1 on every run until git fetch --force origin. Where the refspec was carried, the tools' fetch exited 128 in those same states and left the ref alone. No commit is lost, and main already behaves this way for unforced tags onto the glob.
    • WHAT CAN STILL CLASH now names any carried refspec whose source names no branch, onto a name a branch has: the pull requests in origin/pr/ and C18b's single name.
    • Fix round 1 changes three of these paragraphs and adds one (below).

scripts/bump-leg.py, docstrings only. An AST comparison with the docstrings blanked shows the code is identical to main's, at both heads.

tests/test_family_bump_fetch_refspecs.py

  • New WHICH_CLAUSE rows (both tools' tests ask them):
    • another-name-suffix, +refs/heads/*:refs/remotes/origin/*-old, left out (item 2);
    • another-name-heads-pattern, +refs/heads*:refs/remotes/origin/h*, left out;
    • another-name-heads-pattern-shorter, refs/he*:refs/remotes/origin/he*, left out;
    • carried-not-refs-heads, +refs/headsx/*:refs/remotes/origin/headsx/*, carried;
    • fix round 1: another-name-heads-pattern-refs-h and another-name-heads-pattern-suffix (below).
  • New BRANCH_UNDER_ANOTHER_NAME row heads-pattern-into-a-folder, +refs/heads*:refs/remotes/origin/h*, with x-w1066 and h/x-w1066. It runs end to end through family.py bump and through bump-leg.py via origin and via --local-remote-dir, with the clash premise first.
  • Fix round 1: NO_BRANCH_A_PATTERN_TAKES and its end-to-end rows (below).
  • NIT round: carried-refs-h-not-a-pattern, +refs/h:refs/remotes/origin/hy, carried (below).

tests/test_bump_leg_fetch_refspecs.py: one docstring sentence, and in fix round 1 the leg's NO_BRANCH_A_PATTERN_TAKES rows.

scripts/family.py is unchanged. Its _origin_fetch_refspecs paragraph "A mount whose refspec git cannot satisfy" gives examples and claims no complete list, and it stays true.

Fix round 1, after rv1157's review

One new commit, baa19972a, on a35a97ea5. No merge of main was needed: main's one new commit touches none of these files. The scripts' code is unchanged: astsame.py (AST with docstrings blanked) gives SAME CODE for shape_materialize.py against a35a97ea5 and for bump-leg.py against main.

SHOULD 1 (finding 1): two mutants of the fix survived the rows. Two WHICH_CLAUSE rows are added. Both tools' tests ask them (test_family_bump_fetch_refspecs.py holds the table, and test_bump_leg_fetch_refspecs.py imports it):

  • another-name-heads-pattern-refs-h, +refs/h*:refs/remotes/origin/hh/*, left out. This is the class's lower edge, the shortest start of refs/heads/ that no earlier clause meets.
  • another-name-heads-pattern-suffix, +refs/heads*x:refs/remotes/origin/h*x, left out. Git accepts it and maps branch x onto origin/h/x and ax onto origin/h/ax (probe section B, both gits).

On the pure function (tools/rows.py, logs/rows.log):

row                                     main     head      B1        B9        F2
+refs/h*:refs/remotes/origin/hh/*       carried  left out  carried   left out  left out
+refs/heads*x:refs/remotes/origin/h*x   carried  left out  left out  carried   left out

So each new row fails on main. B1 is killed by -refs-h and B9 by -suffix. F2 was the one survivor of fix round 1. The NIT round's row kills it (see "NIT round" and Mutants).

SHOULD 2 (finding 2): leaving the class out changes the answer from pin to refuse for a commit that only a non-branch ref reaches, where the pattern takes that ref too. The three places that said nothing is lost now say so:

New end-to-end rows pin the refusal, NO_BRANCH_A_PATTERN_TAKES (+refs/heads*:refs/remotes/origin/h* with refs/heads-archive/old-w1100, and +refs/h*:refs/remotes/origin/hh/* with refs/hidden/old-w1100):

  • test_a_ref_a_heads_pattern_takes_answers_for_no_branch runs family.py bump --dry-run and bump. Both refuse member-to-local-only in the plain words, with nothing in the holder moved and no ref of the mount moved.
  • test_a_ref_a_heads_pattern_takes_answers_for_no_branch_of_the_leg runs bump-leg.py via origin and via --local-remote-dir. Both runs refuse bump-leg-commit-local-only in the plain words, with nothing in the root moved and no ref of the leg moved.
  • Each first asserts its premises: the ref reaches the commit, no branch of the remote does, and no remote-tracking ref does. All 6 rows fail on main, where both runs pin (Rows failing on main).

The docstrings' claims, measured by tools/claims.py (the tools' own code from an exported tree: fetch_leg end to end, then family.py's reader and _on_a_remote_branch). It ran on main 961640c6 and on baa19972a, on git 2.43 and 2.47.1. The two gits gave identical output on each tree (logs/claims-{main,head}-{243,247}.log):

case              refspec beside the glob                                ref holding C           main    head
C25h-in           +refs/h*:refs/remotes/origin/hh/*                      refs/hidden/1           PIN     REFUSE
C25h-unforced     refs/h*:refs/remotes/origin/hh/*                       refs/hidden/1           PIN     REFUSE
heads-x-in        +refs/heads*:refs/remotes/origin/h*                    refs/heads-archive/old  PIN     REFUSE
heads-x-unforced  refs/heads*:refs/remotes/origin/h*                     refs/heads-archive/old  PIN     REFUSE
hidden-out        +refs/hidden/*:refs/remotes/origin/hidden/*            refs/hidden/1           PIN     PIN
heads-x-out       +refs/heads-archive/*:refs/remotes/origin/h-archive/*  refs/heads-archive/old  PIN     PIN
pulls-out         +refs/pull/*/head:refs/remotes/origin/pr/*             refs/pull/7/head        PIN     PIN
pulls-onto-glob   +refs/pull/*/head:refs/remotes/origin/*                refs/pull/7/head        REFUSE  REFUSE
  • family.py's answer equals bump-leg.py's in every row, and so do the two readers' lists.
  • On the head, the tools' fetch exits 0 and prunes the class ref (origin/hh/idden/1, origin/h-archive/old). The repository's next own git fetch origin exits 0 and writes it back.

NIT 1 (finding 3), bump-leg.py's tag paragraph. It now says that such a ref is pruned or holds the branch of that name, and that the leg's own next fetch writes it back. It adds NOT WHERE THE LEG WROTE THAT REFSPEC WITHOUT ITS + and the tag's commit does not descend from the branch's: then the leg's own fetch exits 1 on that run and every one after it, until git fetch --force origin. Measured (probe section C, both gits, main and head alike):

case       refspec                              own  tool  own-after  --force (ref back)  plain
C20        +refs/tags/*:refs/remotes/origin/*   0    0     0,0        -                   -     (origin/x back at the tag)
C20u       refs/tags/*:refs/remotes/origin/*    0    0     1,1        0 (back)            0     ! [rejected] x -> origin/x (non-fast-forward)
C20u-desc  same, the tag's commit a child of x  0    0     0,0        -                   -

NIT 2 (finding 4). The 109-character line is reflowed. The longest line this PR adds, over 961640c6..368b87e9b in the four files, is 78 characters (test_bump_leg_fetch_refspecs.py's assert contains(bare_of(remotes), made, ref), f"premise: {ref} reaches it"). In the scripts the longest added lines are 75 (bump-leg.py) and 76 (shape_materialize.py). bump-leg.py's longest line, 81, is main's. This body said 79 until the re-check measured 78.

NIT 3 (finding 5), the overclaim in gives_a_branch_another_name. It no longer says each such refspec puts a branch under another name. Where the part before the * is all of refs/heads and the destination's * comes right after a /, git refuses the name it gives each branch, and the fetch writes no branch. The docstring now says this, and says that the pattern still takes the refs of another namespace, pointing to the new paragraph. Probe section B, both gits: +refs/heads*:refs/remotes/origin/h/* with branches main, x and ax exits 0, says error: * Ignoring funny ref 'refs/remotes/origin/h//x' locally (and the same for h//main and h//ax), and writes only origin/h/foo/x for refs/headsfoo/x.

NIT 4 (finding 6), the stale merge-tree list. Merge-trees were re-run at baa19972a against main and every open PR touching these four files (Merge-trees).

NIT round, after the re-check of fix round 1

One new commit, 368b87e9b, on baa19972a, in tests/test_family_bump_fetch_refspecs.py only: one WHICH_CLAUSE row, a sentence in the table's comment and one in its test's docstring. The scripts are byte for byte baa19972a's (git diff --name-only baa19972a 368b87e9b names that one file). The row is pytest.param("+refs/h:refs/remotes/origin/hy", True, id="carried-refs-h-not-a-pattern"). WHICH_CLAUSE is one table, which tests/test_bump_leg_fetch_refspecs.py imports and asks of _leg_fetch_refspecs, so the one row is two test ids, one in each file, and both tools' tests ask it.

NIT 1 (finding 1): the reason given for leaving F2 was wrong, and the row it said could not be added can be. The first body said F2 differs from this head only on a source that is no pattern and is a start of refs/heads/ (refs/, refs/h, ... refs/heads), that no such full name can be a ref beside the remote's branches, and that a row for it would pin an arbitrary answer. Measured by tools/claims.py (the tools' own code from an exported tree: fetch_leg end to end, then family.py's reader and _on_a_remote_branch), on main 961640c6, on this head and on this head with F2 applied, on git 2.43 and 2.47.1 (logs/claims-{main,head,f2}-{243,247}.log; the two gits gave identical output on each tree):

case                   refspec beside the glob                     ref holding C   main    head    F2
refs-h-no-pattern      +refs/h:refs/remotes/origin/hy              refs/h          PIN     PIN     REFUSE
refs-head-no-pattern   +refs/head:refs/remotes/origin/hz           refs/head       PIN     PIN     REFUSE
refs-heads-no-pattern  +refs/heads:refs/remotes/origin/hx          refs/heads      the remote cannot hold refs/heads and refs/heads/main
hidden-out             +refs/hidden/*:refs/remotes/origin/hidden/* refs/hidden/1   PIN     PIN     PIN
C25h-in                +refs/h*:refs/remotes/origin/hh/*           refs/hidden/1   PIN     REFUSE  REFUSE
  • Only refs/heads itself cannot be such a ref: git update-ref refuses it with "cannot process 'refs/heads/main' and 'refs/heads' at the same time" (exit 128, no ref made). refs/h and refs/head are made beside refs/heads/main with exit 0.
  • With +refs/h:refs/remotes/origin/hy and a commit C that only refs/h reaches, main's code and this head's carry the refspec and PIN C. Under F2 both tools leave it out and REFUSE C (bump-leg-commit-local-only, family_answer=False): the tools' fetch prunes origin/hy, which the repository's own fetch had written, and the next own fetch writes it back.
  • So this head's answer is not arbitrary. It is the rule ANY OTHER SOURCE NAMES NO BRANCH, the same as hidden-out's: a source that is no pattern takes the ref of its name. Where the remote has no ref of that name, git completes the name instead: with a branch named refs/h (the ref refs/heads/refs/h) beside hy, the tools' fetch line exits 128 with Cannot fetch both refs/heads/hy and refs/heads/refs/h to refs/remotes/origin/hy, on git 2.43.0 and 2.47.1, on main as here. That is the WHAT CAN STILL CLASH shape (lane 5's re-check, issuecomment-6371455922, NIT 1).
  • On the pure function (tools/rows.py, logs/rows.log, origin_fetch_refspecs alone):
row                                       main      head      B1        B9        F2
+refs/h:refs/remotes/origin/hy            carried   carried   carried   carried   left out
+refs/head:refs/remotes/origin/hz         carried   carried   carried   carried   left out
+refs/h*:refs/remotes/origin/hh/*         carried   left out  carried   left out  left out
+refs/heads*x:refs/remotes/origin/h*x     carried   left out  left out  carried   left out
  • The new row passes on main by design, as another-name-suffix does, because main's clause 4 (gives_a_branch_another_name) answers False for every source not under refs/heads/ (961640c6, scripts/shape_materialize.py line 1704), and refs/h meets no earlier clause. It passes on this head. It kills F2: with F2 applied to scripts/shape_materialize.py, -k carried-refs-h-not-a-pattern over both test files gives 2 failed (test_which_clause_leaves_each_refspec_out[carried-refs-h-not-a-pattern] and test_which_clause_leaves_each_refspec_out_of_the_legs_fetch[carried-refs-h-not-a-pattern]), and over the 90 rows of Mutants, 2 failed and 88 passed. The runs are in logs/mut-summary.log: the row alone, 2 passed on main's code (logs/mut-ROW-main-code.log), 2 passed on this head (logs/mut-ROW-head.log), 2 failed under F2 (logs/mut-ROW-F2.log).
  • The Mutants section below replaces the F2 sentence with this result.

NIT 2 (finding 2): the line length. Fix round 1's NIT 2 sentence now says 78, and names the scripts' 75 and 76 (above). The new commit adds lines of at most 76 characters, all ASCII, and no assert that joins conditions with and or or.

NIT 3 (finding 3): the #1131 merge-tree line. #1131's head is 55c30f20 now (its NIT round), not a4beec9e. git merge-tree --write-tree 55c30f20 368b87e9b exits 0 with tree e036c02d6a12; at baa19972a it gave bd6ebc5cd3af, as the re-check measured. See Merge-trees.

Rows failing on main

Main's production code (961640c6: scripts/shape_materialize.py and scripts/bump-leg.py) was put in the worktree under this head's two changed test files. Then the committed files were put back, with sha256 and dirty=0 checked (logs/mut-MAIN-code-head-tests.log). The result is 17 failed, 106 passed at baa19972a, and 17 failed, 108 passed at 368b87e9b (logs/mut-MAIN-code-head-tests.log, 125 rows): the NIT round's 2 rows pass on main, by design. The failures are exactly the rows the fix decides:

  • WHICH_CLAUSE [another-name-heads-pattern], [-shorter], [-refs-h] and [-suffix], in both files: 8. Each is carried on main ("Left contains one more item").
  • test_a_branch_put_on_another_branchs_name_fails_no_fetch[heads-pattern-into-a-folder] (member), and ..._of_the_leg[origin-heads-pattern-into-a-folder] and [local-remote-dir-heads-pattern-into-a-folder] (leg): 3. The fetch exits 128 on main, so the runs give the failed-fetch note.
  • Fix round 1's NO_BRANCH_A_PATTERN_TAKES rows: test_a_ref_a_heads_pattern_takes_answers_for_no_branch[heads-pattern] and [refs-h-pattern] (member), and ..._of_the_leg[origin-heads-pattern], [origin-refs-h-pattern], [local-remote-dir-heads-pattern] and [local-remote-dir-refs-h-pattern] (leg): 6. On main the pattern is carried, so the real run pins the commit instead of refusing it.

The first round's 7 are among these. Fix round 1 adds 10: -refs-h and -suffix in both files, and the 6 end-to-end rows. another-name-suffix and carried-not-refs-heads pass on main by design. They kill mutants (below).

Mutants

NIT round, on 368b87e9b with git 2.43, through tools/mut.sh (logs/mut-summary.log). The scripts are baa19972a's, so these are fix round 1's mutants over the NIT round's tests. Each mutant was applied alone to the committed scripts/shape_materialize.py. The runs covered both changed test files with -k "which_clause or another_branchs_name or heads_pattern_takes" (90 rows, 35 deselected). After each run the committed files were put back, and their sha256 and dirty=0 were checked: 9 of 9 restores in the script matched (shape_materialize.py 16c6ec378c36..., bump-leg.py 8f4af9c5e78c...).

mutant                                                        failed  result
main's code (961640c6) under these two files, all 125 rows     17     the rows above (108 passed)
F2 the fix's `bool(star) and` dropped                           2     killed (carried-refs-h-not-a-pattern x2)
B1 the class drawn from `refs/he` up (`refs/h*` carried)        5     killed (-refs-h x2, refs-h-pattern x3)
B9 the fix only where the `*` ends the source                   2     killed (-suffix x2)
B4 the fix undone (`return False` in its place)                17     killed (every row the fix decides)

F2 is killed now. At baa19972a, with 88 rows, it survived (88 passed); B1 (5 failed), B9 (2) and B4 (17) gave the same counts as here. The earlier sentence said F2 differs from this head only on a source that is no pattern and is a start of refs/heads/, and that no such full name can be a ref beside the branches. The first half holds: over the 39,385-config corpus F2 differs from this head only there (refs/, refs/h, ... refs/heads). The second half held for refs/heads alone. refs/h and refs/head are refs a remote can hold beside its branches, and the head carries +refs/h:refs/remotes/origin/hy and pins a commit only that ref reaches, where F2 refuses it (NIT round, NIT 1). The row carried-refs-h-not-a-pattern holds that answer.

At a35a97ea5, whose code this head keeps, the first round measured these over WHICH_CLAUSE in both files (66 rows then):

mutant                                                failed  result
R16 clause 4 compares only the part before the `*`      2     killed (another-name-suffix x2); on main's own rows it survives (102 passed)
R17 clause 4 compares only the part after the `*`       8     killed (the four another-name rows x2)
F3  only `refs/heads*` itself, not shorter starts       2     killed (another-name-heads-pattern-shorter x2)
F4  any pattern starting `refs/h` (too broad)           2     killed (carried-not-refs-heads x2)
F5  every pattern not under refs/heads/ left out        6     killed (carried pulls, not-refs-tags, not-refs-heads x2)
H1  _left_out's OUTSIDE clause removed                  2     killed
H2  _left_out's tag clause removed                     14     killed
H3  _left_out's branches'-glob clause removed           2     killed (branches-glob-pull-requests x2)
H4  _left_out's last clause removed                    14     killed
H5  `if _left_out(...)` negated                        60     killed

Runs

Every run went through run9.sh on the commit 368b87e9b (dirty=0 in each START line): gh stub first on PATH, a bogus token, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, and EMAIL set.

  • git 2.43, the three files that name the functions (test_family_bump_fetch_refspecs.py, test_bump_leg_fetch_refspecs.py, test_bump_leg_reachability_words.py): 159 passed (logs/g243.log; 157 at baa19972a, and the NIT round's 2 rows).
  • git 2.47.1 (a git 2.47.1 build first on PATH; git --version printed git version 2.47.1 in the log): the same three files: 159 passed (logs/g247.log).
  • git 2.43, test_path_guard_hygiene.py, test_pinned_copy_text.py and the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py): 306 passed (logs/hyg243.log, the same count as at baa19972a).

Merge-trees

git merge-tree --write-tree <target> 368b87e9b, run at 2026-10-23T12:06:38Z against main and the head of every open PR that touches one of the four files (55 open PRs listed at 12:06:22Z; logs/merge-trees.log). Each exits 0 with no conflict:

The head's own tree is 351a9b73cda8. Main is still 961640c6: its one commit past 038dafba, #1155, touches only docs/release-notes/v1.2.0.md.

Limits

Closes #1100

Claim: #1100 (comment)
Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Exclude branch-matching patterns that can rename branches from answer fetches while documenting their trade-offs and covering the resulting fetch behavior.

Bug Fixes:

  • Prevent branch-matching refspec patterns such as refs/heads* from causing fetch clashes by excluding them from answer fetches.
  • Document and test the remaining non-fast-forward and no-branch reachability cases introduced by excluding these refspecs.

Enhancements:

  • Extract the refspec exclusion rules into a dedicated helper and clarify the documented trade-offs and remaining clash scenarios.
  • Expand branch-fetch behavior coverage for branch-renaming patterns and references from other namespaces.

Documentation:

  • Clarify fetch behavior, accepted recovery costs, remaining clashes, and the git fetch --force origin recovery path in both fetch workflows.

Tests:

  • Add clause-selection and end-to-end coverage for refs/heads*-style patterns, including refs that match the pattern without being branches.

… leaves out a pattern like `refs/heads*` that puts a branch under another name, its clauses move into `_left_out`, and the docstrings name the clashes left and the way back (#1100)

RV1087's review of #1087 left five follow-ups. Measured on main, git 2.43
and 2.47.1, local bare remotes:

- `+refs/heads*:refs/remotes/origin/h*` with branches `x` and `h/x`: the
  repository's own `git fetch origin` exits 0 and both tools' fetch 128.
  Clause 4 now reads a pattern whose part before the `*` is a start of
  `refs/heads/` as naming branches, as `can_name_a_tag` reads one for
  tags. Under `refs/remotes/origin/` such a pattern never gives a branch
  its own name, so it is left out, and the fetch exits 0. Over 39,385
  configs it changes no refspec outside that class, and over 53 probed
  cases no other outcome.
- `+refs/pull/7/head:refs/remotes/origin/main` alone still exits 0 own and
  128 in the tools. The docstrings now name it beside the folder case.
- R16 (clause 4 comparing only the part before the `*`) survived every
  row on main; the new row `another-name-suffix` kills it.
- With an unforced refspec left out, the repository's own fetch exits 1,
  non-fast-forward, until `git fetch --force origin` (exit 0, the ref
  back, a plain fetch 0 after). bump-leg.py's #1066 paragraph now says
  so, and the cost paragraph records that cost as accepted.
- The clause chain is now `_left_out`: `origin_fetch_refspecs` measures 9
  by S3776 (15 before), with the same output over every config above.

Closes #1100

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes branch refspec classification for patterns such as refs/heads*, factors the four exclusion clauses into _left_out, documents the remaining clash and recovery behavior, and adds end-to-end, clause, and mutation-oriented regression coverage.

Sequence diagram for branch-pattern clash prevention

sequenceDiagram
    participant Tool as AnswerTool
    participant Git as GitFetch
    participant Remote as Remote
    participant Refs as TrackingRefs
    Tool->>Tool: origin_fetch_refspecs(configured)
    Tool->>Tool: _left_out(source, destination)
    alt refs/heads* puts branch under another name
        Tool->>Git: fetch with branch glob only
        Git->>Remote: fetch branches
        Remote-->>Git: branch x
        Git->>Refs: write origin/x
        Git-->>Tool: exit 0
    else refspec is carried and clashes
        Tool->>Git: fetch with conflicting refspec
        Git->>Remote: fetch branch and carried ref
        Git-->>Tool: exit 128
    end
Loading

Sequence diagram for non-fast-forward recovery

sequenceDiagram
    participant User
    participant Git as GitFetch
    participant Remote as Remote
    participant Refs as TrackingRefs
    User->>Git: git fetch origin
    Git->>Remote: fetch branch updates
    Git-->>User: exit 1 non-fast-forward
    User->>Git: git fetch --force origin
    Git->>Remote: fetch branch updates
    Git->>Refs: write ref back
    Git-->>User: exit 0
    User->>Git: git fetch origin
    Git-->>User: exit 0
Loading

Flow diagram for branch refspec filtering

flowchart TD
    A["Configured refspec"] --> B{"_left_out(source, destination)"}
    B -->|"outside origin tracking refs"| X["Leave out"]
    B -->|"can_name_a_tag(source)"| X
    B -->|"destination is branch name glob"| X
    B -->|"branch or refs/heads* pattern changes name"| X
    B -->|"none match"| C["Carry refspec"]
    C --> D["Fetch for branch answer"]
Loading

File-Level Changes

Change Details Files
Extend branch-clash detection to refspec patterns beginning with a partial refs/heads/ prefix, and centralize the four refspec exclusion rules.
  • Detect patterns such as refs/heads* and exclude them when they remap branches under alternate remote-tracking names.
  • Move destination, tag, branch-glob, and alternate-name checks into _left_out.
  • Preserve ordering and behavior of existing exclusion clauses while reducing complexity in origin_fetch_refspecs.
  • Document remaining clash cases and the accepted non-fast-forward cost, including recovery with git fetch --force origin.
scripts/shape_materialize.py
Update leg-fetch documentation to describe the newly handled pattern and its operational recovery path.
  • Document refs/heads* as a former clash now left out.
  • Explain unforced non-fast-forward failures and the force-fetch way back.
  • Clarify that carried non-branch sources can still clash with branch names, including single-name destinations.
scripts/bump-leg.py
Expand regression and mutation coverage for partial-head patterns and clause classification.
  • Add end-to-end coverage for refs/heads* remapping a branch into a folder.
  • Add clause rows for full and shortened partial-head patterns, suffix destinations, and near-match sources.
  • Verify the new behavior through both family and leg fetch paths, including local remote directories.
tests/test_family_bump_fetch_refspecs.py
tests/test_bump_leg_fetch_refspecs.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1100 Treat patterns such as refs/heads* that can match branches as naming branches when deciding which fetch refspecs to leave out, document the remaining clash with a non-branch source landing on a branch name, and add coverage for both cases. ✅
#1100 Add a WHICH_CLAUSE test row for a destination pattern with a suffix after *, so clause 4 is tested against the R16 mutant that ignores that suffix. ✅
#1100 Document the non-fast-forward behavior and git fetch --force origin recovery for unforced refspecs, record the accepted cost, and move the refspec clause chain into a helper to leave Sonar complexity headroom. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1157 (#1100) for lane openRepoShape-5

Verdict: NOT READY. There is no BLOCKER. There are two SHOULDs: a test gap that the body says is not there, and a change to the answer that the docstrings say cannot happen. There are four NITs. The clause-4 fix is right against real git. The head carries no refspec that git maps as a tag or as a branch under another name (main carries 169), and it carries nothing that main does not. CI on the head is green.

I reviewed a35a97ea5, which is refs/pull/1157/head, against main 038dafba. I read both again at 2026-10-22T17:28Z and neither had moved. Everything was measured with local bare remotes on git 2.43.0 and git 2.47.1, and the two gits agreed on every probe that both ran. Two reviewers did this work. rv1157 measured items 1-7 below and findings 1 and 3-6 (15:17Z-15:37Z). rv1157b ran the answer probe (finding 2), re-read each finding against the code, and set the verdict.

What holds, measured

  1. The clause-4 fix, against real git (oracle.py, oracle2-243.log, oracle2-247.log). Each refspec was fetched alone into an empty bare repository. The remote had 32 refs, each at its own commit, so the commit a fetched ref holds shows which remote ref it came from.
    • The corpus was 1,938 refspecs: 57 sources by 34 destinations. It includes 14 sources and 3 destinations that the writer's corpus lacks (refs/heads-*, refs/heads*a, refs/hea*/x, refs/pull*, and others).
    • The head carries 0 refspecs that git maps as a tag or as a branch under another name. Main carries 169, and all of them are in the refs/h* to refs/heads* class.
    • Forced and unforced input gave the same answer for every refspec.
    • 13 refspecs that the head carries still put a non-branch ref on a branch's name. All 13 have sources that name no branch (refs/pull/7/head, refs/tagsx/*, refs/headsx/*, ...). That is the documented C18b limit.
    • Caveat: 69 fetches failed on name conflicts inside the scratch repository and were not classified.
  2. The 39,385-config differential, re-run in full on main and on the head as exported from git: 8,310 configs differ, 207 refspecs are carried only by main, and 0 only by the head. This matches the body.
  3. Rows failing on main. Main's shape_materialize.py under the head's two test files gives 7 failed, 106 passed: exactly the 7 rows the body names (mut-MAIN-code-head-tests.log).
  4. R16.
    • On main's code with main's own two test files, R16 gives 102 passed, so it survives main's rows. This was measured directly, not inferred.
    • On the head it gives 2 failed (another-name-suffix in both files), so it is killed.
    • Two more mutants are killed: the fix undone (7 failed) and clause 1 without its / (2 failed, outside-origin-prefix).
    • After every mutant, the three files were restored and checked by sha256 (70788022..., c63ef2d3..., 16762a5e...), and the worktree was clean (mut-summary.log).
  5. THE COST, ACCEPTED, and the way back, against real repositories.
    • Each case used the tools' own refspec readers: bump-leg.py's _leg_fetch_refspecs, which gave the same list as family.py's _origin_fetch_refspecs in every case.
    • The fetch was the tools' exact command line, once through origin and once through the path.
    • Head table (main differs only on the C25 rows):
    case       own tool o/p  after1 after2 --force(ref)  plain again
    C05u       0   0/0       1      1      0 (back)      0     1
    C02        0   0/0       1      1      0 (back)      0     1
    C02c       0   0/0       1      1      0 (back)      0     1
    C02b       0   0/0       0      0      -             -     -
    C25        0   0/0       0      0      -             -     -     (main 128/128)
    C25u       0   0/0       1      1      0 (back)      0     1     (main 128/128)
    C25u-desc  0   0/0       0      0      -             -     -     (x descends from h/x)
    C20u       0   0/0       1      1      0 (back)      0     1     (main the same)
    C32u       0   0/0       1      1      0 (back)      0     1     (main the same)
    C18b       0   128/128   0      0      -             -     -     (main the same)
    
    • The C25u rejection reads ! [rejected] x -> origin/h/x (non-fast-forward).
    • All of the new docstrings' claims about the cost hold: the descent condition, "every one after it", --force exiting 0 with the ref back, a plain fetch exiting 0 after it, and the cost returning once either tool runs again.
  6. Gates at the head commit (dirty=0, through run9.sh):
    • git 2.43, the three files that name the functions: 147 passed (g243.log);
    • git 2.47.1, the same three files: 147 passed (g247.log);
    • git 2.43, test_path_guard_hygiene.py, test_pinned_copy_text.py and the trio: 306 passed (hyg243.log).
    • CI on a35a97ea5: tests passed (1h25m35s), and so did tests-git-bash, package and SonarCloud Code Analysis.
  7. Merge-trees of a35a97ea5, run again at 17:29Z. The targets are main and the current head of every open PR that touches one of the four files (37 open PRs, re-listed at 17:29Z). All exit 0:
  8. Sonar. I did not run the analyzer. These S3776 counts were taken by hand:
    • origin_fetch_refspecs is 9 on the head: for +1, then four constructs at nesting 1 at +2 each (the ^ check, the conditional expression, if _left_out, the dedupe). On main it is 15.
    • _left_out is 3.
    • gives_a_branch_another_name is 2.
    • These agree with the body. SonarCloud's PR comment (13:43:06Z) says "Quality Gate passed" with 0 new issues.

Findings

  1. SHOULD (tests, body): two mutants of the fix survive, and the body says "F2 is the one survivor".

    • Reproduction: tools/mutate.py applied each mutant alone to scripts/shape_materialize.py. run9.sh then ran both changed test files with -k "which_clause or another_branchs_name" (78 rows) on git 2.43. After each run the files were restored and checked by sha256, with dirty=0.
      • B1, the class drawn from refs/he up (return bool(star) and HEADS_PREFIX.startswith(fixed) and len(fixed) > 6, so refs/h* is carried again): 78 passed. It survives.
      • B9, the fix applied only where the * ends the source (return source.endswith("*") and HEADS_PREFIX.startswith(fixed), so refs/heads*x is carried again): 78 passed. It survives.
      • F2: 78 passed. It survives, as the body says.
    • Cause: the new rows pin only refs/heads* and refs/he*, and both end in *. The class's lower edge, refs/h*, is the body's own C25h case and has no row. Nothing pins a suffix after the * either.
    • Two rows would kill both mutants. Each is carried by main, so each also fails on main. Measured on the pure function (tools/rows.py):
      +refs/h*:refs/remotes/origin/hh/*      main carried  head left out  B1 carried  B9 left out
      +refs/heads*x:refs/remotes/origin/h*x  main carried  head left out  B1 left out B9 carried
      
    • Git accepts the second refspec and maps a branch under another name with it: x -> origin/h/x and ax -> origin/h/ax, exit 0, on both gits.
    • Fix: add those two WHICH_CLAUSE rows, or name B1 and B9 as survivors in the body.
  2. SHOULD (docs, body): leaving out the class changes the answer from pin to refuse for a commit that only a non-branch ref matched by the pattern reaches. The docstrings say nothing is lost to the answer.

    • Reproduction: tools/answer.py and tools/answer2.py (logs/answer-*.log, logs/answer2-*.log). The setup:
      • The bare remote has main at M and one non-branch ref at C, a child of M.
      • The repository holds the glob and the refspec named in the table, and runs its own git fetch origin first.
      • Then comes bump-leg.py's own fetch_leg(repo, "example/leg", C, None): its reader, its exact fetch line, peeled_target, and on_a_remote_branch.
      • Then family.py's _origin_fetch_refspecs and _on_a_remote_branch.
      • Each ran on the exported main and head trees, on both gits, with identical results:
    case           refspec beside the glob                                ref holding C           main  head
    C25h-in        +refs/h*:refs/remotes/origin/hh/*                      refs/hidden/1           PIN   REFUSE
    C25h-unforced  refs/h*:refs/remotes/origin/hh/*                       refs/hidden/1           PIN   REFUSE
    heads-x-in     +refs/heads*:refs/remotes/origin/h*                    refs/heads-archive/old  PIN   REFUSE
    hidden-out     +refs/hidden/*:refs/remotes/origin/hidden/*            refs/hidden/1           PIN   PIN
    heads-x-out    +refs/heads-archive/*:refs/remotes/origin/h-archive/*  refs/heads-archive/old  PIN   PIN
    pulls-out      +refs/pull/*/head:refs/remotes/origin/pr/*             refs/pull/7/head        PIN   PIN
    
  3. NIT (docs, bump-leg.py:1103-1106): the tag paragraph keeps the claim this PR corrected elsewhere.

  4. NIT (format): scripts/bump-leg.py:1126 is 109 characters. The longest line of bump-leg.py on main is 81. The reflow missed "NOT WHERE THE LEG'S REFSPEC WAS WRITTEN WITHOUT ITS +".

  5. NIT (docs, shape_materialize.py:1708-1709): "so each such refspec puts a branch under another name" overclaims.

    • Where the part before the * is exactly refs/heads, the * starts with /. A destination with / before its * then gives git a funny ref and nothing is written. Reproduction: +refs/heads*:refs/remotes/origin/h/* with a branch x gives error: * Ignoring funny ref 'refs/remotes/origin/h//x' locally, exit 0 and no ref, on both gits (probe/slash2-*).
    • 50 of the 71 refspecs that the head leaves out where the oracle's remote showed no harm are of this kind: left out although this remote showed neither: 71 {'fix class refs/h*..refs/heads*': 50, 'other': 21}. The other 21 are left out on main too.
    • Leaving them out costs nothing for branches. For a non-branch ref that such a pattern does map, finding 2 applies. For example, +refs/heads*/x:refs/remotes/origin/release/* ignores release/ for branch x but writes origin/release/foo for refs/headsfoo/x, exit 0, on both gits (probe/rows-*).
  6. NIT (body): the merge-tree list is stale.

Not done

  • I did not run the SonarQube analyzer snippet. The S3776 counts above were taken by hand.
  • I did not run the gates again. rv1157's runs at the head commit (item 6) and CI on the head stand.

Lane: openreposhape-5 (openRepoShape-5)

…y a non-branch ref a `refs/heads*` pattern takes reaches is refused now, and the tests pin `refs/h*`, a suffix after the `*`, and that refusal end to end (#1100)

Fix round 1 of #1157, for the independent review rv1157/rv1157b
(issuecomment-6347113460: NOT READY, 2 SHOULD, 4 NIT).

SHOULD 1: two WHICH_CLAUSE rows, asked of both tools' readers:
`+refs/h*:refs/remotes/origin/hh/*` (the class's lower edge) and
`+refs/heads*x:refs/remotes/origin/h*x` (text after the `*`). Each is
carried on main and left out here; they kill B1 and B9.

SHOULD 2: leaving the class out changes the answer from pin to refuse
for a commit that only a non-branch ref the pattern takes reaches
(`refs/heads-archive/old` by `refs/heads*`, `refs/hidden/1` by `refs/h*`).
`bump-leg.py`'s #1066 paragraph, `origin_fetch_refspecs`' LEFT OUT BY
EITHER OF THE LAST TWO paragraph and `_left_out`'s comment now say so,
and say no BRANCH is lost. NO_BRANCH_A_PATTERN_TAKES pins it end to end
through `family.py bump` and through `bump-leg.py` via `origin` and via
`--local-remote-dir`.

NITs: the tag paragraph of `_leg_fetch_refspecs` names the unforced
exception (C20u); the 109-character line is reflowed;
`gives_a_branch_another_name` no longer says every such refspec puts a
branch under another name (`refs/heads*` onto `origin/h/*` writes none,
"Ignoring funny ref"). The scripts' code is unchanged: docstrings and
comments only.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1157 (#1100) fix round 1 for lane openRepoShape-5

Verdict: READY. All six review items (2 SHOULD, 4 NIT) are fixed as the body says, and each was measured. This round adds no BLOCKER and no SHOULD. There are three NITs, all in the body; the code and the docstrings need no change.

I re-checked baa19972a, which is refs/pull/1157/head and fix/1100-refspec-followups (read back with ls-remote at 21:44:50Z), against main 961640c68 (#1155). Nothing was pushed or changed: the scratch worktree is detached at baa19972a, clean at the end, and every mutant restore matched sha256 with dirty=0. Probes used local bare remotes only, on git 2.43.0 and git 2.47.1. For each tree the two gits gave the same output once probe paths were normalised.

What holds, measured

  1. SHOULD 1 (two mutants survived): fixed.

    • On the pure function (rows.py):
      row                                     main     head      B1        B9        F2
      +refs/h*:refs/remotes/origin/hh/*       carried  left out  carried   left out  left out
      +refs/heads*x:refs/remotes/origin/h*x   carried  left out  left out  carried   left out
      
      So each new row fails on main. B1 carries -refs-h, and B9 carries -suffix.
    • Mutants were applied alone to scripts/shape_materialize.py. Each run covered both test files with -k "which_clause or another_branchs_name or heads_pattern_takes" (88 selected, 35 deselected) on git 2.43. After every run the files were copied back, checked by sha256 (16c6ec378c36, 8f4af9c5e78c) and found clean (5 of 5):
      run                                        result
      main's code (961640c68) under head tests   17 failed, 106 passed
      B1 class from refs/he up                   5 failed: -refs-h x2, refs-h-pattern x3   KILLED
      B9 fix only where `*` ends the source      2 failed: -suffix x2                      KILLED
      F2 `bool(star) and` dropped                88 passed                                 SURVIVES
      B4 fix undone (`return False`)             17 failed                                 KILLED
      
    • These match the body's Mutants table row for row, and its "Rows failing on main" list name for name (8 WHICH_CLAUSE, 3 heads-pattern-into-a-folder, 6 NO_BRANCH_A_PATTERN_TAKES).
    • On main, the 6 new end-to-end rows fail after their premises pass: "member HEAD changed" for the member, and "head is not as it was found" for the leg. The real run pinned, as the body says.
    • F2 survives as the body names it. Finding 1 is about the reason the body gives for leaving it.
  2. SHOULD 2 (pin turned to refuse, undocumented): fixed.

    • What the docs now say:
    • A grep of scripts/, README.md, docs/ and AGENTS.md finds no "nothing is lost" / "loses nothing" claim about the answer left.
    • Measured end to end with the tools' own code (claims_rc.py): bump-leg.py's fetch_leg, then family.py's _origin_fetch_refspecs and _on_a_remote_branch, on exported main and head trees:
      case              refspec beside the glob                                ref holding C           main    head
      C25h-in           +refs/h*:refs/remotes/origin/hh/*                      refs/hidden/1           PIN     REFUSE
      C25h-unforced     refs/h*:refs/remotes/origin/hh/*                       refs/hidden/1           PIN     REFUSE
      heads-x-in        +refs/heads*:refs/remotes/origin/h*                    refs/heads-archive/old  PIN     REFUSE
      heads-x-unforced  refs/heads*:refs/remotes/origin/h*                     refs/heads-archive/old  PIN     REFUSE
      hidden-out        +refs/hidden/*:refs/remotes/origin/hidden/*            refs/hidden/1           PIN     PIN
      heads-x-out       +refs/heads-archive/*:refs/remotes/origin/h-archive/*  refs/heads-archive/old  PIN     PIN
      pulls-out         +refs/pull/*/head:refs/remotes/origin/pr/*             refs/pull/7/head        PIN     PIN
      pulls-onto-glob   +refs/pull/*/head:refs/remotes/origin/*                refs/pull/7/head        REFUSE  REFUSE
      
      • This is the body's table exactly.
      • The refusal code is bump-leg-commit-local-only, and family.py's answer agrees in every row.
      • The two readers gave the same list in every row.
      • On the head, the tools' fetch prunes origin/hh/idden/1 or origin/h-archive/old, and the next own git fetch origin exits 0 and writes it back.
    • The new e2e rows check their premises first (the ref reaches C, no branch does, no remote-tracking ref does). They run both the dry run and the real run: both_runs for the member, and assert_one_refusal via origin and --local-remote-dir for the leg.
  3. NIT 1 (the tag paragraph): fixed. Section C of the probe, on main and head alike, both gits:

    C20        +refs/tags/*:refs/remotes/origin/*  own-after 0,0  origin/x back at the tag
    C20u       refs/tags/*:refs/remotes/origin/*   own-after 1,1  "! [rejected] x -> origin/x (non-fast-forward)"; --force 0 (back at the tag), plain 0
    C20u-desc  same, tag's commit a child of x     own-after 0,0
    

    The new text at bump-leg.py:1105-1112 says exactly this.

  4. NIT 2 (the 109-character line): fixed. The longest line bump-leg.py adds is 75 characters. Its longest line overall is 81 (line 78), the same as on main.

  5. NIT 3 (the overclaim): fixed. Section B of the probe, on both gits:

    • +refs/heads*:refs/remotes/origin/h/* with branches main, x, ax and the ref refs/headsfoo/x exits 0.
    • It prints Ignoring funny ref 'refs/remotes/origin/h//x' (and the same for h//main and h//ax).
    • It writes only origin/h/foo/x. Unforced, it does the same.
    • +refs/heads*x:refs/remotes/origin/h*x writes origin/h/x, origin/h/ax and origin/hfoo/x.
    • The tools carry none of these.
    • The new docstring's "Or, ... writes no branch at all" and "Either way it brings no branch the first refspec does not bring" hold.
  6. NIT 4 (stale merge-trees): fixed when written. I re-ran them at 21:51:35Z (git merge-tree --write-tree <target> baa19972a). The targets were main and every open PR touching one of the four files (46 open PRs listed). All exit 0:

    These trees match the body. bump-leg.py: a tag a leg with no origin holds without its commit, or through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold YAML_NOT_TEXT_RE's exponents of two digits (#1103) #1131 has moved since (finding 3).

  7. The code is unchanged in this round. With the docstrings blanked, the AST shows SAME CODE for shape_materialize.py against a35a97ea5, and for bump-leg.py against main and against a35a97ea5. Other checks:

    • Main's one new commit touches only docs/release-notes/v1.2.0.md. The merge base is still 038dafba.
    • No new assert joins conditions with and or or.
    • The added lines are ASCII.
    • The commit subject is 230 characters.
  8. Gates at baa19972a (dirty=0, through run9.sh):

    • git 2.43, the three files that name the functions: 157 passed (g243.log);
    • git 2.47.1, the same three files: 157 passed (g247.log);
    • git 2.43, test_path_guard_hygiene.py, test_pinned_copy_text.py and the trio: 306 passed (hyg243.log).

    These are the body's counts. CI on baa19972a is green: tests (20:24:49Z-21:48:40Z), tests-git-bash, package and SonarCloud Code Analysis. The PR is a draft with no milestone.

  9. The body.

Findings

  1. NIT (body, Mutants): the reason given for leaving F2 is wrong.

    • The body says F2 "differs from this head only on a source that is not a pattern and is itself a start of refs/heads/: refs/, refs/h, ... refs/heads. No such full name can be a ref beside the remote's branches. A row for it would pin an arbitrary answer, so none was added."
    • Only refs/heads itself cannot be such a ref: update-ref refuses it with "cannot process 'refs/heads/main' and 'refs/heads' at the same time". refs/h and refs/head are created beside refs/heads/main with exit 0.
    • With +refs/h:refs/remotes/origin/hy (or +refs/head:refs/remotes/origin/hz) and a commit C that only that ref reaches:
      • on main and on the head, both tools carry the refspec and PIN C;
      • under F2, both tools leave it out and REFUSE C (bump-leg-commit-local-only, family_answer=False);
      • this is identical on git 2.43 and 2.47.1 (claims-{main,head,f2}-{243,247}.log, cases F2-refs-h and F2-refs-head).
    • So the head's answer is not arbitrary. It is the rule ANY OTHER SOURCE NAMES NO BRANCH, the same as hidden-out.
    • One WHICH_CLAUSE row would kill F2: pytest.param("+refs/h:refs/remotes/origin/hy", True, id=...), carried. It passes on main by design, as another-name-suffix does.
    • The sentence predates this round (it is in the first body), and the review did not test it.
    • Fix: add that row, or replace the sentence with the measured reason F2 survives.
  2. NIT (body, NIT 2's line): "The longest changed line is now 79 characters" measures 78.

    • The longest added line in the four files, over 961640c68..baa19972a and over a35a97ea5..baa19972a, is 78 characters. It is test_bump_leg_fetch_refspecs.py's assert contains(bare_of(remotes), made, ref), f"premise: {ref} reaches it".
    • In the scripts, the longest added lines are 75 (bump-leg.py) and 76 (shape_materialize.py).
    • Fix: say 78, or name the scripts' 75 and 76.
  3. NIT (body, Merge-trees): bump-leg.py: a tag a leg with no origin holds without its commit, or through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold YAML_NOT_TEXT_RE's exponents of two digits (#1103) #1131 has moved again.

    • Its head is 55c30f20 now, not a4beec9e (F3's NIT round).
    • git merge-tree --write-tree 55c30f20 baa19972a exits 0 with tree bd6ebc5cd3af.
    • Fix: update that line at the next body edit.

Not measured

  • The 39,385-config differential and the 53-case probe of the first round were not run again. I rely on the review's re-run (review items 1-2) for those. Not measured here.
  • The SonarQube analyzer and the S3776 counts were not run in this round. The scripts' code is unchanged from a35a97ea5 (item 7), and the SonarCloud check on baa19972a passed. Not measured here.
  • The CI tests log was not read. Only its conclusion (SUCCESS) was.

Lane: openreposhape-5 (openRepoShape-5)

…that `+refs/h:refs/remotes/origin/hy`, a start of `refs/heads/` with no `*`, is carried by both tools, so a clause 4 that drops its `bool(star) and` fails a row (#1100)

NIT round 2 of #1157, for the re-check of fix round 1 (finding 1). The first
body left F2 (clause 4 without `bool(star) and`) alive, on the ground that a
source that is a start of `refs/heads/` and no pattern, `refs/`, `refs/h`,
`refs/head` or `refs/heads`, cannot be a ref beside the remote's branches. It
can, for all but `refs/heads` itself: `update-ref` makes `refs/h` beside
`refs/heads/main`. With `+refs/h:refs/remotes/origin/hy` and a commit that only
that ref reaches, main's code and this head's carry the refspec and pin the
commit, and F2 leaves it out and refuses it. The head's answer is the rule ANY
OTHER SOURCE NAMES NO BRANCH, as `hidden-out`'s is. One row in the shared
`WHICH_CLAUSE` table, which `test_bump_leg_fetch_refspecs.py` imports, so both
tools' tests ask it. It passes on main by design, as `another-name-suffix` does.
Tests only: no script changes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1157 (#1100) NIT round for lane openRepoShape-5

READY at 368b87e9b

There is no MUST and no SHOULD. The commit does what the body says, the row kills F2, and each of the three NITs is answered truly. I found two small NITs of my own, one in the new wording and one that is stale in the body.

I re-checked 368b87e9b. ls-remote read it back as refs/pull/1157/head and as fix/1100-refspec-followups at 17:57Z. I ran every test through run9.sh with a detached scratch worktree. It ended clean at 368b87e9b: dirty=0, and the four touched files' sha256 match the values taken before the first mutant (shape_materialize.py 16c6ec378c36, bump-leg.py 8f4af9c5e78c, the two test files a1d940307de2 and 6a3fc3f1d86a). Nothing was pushed or posted.

The host reboot at 15:46Z killed a B9 run that was in progress. I found B9 still applied in my worktree and restored it with git checkout. The sha256 matched. I then re-ran B1, B9 and B4 from the start. Every log I cite below ends with an END line.

Findings

  1. NIT (wording, tests/test_family_bump_fetch_refspecs.py lines 580-582 and 690-693, the commit message, and the body's NIT 1): the new text says refs/h "takes the one ref of that name, which is no branch". That is not quite true. Git also completes a source that names no ref.

    • Measured on git 2.43.0 and 2.47.1. The remote had branches main, hy and refs/h, so the ref refs/heads/refs/h, and no ref refs/h. The tools' fetch line was the branches' glob plus +refs/h:refs/remotes/origin/hy. The result was fatal: Cannot fetch both refs/heads/hy and refs/heads/refs/h to refs/remotes/origin/hy, exit 128.
    • It needs a branch literally named refs/h. Main carries the refspec the same way, so this is not new in this round. It is the documented WHAT CAN STILL CLASH shape: a carried refspec whose source names no branch, onto a name that is a branch's.
    • It does not change the row. carried is still the head's rule, and the row kills F2. Fix: say "takes the ref of that name, or the branch git completes that name to", or leave it.
  2. NIT (body, Merge-trees section): it says "Main is still 961640c6". Main is ef619c74a now, four commits on. Those commits are family.py: a failed remove's by-hand lines spell a leg mounted at a path that is not UTF-8 as a printf word of octal escapes ending in a slash, so git -C <member>/<leg> and submodule update -- <leg> typed in any POSIX shell name that leg (#1086) #1098 (family.py), scaffold-project.py: a push ssh refused the host's key for names how to check that key against GitHub's fingerprints, not the ruleset hint, and the hint on GitHub says what its re-run can still wait at (#1108) #1130 (scaffold-project.py), tests/test_bom_tool_io.py: the byte-order-mark rows hold each tool's own read and write and that only U+FEFF is cut, so a utf-8-sig read that drops the mark and a cut that also takes spaces no longer survive (#1176) #1188 (test_bom_tool_io.py) and ef619c74a (tests/test_adopt_check_throwaway_repos.py).

    • None of them touches the PR's four files; the overlap is empty.
    • git merge-tree --write-tree ef619c74a 368b87e9b exits 0 with tree e4d3b8bd200a.
    • The line was true at its own 12:06Z stamp, so this is harmless. It matters only at landing, which will update the branch anyway.

Checked and true

Not checked

  • The 39,385-config differential and the 1,938-refspec oracle corpus were not re-run. The claim that F2 differs from the head only on a source that is no pattern and is a start of refs/heads/ follows from the clause's code, which I read, and the pure-function table.
  • The list of open PRs that touch the four files was not re-listed. I used the body's six targets, and I did not look for a PR opened since 12:06Z.
  • I did not re-run the SonarQube analyzer or read CI on 368b87e9b. The commit changes one test file only, and the scripts are baa19972a's, where SonarCloud passed.
  • I ran no gate on the merged tree with current main. The merge is clean and main's four new commits touch none of the PR's four files.

(Posted by lane openRepoShape-5's coordinator. rc1157b is lane 5's independent sonnet re-checker; it wrote none of #1157. Neither NIT needs a new head: NIT 1's wording is corrected in the body before #1157 lands, and NIT 2's merge-tree note is a dated snapshot.)

Lane: openreposhape-5 (openRepoShape-5)

@brettheap
brettheap marked this pull request as ready for review October 23, 2026 19:09
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 hours and 40 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-23T20:36:20Z) Follow-up filed: #1230, rc1157b's NIT 1, the refs/h wording in tests/test_family_bump_fetch_refspecs.py (issuecomment-6371455922). Nothing in it blocks #1157.

Lane: openreposhape-5 (openRepoShape-5)

brettheap added a commit that referenced this pull request Oct 24, 2026
… the docstring of `test_which_clause_leaves_each_refspec_out` say the source `refs/h` takes the one ref git completes it to, which may be a branch or tag, not `no branch` (#1230) (#1235)

#1230 is a wording NIT left by the re-check of #1157 (#1100, NIT 1). Two passages of `tests/test_family_bump_fetch_refspecs.py` said that the carried refspec `+refs/h:refs/remotes/origin/hy` takes "the one ref of that name, which is no branch". They now say what git does with the source `refs/h`: it takes the one ref that name stands for, which is `refs/h` itself or a ref git completes it to, such as the branch `refs/heads/refs/h` or the tag `refs/tags/refs/h`. A reader of the `carried` rule is no longer told that the source is never a branch.

Wording only, in one comment and one docstring of that file. No row, no script, no other file. Two commits: the first (`644982da1`) is the wording, and round 1 (`81582301e`) answers the independent review rv1235 (#1235 issuecomment-6375269462: READY, no MUST, two SHOULDs, three NITs). See "Round 1" below.

The file's blob on main is `6e752bbca9c9`, the same as at #1157's head, and it is still that blob on main `0ed3e57f8`. The two passages are at the lines the issue names:

```
580: #: `carried` where none does (`carried-refs-h-not-a-pattern`, a start of
581: #: `refs/heads/` with no `*`, is no pattern: it takes the one ref of that
582: #: name, which is no branch), and `skipped` for a negative refspec, which
...
690:     of `refs/heads/` that is no pattern and takes only the ref of that name,
691:     which is no branch and may stand beside the branches (the re-check of
692:     #1157, NIT 1), and a branch, a folder of them or a pattern of them,
```

That is true of an exact `refs/h` only. A remote can also hold a branch `refs/heads/refs/h` or a tag `refs/tags/refs/h`, and git completes the source `refs/h` to either. Measured on this branch's base, with a local bare remote per row (no network, `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`), the remote holding `main` and `hy` from one empty commit and then the ref of the row, and a fresh mount with that remote as `origin` running

```
git fetch origin '+refs/heads/*:refs/remotes/origin/*' '+refs/h:<destination>'
```

git 2.43.0 and git 2.47.1 gave the same answers:

```
the remote also has            destination                 result
branch refs/heads/refs/h       refs/remotes/origin/hy      128 Cannot fetch both refs/heads/hy and refs/heads/refs/h to refs/remotes/origin/hy
tag refs/tags/refs/h           refs/remotes/origin/hy      128 Cannot fetch both refs/heads/hy and refs/tags/refs/h to refs/remotes/origin/hy
exact ref refs/h (update-ref)  refs/remotes/origin/hy      128 Cannot fetch both refs/heads/hy and refs/h to refs/remotes/origin/hy
none of these                  refs/remotes/origin/hy      128 couldn't find remote ref refs/h
branch refs/heads/refs/h       refs/remotes/origin/not-a-branch    0
tag refs/tags/refs/h           refs/remotes/origin/not-a-branch    0
exact ref refs/h (update-ref)  refs/remotes/origin/not-a-branch    0
```

So the source can be a branch or a tag, and what clashes is the destination: the first three rows fail because `origin/hy` is where the glob puts the branch `hy`, and the same sources fetch with exit 0 onto a name no branch has. The fourth row is the other documented way a carried refspec fails, a source the remote lacks, and is not this clash.

The review (rv1235) measured two more completions on both gits: `refs/refs/h` and `refs/remotes/refs/h` are taken too (exit 0 onto a free name, and 128 `Cannot fetch both refs/heads/hy and refs/refs/h` and `... refs/remotes/refs/h ...` onto `origin/hy`), so the list of what git completes the name to is open. It also measured a carried branch: `+refs/heads/main:refs/remotes/origin/main` and `+refs/heads/rel/*:refs/remotes/origin/rel/*` exit 0, where `+refs/pull/7/head:refs/remotes/origin/main` exits 128 `Cannot fetch both refs/heads/main and refs/pull/7/head`. So a carried source clashes onto a branch's name when it is not that branch.

Both passages say the same thing, in this file only. The comment above `WHICH_CLAUSE`, lines 580-586 (reflowed to the end of its sentence):

```
#: `carried` where none does (`carried-refs-h-not-a-pattern`, a start of
#: `refs/heads/` with no `*`, is no pattern: it takes the one ref that name
#: stands for, `refs/h` itself or a ref git completes it to, such as the
#: branch `refs/heads/refs/h` or the tag `refs/tags/refs/h`, and not the
#: branches as a set), and `skipped` for a negative refspec, which no
#: clause is asked of. `bump-leg.py`'s fetch of a leg is asked the same
#: rows (`test_bump_leg_fetch_refspecs.py`).
```

The docstring of `test_which_clause_leaves_each_refspec_out`, lines 692-697:

```
    of `refs/heads/` that is no pattern and takes only the one ref that name
    stands for (`refs/h` itself or a ref git completes it to, such as the
    branch `refs/heads/refs/h` or the tag `refs/tags/refs/h`), whose fetch
    clashes, as a pull request's ref does, where its destination is a
    branch's name (the re-check of #1157, NIT 1), and a branch, a folder of
    them or a pattern of them, each under the name `BRANCHES` gives it."""
```

- The diff against main is 11 lines added and 7 removed in `tests/test_family_bump_fetch_refspecs.py`, all in that comment and that docstring.
- The longest added line is 76 columns and the file's longest line is still 79. The file holds no non-ASCII line, and `git diff --check` is clean.
- The commit message of `0d76e4c59` and #1157's body carry the old sentence and cannot be changed. These two passages are the only place to correct it.
- The code's own docstrings are left alone. `scripts/bump-leg.py` (1165) and `scripts/shape_materialize.py` (1874) define the clashing shape by examples and make no claim about `refs/h`. The docstring of `gives_a_branch_another_name` (`scripts/shape_materialize.py:1721-1722`) says "ANY OTHER SOURCE NAMES NO BRANCH", which is untrue where git completes the source to a branch. It is a shipped tool, so it is left for #1238; this PR is wording only, in the test file.

Rows failing on main: none. No row is added or changed, so there is nothing for main to fail. Mutants: none, because no behaviour changed; `carried-refs-h-not-a-pattern` is the same row, and the syntax tree below shows nothing outside the docstring moved.

- **SHOULD 1.** The docstring said `refs/h` clashes "like any carried source". A carried branch does not clash onto its own name, so "any" was untrue. It now says the fetch of the source clashes "as a pull request's ref does", where its destination is a branch's name.
- **SHOULD 2 and NIT 3.** Both passages listed "the branch or tag of that name", which reads as a complete list and could be taken for a branch `h`. Both now say `refs/h` itself or a ref git completes it to, "such as" the branch `refs/heads/refs/h` or the tag `refs/tags/refs/h`, with the refs spelled. The docstring says the fetch clashes (not the ref), and its citation, "(the re-check of #1157, NIT 1)", follows that clause in parentheses.
- **NIT 4.** The sentence "make no claim about `refs/h`" is replaced by the paragraph above: the one code sentence that does cover it is left for #1238.

All through the lane's runner `run9.sh` (stub `gh` first on PATH, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`), on the committed head `81582301e` with the tree clean (`dirty=0`), Python 3.12.3. The `git` on that runner's PATH is `/usr/bin/git`, git 2.43.0 (measured with `command -v git` and `git --version` under the runner's PATH). The review's own runs had git 2.47.1 first on PATH and gave the same counts.

- **Syntax tree** (`python3 -I`, `ast.dump`): the file at this head and on main `0ed3e57f8`, each with the docstring of `test_which_clause_leaves_each_refspec_out` removed, are equal (75050 characters of dump both). A control that renames `WHICH_CLAUSE` reports them different (75050 against 75052).
- **`-k carried-refs-h-not-a-pattern`** over `tests/test_family_bump_fetch_refspecs.py` and `tests/test_bump_leg_fetch_refspecs.py`: 2 passed, 123 deselected, as at `368b87e9b`.
- **`tests/test_repo_hygiene.py` and `tests/test_path_guard_hygiene.py`**: 222 passed.
- **`git grep -n 'which is no branch' -- tests/test_family_bump_fetch_refspecs.py`**: finds nothing at this head (it finds lines 582 and 691 on main). The one other line of the file that has both `refs/h` and "no branch" is line 691, where "no branch" is said of `refs/headsx/`.

Merge-trees: `git merge-tree --write-tree origin/main HEAD` is clean (exit 0). Main was `0ed3e57f8` when read, and the merge base is `e04826db4`; main's one commit since touches none of the three files named here. Of the 49 open PRs, only this one touches `tests/test_family_bump_fetch_refspecs.py` (read from `gh pr list --json files`; the largest has 9 files).

Review: rv1235, lane 5's independent sonnet reviewer, which wrote none of #1235: READY at `81582301`, H0 of this landing (issuecomment-6375412139), after the two SHOULDs of its first round were fixed (issuecomment-6375269462); its three NITs are optional. CI (Brett Heap's ruling 27): H0 is the head 8158230 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base e04826d; and the local gate passed on its predicted squash tree 0792b0c over main e9a1650: the family_bump_fetch_refspecs, repo_hygiene, agents_md_codes, cli_reference sets, 228 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1230

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…r through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold `YAML_NOT_TEXT_RE`'s exponents of two digits (#1103) (#1131)

The three things rv1090's review of #1090 (#912) left (#1090 issuecomment-6316009648): a row that holds `YAML_NOT_TEXT_RE`'s exponents of two or more digits, a true refusal where a leg with no `origin` holds a tag without its commit, and no `else` that nothing reaches. This is #1103. Brett Heap's ruling of 2026-10-21T10:32:04Z held it until the v1.2.0 tag, and it is now on the v1.2.1 milestone.

Three commits: `9d14c29a` on main `6da757cb`, a merge of main `038dafba` (clean), and fix round 1 `a4beec9e`, which answers rv1131's review (#1131 issuecomment-6321276436, READY with two NITs): see `## Fix round 1`. Together they change `scripts/bump-leg.py`, `tests/test_neutral_pin_entry_one_spelling.py` and `tests/test_bump_leg_reachability_words.py`. The sections down to `## Runs` are the first commit's, as rv1131 reviewed them.

Each claim was measured on main `6da757cb` by name; the issue's lines are at `d5389eb6`. All three held, at the same names.

- **The SHOULD held.** `YAML_NOT_TEXT_RE` (`scripts/shape_materialize.py:538-543`) has two exponent groups, `(?:[eE][-+][0-9]+)?` and `(?:[eE][-+]?[0-9]+)?`, and every exponent in `YAML_FLOATS` (`tests/test_neutral_pin_entry_one_spelling.py:92`) has one digit.
  - rv1090's R2 changes `[0-9]+` to `[0-9]` in both groups.
  - Applied to main, R2 passes all 208 rows of the spelling file, `test_scaffold_pin_and_reuse.py` and `test_adopt_pins.py` that `-k "spell or entry or name or rule or stock or every"` selects.
  - The head before this change quotes `.5e-10`, `.5E+10`, `.5_e-10`, `.5e10` and `.05E-12` correctly, so this is a missing row, not a defect.
- **NIT 1 held.** `_unreachable_refusal` (`scripts/bump-leg.py:1239`) was called by the probe on a leg that holds the tag `v1`'s object but not its commit.
  - With `has_origin=False` and either `fetched`, it said `bump-leg-commit-not-on-remote: <tag> is not in the leg's own object store, and the leg's mount, <leg>, has no remote named `origin` to fetch it from, ...`. That is untrue: `cat-file -t` says `tag`.
  - With `has_origin=True` it said `opensoft/openGlass has no branch containing <tag>`, which is true.
- **NIT 2 held.** A line trace of `peeled_target` (`scripts/bump-leg.py:979`) ran over a commit, a tag, a tag of a tag, a tree, a blob, a tag of either, a tag and a tag of a tag held without their commit, and an id the leg lacks.
  - `names a {kind}` was reached by the tree and the blob, and `is an annotated tag of a {names}` by the tag of a tree and the tag of a blob.
  - `else: what = "is an annotated tag that names no commit"` was reached by nothing.

- **`YAML_FLOATS`** gains `.5e-10`, `.5E+10`, `.5_e-10` and `.5e10`, as the issue asks.
  - It also gains `.5.5e-10`. All four of those reach the second pattern (`\.[0-9_]+`), so a cut to the first group's exponent alone still passed them. Only the first pattern, YAML 1.1's as written, takes `.5.5e-10`. Each group is now held on its own (R2a and R2b below).
  - The list's comment no longer says `--pin` admits every name in it: `.e+5`, `.5e+3` and `.5E+10` hold a `+`, which `PIN_ARG_RE` refuses, so it says a plain scalar can spell them.
  - Each new name runs through the two rows that take `YAML_FLOATS`: double-quoted and read back, and double-quoted by the rule alone. That is 10 new rows.
- **`_unreachable_refusal`**: where the leg has no `origin` and `object_kind(id)` is `tag`, the refusal keeps its code, `bump-leg-commit-not-on-remote`. Its words are now:
  - `<tag> is an annotated tag the leg's own object store holds without the commit it names, and the leg's mount, <leg>, has no remote named `origin` to fetch that commit from, so whether a branch of <repository> reaches it is not known`
  - `Remediation: check the tag -- the commit it names belongs to the LEG, not to the assembly root -- and push that commit to a branch of the leg first if it is not pushed yet; give <leg> back a remote named `origin`, ...` (an em dash in the tool, as in the words beside it).
  - An id the leg does not hold keeps its words, and so does every case with an `origin`.
  - Fix round 1 gives two rarer held tags this branch caught their own true words: a tag through a tag the leg lacks, and a tag of a tree or a blob.
  - No other text pins the old sentence. `grep` of `tests/`, `README.md`, `docs/`, the handbook and `AGENTS.md` finds it only in `bump-leg.py`, and `AGENTS.md`'s codes table is unchanged.
- **`peeled_target`**: the three-way `if`/`elif`/`else` is now one expression: `is an annotated tag of a {names}` for a tag, `names a {kind}` otherwise, with a comment saying why nothing else is left. The words for a tree, a blob and a tag of either are unchanged, by the same trace on the head. `shape_materialize.not_a_commit`, which `commit_on_forge` uses, still says "an annotated tag that names no commit"; it is not touched.

- `tests/test_bump_leg_reachability_words.py`, `test_a_tag_a_leg_with_no_origin_holds_alone_is_told_it_is_held`.
  - The setup: a tag of a commit pushed nowhere is made in a clone of the leg's remote, and only the tag object goes into the leg (`git hash-object -t tag -w`). The leg's `origin` is renamed to `upstream`, then both runs go through `assert_one_refusal`.
  - It asserts the note and the two lines exactly, through `told_no_origin`, and that the old sentence is not in stderr.
  - It reuses the file's `private_remote`, `without_origin`, `runner` and `origin_back`.
- **On main's code** (the two test files copied onto `6da757cb`, git 2.43): 1 failed, 159 passed. The one failure is that row, which got main's `REFUSED bump-leg-commit-not-on-remote: <tag> is not in the leg's own object store`.
  - The 10 float rows pass on main, as they should, because main's rule is right. They are there to kill R2, below.
- No assert joins conditions with `and` or `or`.

Each mutant was applied alone to a scratch clone holding this commit's files, then restored and compared with the worktree.

| Mutant | Rows run | Result |
| --- | --- | --- |
| R2: `[0-9]+` to `[0-9]` in both exponent groups | the spelling file | killed: 10 failed, 115 passed (all 10 new rows) |
| R2 on main's rows | the 208 rows above | survives: 208 passed |
| R2a: the first group only | the spelling file | killed: 2 failed (`.5.5e-10`, both rows) |
| R2b: the second group only | the spelling file | killed: 4 failed (`.5_e-10` and `.5e10`, both rows) |
| N1: the held-tag branch removed, so the old words answer | `-k "no_origin or held"` in the reachability file | killed: 1 failed, 3 passed |

All runs go through lane 5's `run9.sh`: a stub `gh` first on PATH, a bogus token, every `LANES_*` variable cleared, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`.

- **The two changed test files on the head:**
  - git 2.43: 160 passed;
  - git 2.47.1 (a git 2.47.1 build first on PATH; `git --version` printed `git version 2.47.1`): 160 passed.
- **git 2.43, every test file that runs `bump-leg.py`.** The commit changes production code in `scripts/bump-leg.py` only. `scripts/shape_materialize.py` is unchanged: only its test rows were added, and those ran on both gits above. So the related-file runs take the test files that name `bump-leg` or `bump_leg`, and the files that name only `shape_materialize` are not needed.
  - The set is `grep -rl -E 'bump-leg|bump_leg' tests --include='*.py'`: 56 files, sorted and split into two halves. It holds `tests/test_bump_leg_reachability_words.py` again, and `tests/test_repo_hygiene.py` and `tests/test_agents_md_codes.py`.
  - shard a, 28 files: 1038 passed, 1 skipped, 0 failed;
  - shard b, 28 files: 2324 passed, 1 skipped, 0 failed;
  - together 3362 passed, 2 skipped, 0 failed. Both skips are rows for another host: one needs a disk that ignores case (macOS, Windows), in `tests/test_bump_leg.py`, and one is Windows' own stdout default, in `tests/test_stdout_encoding.py`.
- **git 2.43, the trio** `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`: 170 passed.

Commit `a4beec9e`, on the merge of main `038dafba` into the branch (`ae830e2b`, clean, tree `10c2372fe578`). It answers rv1131's review (#1131 issuecomment-6321276436).

### What changed

- **NIT 1: the no-`origin` words for a held tag were untrue in two rarer cases.** The branch keyed on `object_kind(id) == "tag"` alone, so every held tag `peeled_target` cannot peel was told the leg holds it "without the commit it names". Each case now reads the tag chain from the tags' own headers, `git cat-file tag`'s `type` and `object` lines, one tag at a time while the leg holds the next (`tag_chain_end`, new). The peel (`<id>^{}`) says nothing where the leg lacks an object on the way.
  - **(a) A tag through a tag the leg lacks**, with the commit at the end held or not: still `bump-leg-commit-not-on-remote`, now in these words:
    - `<tag> is an annotated tag the leg's own object store holds without a tag it leads through, <inner tag>, and the leg's mount, <leg>, has no remote named `origin` to fetch that tag from, so what that tag names, and whether a branch of <repository> reaches it, are not known`
    - `Remediation: check the tag -- the tags it leads through, and what they name, belong to the LEG, not to the assembly root -- and push them to the leg first if they are not pushed yet, and any commit they name to a branch of it; give <leg> back a remote named `origin`, ...` (an em dash in the tool, as in the words beside it).
  - **(b) A tag, or a tag of a tag, of a tree or a blob the leg lacks** names no commit, so `peeled_target` now refuses it `bump-leg-target-not-a-commit`, in the words a tag of a held tree or blob already had: `--to <tag> is an annotated tag of a tree in the leg (<repository>), not a commit`. That is with or without an `origin`, since no fetch makes a tree a commit.
    - Measured first: for the same tag with its tree or blob held, main and `9d14c29a` both refuse it `bump-leg-target-not-a-commit` in those words. The new refusal is byte for byte the same as main's for the same tag id held with its tree or blob, in all 6 (leg, id) pairs of (b) below.
    - With an `origin`, such a tag was refused `bump-leg-commit-not-on-remote`, "<repository> has no branch containing <tag>", with "check the commit". That is the one with-`origin` answer this round changes.
  - **The issue's own case is as it was:** a tag held without its commit, and a tag of a tag with both tags held, keep the head's words, byte for byte.
  - Neither code is added or removed. Neither code, nor any changed sentence, appears in `README.md`, `docs/`, the handbook or `AGENTS.md`; `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py` pass.
  - `peeled_target` and `_unreachable_refusal` keep their docstrings' accounts true: the #912 paragraph now says a tag held without THE COMMIT it names, and each function says what (a) and (b) are now told.
  - A cycle of tags made with `refs/replace/` is never walked: git's own peel never leaves one (measured, git 2.43: `rev-parse <tag>^{}` was still running at a 10 s timeout), and the walk is reached only where the peel failed.
- **NIT 2: mutant M5 survived.** `.5.5E-10` is added to `YAML_FLOATS`: only the first exponent group takes it, and only with its `E`. The list's comment says so.

### Words compared with main's and the head's tool

The probe called `peeled_target` and `_unreachable_refusal` from main `038dafba`, the head `9d14c29a` and this head on 15 legs x 10 ids x 4 `(has_origin, fetched)`, as rv1131's did, with legs for (a) and (b) added. Each id was asked as typed (`raw`) and, where `peeled_target` returned, as it returned (`flow`). That is 1578 answers from main and the head, and 1554 from this head, where 6 (leg, id) pairs are refused before the verdict.

| Pair | The same | Differ |
| --- | --- | --- |
| `9d14c29a` vs this head | 1532 of 1578 | 46: (a) 16, (b) 30, and nothing else |
| main vs this head | 1492 of 1578 | 86: those 46, the issue's case (12) and 28 `raw` asks |

- **(a):** 4 legs, `has_origin=False` only. Each holds the outer tag without the inner one: a tag of a tag of a commit, with the commit or without it, and a tag of a tag of a tree, with the tree or without it.
- **(b):** 6 (leg, id) pairs, `peeled_target` and the four verdicts each: a tag of a tree and a tag of a blob, each alone and under a held tag of it, and those two tags of a tag, all held without the tree or blob.
- **The 28 `raw` asks** ask `_unreachable_refusal` directly about a held tag of a tree or blob, which the flow never does: `peeled_target` refuses it first, with the tree or blob held or not. They keep the head's words.
- **Every other answer**, every id with an `origin` but (b)'s, every missing id and every held commit keeps main's words.

### Rows, failing on `9d14c29a` and on main

In `tests/test_bump_leg_reachability_words.py`, in the file's style, through `assert_one_refusal` (both runs, exit 2, the same words, nothing written). They reuse `private_remote`, `without_origin`, `runner`, `told_no_origin`, `refusal` and `origin_back`. Two helpers, `pushed_nowhere` and `written_into_the_leg`, are split out of `held_without_its_commit`, which keeps what it did.

- `test_a_tag_through_a_tag_the_leg_lacks_is_told_which_tag_it_lacks[commit-held, commit-missing]`: (a). The leg holds only the outer tag; the commit is the leg's own pin, or one pushed nowhere. The note and the two lines are asserted exactly, and "without the commit it names" is not in stderr.
- `test_a_tag_of_a_tree_or_a_blob_the_leg_lacks_is_refused_by_name[...]`: (b), 6 rows. The kinds are a tag of a tree, a tag of a blob, and a tag of a tag of a tree with both tags held. Each runs with the leg's `origin` and without it. The tree and the blob are pushed nowhere, so no fetch brings them, and the row checks that afterwards. The two lines are asserted exactly, with the note where there is no `origin`.
- **On `9d14c29a`'s `bump-leg.py`:** 8 failed, 59 passed (git 2.43; the rows of `tests/test_bump_leg_reachability_words.py` and `tests/test_bump_leg_dry_run_truth.py`). The 8 are exactly the 8 new rows: the 2 of (a) and the 6 of (b). The old file was put back by sha256 afterwards.
- **On main `038dafba`'s `bump-leg.py`:** 9 failed, 58 passed (the same two files, git 2.43). The 9 are those 8 and the issue's row, `test_a_tag_a_leg_with_no_origin_holds_alone_is_told_it_is_held`. The old file was put back by sha256 afterwards.
- **`.5.5E-10`** adds 2 rows, one to each test that takes `YAML_FLOATS`. They pass on main and on the head, as they should: the rule is right, and they are there to kill M5.
- No assert joins conditions with `and` or `or`. Added lines are at most 79 characters wide. The only non-ASCII character is the em dash in the tool's own words.

### Mutants

Each was applied alone in the worktree, at this commit. Its rows were run, then the file was restored and checked against the commit by sha256. For `bump-leg.py` the rows are `tests/test_bump_leg_reachability_words.py` and `tests/test_bump_leg_dry_run_truth.py`; for `shape_materialize.py`, the spelling file.

| Mutant | Result | Rows that failed |
| --- | --- | --- |
| M5: `shape_materialize.py`, the first exponent group's `[eE]` cut to `[e]` | killed: 2 failed, 125 passed | `.5.5E-10`, in `test_a_name_yaml_takes_for_no_text_is_double_quoted` and `test_the_rule_quotes_it_whatever_the_projects_reader_reads` |
| NM1: `names = names or tag_chain_end(submodule, target)[0]` cut to `names = names`, so the header is never read where the peel says nothing | killed: 6 failed, 61 passed | the 6 rows of (b) |
| NM2: the walk's `tag = named` step replaced by a second `return`, so the chain is never followed past its first tag | killed: 2 failed, 65 passed | the 2 `tag of a tag of a tree` rows of (b) |
| NM3: (a)'s branch made dead, `if names == "tag":` to `"tags"` | killed: 2 failed, 65 passed | the 2 rows of (a) |
| NM4: a blob not refused, `("tree", "blob")` to `("tree",)` | killed: 4 failed, 63 passed | the 2 `tag of a blob` rows of (b), and `tag of a blob` for `local-remote-dir` and `origin` in `test_a_tree_a_blob_or_a_tag_of_either_is_refused_by_name_in_both_runs` |
| NM5: (a)'s note names the outer tag, `{named}` to `{commit}` | killed: 2 failed, 65 passed | the 2 rows of (a) |
| NM6: the held-tag words given with an `origin` too, `not has_origin and` cut | killed: 2 failed, 65 passed | `test_a_tag_the_leg_holds_without_its_commit_is_not_on_the_remote`, for `local-remote-dir` and `origin` |
| NM7: the header key read is `tag`, not `type` | killed: 8 failed, 59 passed | the 8 new rows |

No mutant survived. After each, `scripts/shape_materialize.py` and `scripts/bump-leg.py` were restored from the commit and their sha256 checked: `a37e3d83fbcda475...` and `d6822093b1aa4b0e...` both matched, with the worktree clean.

### Runs

All go through lane 5's `run9.sh`, as above, at `a4beec9e`.

- **The two changed test files** (`tests/test_neutral_pin_entry_one_spelling.py` and `tests/test_bump_leg_reachability_words.py`): 170 passed, which is the first commit's 160 and this round's 10 new rows.
  - git 2.43: 170 passed;
  - git 2.47.1 (a git 2.47.1 build first on PATH; `git --version` printed `git version 2.47.1`): 170 passed.
- **git 2.43, the path guard and the trio**: `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`: 219 passed.
- **git 2.43, every test file that runs `bump-leg.py`**, picked as above (`grep -rl -E 'bump-leg|bump_leg' tests --include='*.py'`): 57 files, which are the 56 files of the first commit's set and `tests/test_path_guard_hygiene.py`, which main gained since. Sorted and split in two:
  - shard a, 29 files: 1094 passed, 1 skipped, 0 failed;
  - shard b, 28 files: 2325 passed, 1 skipped, 0 failed;
  - together 3419 passed, 2 skipped, 0 failed. The two skips are the same two rows for another host as before: `tests/test_bump_leg.py` (a disk that ignores case) and `tests/test_stdout_encoding.py` (Windows' own stdout default).
- Every run in this section was on `a4beec9e` itself, committed, with the worktree clean (`dirty=0` in each log's START line).

Refreshed for fix round 1 (rv1131's body NIT). `git merge-tree --write-tree` of the head `a4beec9e` was run against origin/main, re-fetched (`-c gc.auto=0 -c maintenance.auto=false`), and against each open PR that touches these files:

| Against | Head | Result |
| --- | --- | --- |
| origin/main, re-fetched at 15:16Z on 2026-10-22 | `038dafba9dbe` | clean, tree `f571322318b1`, the head's own tree: the head holds main by its merge |
| #1107 `fix/1019-points-at-row` (DRAFT) | `ee48b5fe9693` | clean, tree `7383abd73770` |
| #1157 `fix/1100-refspec-followups` (DRAFT) | `a35a97ea52f4` | clean, tree `de32065b7371` |

- #1107 touches `tests/test_bump_leg_reachability_words.py`. It gives `made_in_the_leg` an optional `on=` and adds rows after `test_a_tag_of_a_tag_the_two_share_reaches_it`.
  - The rows here call neither `made_in_the_leg` nor anything #1107 changes. The merged file parses, and no name in it is defined twice.
- #1157 touches `scripts/bump-leg.py`, `scripts/shape_materialize.py` and two refspec test files, so it shares only `scripts/bump-leg.py` with this PR.
  - Its two hunks there are inside `_leg_fetch_refspecs`, at lines 1109 to 1146 of main's file. This PR's hunks in that file are at main's lines 976 to 1043 (`tag_chain_end`, new, and `peeled_target`) and 1250 to 1289 (`_unreachable_refusal`), so the two do not meet and the merge is clean.
- None of the other 34 of the 37 open PRs touches these three files.
- The branch is no longer behind main: the merge `ae830e2b` brought in every file main gained since `6da757cb` (among them #1111's `tests/test_path_guard_hygiene.py`). None of them is one of these three files, and the runs above stand on the head's own tree.

rc1131 re-checked fix round 1 at `a4beec9e` and found it READY with four NITs (#1131, issuecomment-6347256815). The commit `55c30f20f005` mends the first three, in `scripts/bump-leg.py` and `tests/test_bump_leg_reachability_words.py`. NIT 4, the title, is not in a commit. Nothing the tool does changes: the production edit is a comprehension that gives the same dict and a longer docstring.

### What changed

- **NIT 1, SonarCloud python:S7494 at `bump-leg.py:999`.** In `tag_chain_end`, `dict(line.split(" ", 1) for line in lines if " " in line)` is now `{key: value for key, sep, value in (line.partition(" ") for line in lines) if sep}`, the form rc1131 gave.
  - **Same dict:** `tools/equiv2.py` compares the two on every list of up to two lines of up to five characters, and every list of up to three lines of up to three characters, over `a`, `b`, a space and a tab (2478716 distinct lists: the script runs the two ranges one after the other, and the 7311 lists of up to two lines of up to three characters sit in both, so it makes 2486027 comparisons, as rc1131's count of the union shows), and on a real tag header with a continuation line, a line with no space and a `type ` line with a trailing space: equal in all. The commit message says "every list of up to three lines"; the coverage is the one in this sentence (`equiv.py`'s first pass, 3864591 lists, drew its three-line lists from a prefix of the strings).
  - **The rule:** SonarQube's local analysis of a standalone file holding both forms (project `opensoft_openRepoShape`, python, MAIN) reports one issue, python:S7494 MINOR, at the line of the old `dict(...)` form, and none at the comprehension.
  - **SonarCloud's own analysis, read through the SonarQube API** (rc1131's reading, issuecomment-6352247887, posted 2026-10-22T22:00:11Z; lane 5 read the same two answers again between 12:05Z and 12:08Z on 2026-10-23): for PR 1131 the API lists exactly one issue, python:S7494 at `scripts/bump-leg.py:999` (MINOR, created 2026-10-22T13:31:04Z), and its status is CLOSED, so there are 0 open issues. The quality gate is OK on all five conditions (new reliability, security and maintainability ratings 1, new duplicated lines density 0.0, new security hotspots reviewed 100.0). The API gives no commit, so it does not say which analysis it read: it shows the issue closed, not that the analysis was of `55c30f20`.
- **NIT 2, the walk of two headers.** `TAGS_OF_NO_COMMIT` has a fourth kind, `tag of a tag of a tag of a tree`: three tags, all held, and the tree pushed nowhere, run with and without an `origin` like the other kinds. Its values are now `(what, depth)`, and the row makes `depth - 1` tags over the first one (`outer1-w1131`, `outer2-w1131`) where it made the one `outer-w1131` for the second kind; no other row reads the dict.
- **NIT 3, `tag_chain_end`'s docstring.** It now says `("tag", <id>)` is also what it returns "through a tag whose header misstates what it names (`type tag` over an `<id>` the leg holds as something else, which only a malformed tag has)".

### Rows, failing on main

Both rows of the new kind (`test_a_tag_of_a_tree_or_a_blob_the_leg_lacks_is_refused_by_name[tag of a tag of a tag of a tree-origin]` and `[...-no-origin]`) fail on main's `bump-leg.py` (`961640c6`) and pass on the head's before this commit (`a4beec9e`'s). The two test files that hold rows for `peeled_target` (`tests/test_bump_leg_reachability_words.py` and `tests/test_bump_leg_dry_run_truth.py`) run 69 rows:

| `bump-leg.py` from | Result | Failing rows |
| --- | --- | --- |
| main `961640c6` | 11 failed, 58 passed | the 9 rows of fix round 1, and the 2 new rows |
| `a4beec9e` (this PR before the commit) | 69 passed | none: the new rows hold on the code as it was |
| `55c30f20` | 69 passed | none |

With main's file the new rows fail on the code of the refusal, as the other rows of the kind do: the row expects `REFUSED bump-leg-target-not-a-commit`, and the run with an `origin` prints `bump-leg-commit-not-on-remote: testorg/Atlas-spec has no branch containing <the tag>` (main's `peeled_target` returns the tag as typed where its peel cannot be read, because the leg lacks the tree, and the run goes on to the reachability verdict), and the run without one prints the `git fetch origin ... has no remote named origin` note and goes the same way.

### Mutants

Each was applied alone to `scripts/bump-leg.py`, the 69 rows run on git 2.43.0, and the file restored from `HEAD`; all 11 restores matched (`bl=1a7a69d38f737dce`, `dirty=0`). The mutants are in `tools/mutate.py`.

| Mutant | Result | Rows that failed |
| --- | --- | --- |
| NX1 (rc1131's): the walk reads at most two headers, `while True:` as `for _ in range(2):` | **killed**: 2 failed, 67 passed | the 2 rows of the new kind: the walk stops at `tag`, and the refusal is `bump-leg-commit-not-on-remote`, not `bump-leg-target-not-a-commit` |
| NX0: the walk reads one header | killed: 4 failed | the 2 rows of the third kind and the 2 of the new one |
| NM2 (fix round 1's): `tag = named` replaced by a return | killed: 4 failed | the same 4 rows |
| NX2 (rc1131's): no check that the leg holds the next tag | killed: 2 failed | the 2 rows of `test_a_tag_through_a_tag_the_leg_lacks_is_told_which_tag_it_lacks` |
| C3: the comprehension's `key: value` as `value: key` | killed: 10 failed | the rows that read a tag's header |
| C6: the comprehension keeps the lines with no space (`if not sep`) | killed: 10 failed | the same 10 rows |
| NX3: the walk reads at most three headers | **survives**: 69 passed | none |
| C1: the `if sep` filter dropped | survives: 69 passed | none |
| C2: `rpartition` for `partition` | survives: 69 passed | none |

- **NX3 survives by construction.** A walk bounded at three headers gives the code's answer on every chain of up to three tags, and the longest chain the rows hold is three. Any bound survives a chain of that length, so a row of four tags would only move the survivor to a bound of four. NX1 is the mutant the review named, and it is killed.
- **C1 and C2 are equivalent for every header git writes.** The `type` and `object` lines each carry exactly one space and each appears once, and nothing reads another key. C1 keeps a line with no space as a key with an empty value, which `.get("type", "")` and `.get("object", "")` already say of a missing key. C2 splits at the last space, which for a line with one space is the first. Where they could differ is a hand-forged header that repeats a bare `type` line after `type tag`; no row forges one.

### Runs

Every run went through `run9.sh` with `EMAIL=service@opensoft.dev`, on `55c30f20` itself, committed, with the worktree clean (`dirty=0` in each log's START line). Logs are in `$Q/v124/n1131/`, where `$Q` is the lane's scratchpad.

| Run | git | Result |
| --- | --- | --- |
| `tests/test_bump_leg_reachability_words.py`, `tests/test_neutral_pin_entry_one_spelling.py`, `tests/test_bump_leg_dry_run_truth.py`, `tests/test_path_guard_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_pinned_copy_text.py` (`g243.log`) | 2.43.0 (`/usr/bin/git`) | 402 passed |
| the first three of those files (`g247.log`) | 2.47.1 (first on PATH; `git --version` printed `git version 2.47.1`) | 196 passed |

- The 57 files that run `bump-leg.py`, which fix round 1 ran in two shards (3419 passed), were not run again: the edit is inside `tag_chain_end`, and the rows that reach it are the two files above, which ran in full. CI's `tests` job on the pushed head is the full-suite run.
- `tests/test_path_guard_hygiene.py` passes: the commit adds no `is_file`, `is_dir`, `exists`, `is_symlink` or `os.path` guard.
- No added line is wider than 79 characters, none is non-ASCII, and no added assert joins conditions with `and` or `or`.

### Merge-trees at `55c30f20`

`git merge-tree --write-tree` of the head against origin/main, re-fetched (`-c gc.auto=0 -c maintenance.auto=false`), and against each open PR that touches one of this PR's three files. Of the 41 open PRs listed at 20:09Z (`gh pr list --json number,headRefName,headRefOid,files`), only #1157 and #1107, besides this one, touch them.

| Against | Head | Result |
| --- | --- | --- |
| origin/main, re-fetched before the push | `961640c681b5` | clean, tree `4d7968be5547`: the head's tree and `docs/release-notes/v1.2.0.md`, the one file main gained since `038dafba` (#1155) |
| #1157 `fix/1100-refspec-followups` (DRAFT) | `368b87e9be87` (re-measured at 2026-10-23T12:06:38Z, after #1157's NIT round, which changes only `tests/test_family_bump_fetch_refspecs.py`; `baa19972a271` gave the same clean merge with tree `bd6ebc5cd3af`, and `a35a97ea52f4` when this round began) | clean, tree `e036c02d6a12` |
| #1107 `fix/1019-points-at-row` (DRAFT) | `ee48b5fe9693` | clean, tree `a72145c05633` |

- This commit's hunks in `scripts/bump-leg.py` are at lines 979 to 1003, in `tag_chain_end`; #1157's against main are in `_leg_fetch_refspecs`, from line 1105. They do not meet.
- The other 38 of the 41 touch none of the three files.

### After the second re-check

rc1131 found this round READY at `55c30f20f` (#1131, issuecomment-6352247887), with two NITs of its own, both in this body and neither in the code. This body now answers both: the count of lists in NIT 1's equivalence check (2478716 distinct lists, 2486027 comparisons), and SonarCloud's own analysis (python:S7494 CLOSED, 0 open issues, gate OK, no commit named by the API). The one other change is the #1157 row of the merge-trees table, which moved when #1157's NIT round added a commit. The head `55c30f20f` is unchanged.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1131: READY at `55c30f20` (issuecomment-6352247887), with rc1131's NITs 1 to 3 fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 55c30f2 passed tests, tests-git-bash and SonarCloud; the head db8533a is a clean update-branch merge of main 0d5704b, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 8fe39a5 over main 0d5704b: the bump_leg_reachability_words, neutral_pin_entry_one_spelling, repo_hygiene, agents_md_codes, cli_reference sets, 351 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1103

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant