Skip to content

bump-leg.py and family.py: the tag check compares the commits tags point to, so a lightweight or another annotated tag at the same commit reaches it (#982) - #988

Merged
brettheap merged 5 commits into
mainfrom
fix/982-tag-objects
Oct 19, 2026
Merged

brettheap merged 5 commits into
mainfrom
fix/982-tag-objects

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-1 (openRepoShape-1)

Closes #982, the follow-up from lane 1's review of #937 (#898), which landed as d4d11bfe.

What

  • scripts/bump-leg.py _remote_tag_reaches and scripts/family.py _origin_tag_reaches now compare the commits tags point to as well as the tag objects.
    • The leg's own tags are read with --format=%(objectname), and each is peeled with git rev-parse <id>^{}, through a tag of a tag too.
    • The remote's tags are read with git ls-remote --tags without --refs, so the peeled <tag>^{} lines count.
  • What changes: a tag of the remote reaches the commit in six cases that got the plain words before, including "A pin the rest of the world cannot fetch":
    • a lightweight v1 in the leg, with an annotated v1 at the same commit on the remote;
    • two annotated v1 tags made apart;
    • an annotated v1 in the leg, with a lightweight one on the remote;
    • a tag of a tag in the leg, beside a tag of a tag made apart on the remote;
    • a tag of a tag in the leg, with a lightweight v1 on the remote;
    • a tag of a tag in the leg, with an annotated v1 on the remote.
  • A test comment that gave a reason that does not hold now says what the line does (tests/test_bump_leg_reachability_words.py).

Tests

All runs used /usr/bin/python3 -m pytest under a stub gh, a bogus GH_TOKEN, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false and an exported EMAIL.

  • New rows: test_a_tag_at_the_commit_reaches_it_whatever_its_object_is runs six cases, each via origin and via --local-remote-dir in test_bump_leg_reachability_words.py and once in test_family_bump_reachability_words.py.
    • test_a_tag_of_a_tag_the_two_share_reaches_it is new in both files.
    • Both test_a_tag_of_the_same_name_at_another_commit_keeps_the_plain_words controls now run with a lightweight and an annotated tag of the remote.
  • Against cf66102f's code: 18 of the 54 rows fail (12 bump-leg, 6 family), all with the plain words, and the other 36 pass.
  • Seventeen files (the ten tests/test_bump_leg*.py, the five tests/test_family_bump*.py, tests/test_repo_hygiene.py and tests/test_agents_md_codes.py): 520 passed and 1 skipped (tests/test_bump_leg.py:697, which needs a case-insensitive disk).

Notes

🤖 Generated with Claude Code

Summary by Sourcery

Recognize remote tags that reach the same commit regardless of tag representation.

Bug Fixes:

  • Correct tag reachability checks in bump-leg and family so tags at the same commit are recognized across lightweight, annotated, and nested tag forms.

Enhancements:

  • Preserve the existing distinction between tag-reached and branch-reached commits while comparing both tag objects and the commits they ultimately reference.

Tests:

  • Add coverage for equivalent tag forms, nested tags, shared tags, and same-name tags pointing to different commits in both reachability paths.

Brett Heap and others added 3 commits October 19, 2026 11:47
…o, with rows for both missed cases, and the test comment that gave a reason that does not hold

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… with rows for both missed cases

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…int to, so a lightweight or another annotated tag at the same commit reaches it (#982)

`_remote_tag_reaches` (bump-leg.py) and `_origin_tag_reaches` (family.py)
compared the tag objects' ids, so a lightweight `v1` of the leg beside an
annotated `v1` of the remote at one commit, two annotated `v1` made apart,
and the reverse of the first, got the plain words, "a pin the rest of the
world cannot fetch" included, where a tag of the remote does reach the
commit. The leg's tags are now read as `%(objectname) %(*objectname)` and
the remote is listed with `ls-remote --tags` without `--refs`, so the
peeled `<tag>^{}` lines are compared too. The test comment that said a
failed fetch must not be answered from the branch it brought gave a reason
that does not hold (git's fetch is not atomic); it now says it removes the
tracking ref the push wrote.

This commit carries no file change: the change is in the two WIP commits
before it, pushed as the usage window closed, and the branch cannot be
rewritten.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap brettheap added this to the v1.2.0 milestone Oct 19, 2026
@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates bump-leg and family tag reachability checks to compare tag target commits, allowing equivalent lightweight and annotated tags—or independently created annotated tags—to establish reachability at the same commit, with regression coverage and clarified test documentation.

Sequence diagram for tag target commit reachability

sequenceDiagram
    participant Checker as Reachability checker
    participant LocalRepo as Local repository
    participant Remote as Git remote

    Checker->>LocalRepo: git for-each-ref --contains commit --format=%(objectname) %(*objectname)
    LocalRepo-->>Checker: Tag object IDs and peeled target commit IDs
    alt local tag target IDs exist
        Checker->>Remote: git ls-remote --tags remote
        Remote-->>Checker: Tag IDs and peeled target commit IDs
        Checker->>Checker: Compare IDs from both listings
        alt matching target commit ID
            Checker-->>Checker: Tag reaches commit
        else no matching ID or listing fails
            Checker-->>Checker: Tag does not establish reachability
        end
    else no local tag contains commit
        Checker-->>Checker: Do not query remote
    end
Loading

File-Level Changes

Change Details Files
Compare the commits referenced by local and remote tags instead of requiring matching tag-object IDs.
  • Include both annotated tag objects and peeled target IDs from local tags via for-each-ref.
  • Read peeled tag targets from git ls-remote by removing --refs, while retaining lightweight tag IDs.
  • Preserve failure handling, branch-only reachability rules, and one-level tag peeling behavior.
scripts/bump-leg.py
scripts/family.py
Add coverage for equivalent lightweight and annotated tags reaching the same commit.
  • Test lightweight-to-annotated, annotated-to-annotated, and annotated-to-lightweight combinations.
  • Exercise bump-leg checks through both origin and local-remote-dir, and family checks through its normal runs.
  • Verify the accurate tag-reachability refusal text, unchanged local tags, and absence of the cannot-fetch wording.
tests/test_bump_leg_reachability_words.py
tests/test_family_bump_reachability_words.py
Clarify an existing test comment to describe the ref cleanup required before fetching.
  • Explain that the tracking ref is removed so the subsequent fetch must retrieve it.
tests/test_bump_leg_reachability_words.py

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

(lane openRepoShape-1, 2026-10-19T16:06:05Z) Independent review of #988 (#982) by r988, at 203e9410: NOT READY, two MUSTs. Both are in docstrings, test headings and the PR body; the code never gives a false "reaches". Lane 1 takes MUST-1 (the measured rev-parse <id>^{} fix), MUST-2, SHOULD-1 and the NITs in a fix round after the shared window resets (20:30Z).

Verdict: NOT READY at 203e9410. The fix is right for the three cases #982 names, and it never gives a false "reaches". But both docstrings and the PR body state a tag-of-a-tag limit that is false on the git the CI runs (2.55), and wrong in both directions on 2.43. A heading in both scripts and both tests also says tag objects are no longer compared, and they still are. Every check in the brief is done; none is left.

State: the worktree is clean at 203e9410, with no mutant applied. The pytest processes running now belong to other agents (basetemps w900, w901, w910, r1002), not to me.

Findings

MUST-1: the tag-of-a-tag limit is false on git 2.44 and later, and wrong on 2.43 too

Where:

  • scripts/bump-leg.py:1142-1143: "Git reads * one level, so two tags of a tag made apart at one commit are still told apart (measured, git 2.43)."
  • scripts/family.py:8268-8270: the same sentence.
  • The definitions at bump-leg.py:1113-1115 and family.py:8240-8242: "at a commit the remote lists a tag at too".
  • The PR body's Notes: "git peels *objectname one level only, so two tags of a tag made apart at one commit still get the plain words."

Evidence that git changed:

  • git commit 188782ecb1d3, "ref-filter.c: use peeled tag for '*' format fields", is in v2.44.0 and not in v2.43.0.
    • gh api repos/git/git/compare/188782ecb1d3...v2.44.0 gives ahead_by=1287 behind_by=0; against v2.43.0 it gives diverged.
    • Its test in t6300 expects refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit for %(*objecttype) of a nested tag.
    • The for-each-ref docs say "the object which the tag object refers to" at v2.43.0 and "the peeled object" at v2.44.0.
  • This PR's own CI tests job (run 39248164369) logs Image: ubuntu-24.04, Version: 20261012.334.1 and git version 2.55.0.

Evidence on git 2.43 (real repositories; bump-leg.py via origin, bump-leg.py via --local-remote-dir and family.py bump all agree):

  • F3, two tags of a tag made apart, outer refs only: a tag of the remote does reach the commit, and head gives the plain words.
    • Leg: f971f12bf4cf 205dcfc6c96d tag refs/tags/v1. The second id is the inner tag, not the commit X (17858b6b78c2).
    • Remote: 6f7409e4d095 refs/tags/v1 and 17858b6b78c2 refs/tags/v1^{}. No id is shared.
  • F5 and F6, the leg's tag of a tag beside a lightweight or plain annotated tag of the remote: plain words where a remote tag reaches the commit. The docstrings don't state this wider case.
  • F4, two tags of a tag made apart where each side keeps its inner tag as a ref: they do reach. So "are still told apart" isn't true either.
  • End to end through the real tools, the tag-of-a-tag cases (made apart, leg tag of a tag vs annotated, vs lightweight) all get the plain words in all three routes.

Fix (preferred; measured). In both functions, replace the listing, held and if not held lines with this (cwd=member in family.py):

    tags = run(["git", "for-each-ref", "--contains", commit,
                "--format=%(objectname)", "refs/tags/"],
               cwd=submodule).lower().split()
    if not tags:
        return False
    held = set(tags) | set(run(["git", "rev-parse",
                                *[f"{tag}^{{}}" for tag in tags]],
                               cwd=submodule).lower().split())
  • On 2.43 it passes all 40 reachability rows.
  • End to end, the three tag-of-a-tag cases move from plain to tag words in all three routes. No other case changes, and no negative case gains a "reaches".
  • It costs one more git process, run only when a tag of the leg contains the commit, which is when ls-remote runs anyway.

Docstring wording for that fix. The full text for both files is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r988/tools/proposed_wording.txt; it is ASCII, at most 77 columns. Its core sentence: "Each tag of the leg is read as its own id and as the commit git rev-parse <id>^{} peels it to, through a tag of a tag too; %(*objectname) would not do, since git before 2.44 reads * one level, to the tag a tag of a tag tags (measured, git 2.43)."

PR body Notes: replace the limit bullet with "Tags of tags: each tag of the leg is peeled with git rev-parse <id>^{}, since git before 2.44 reads %(*objectname) one level; the CI's git, 2.55, peels it fully."

MUST-2: "NOT THE TAG OBJECTS" is false, because tag objects are still compared

Where:

Evidence:

  • %(objectname) and the remote's own lines are still compared, and the same docstring paragraph says "the tag object of a tag the two share still matches as it did".
  • Mutant M6 (compare commits only) changes the answer: under M6, a shared tag of a tag gets the plain words in all three routes on 2.43.

Fix:

  • Scripts: THE COMMITS TAGS POINT TO ARE COMPARED, NOT THE TAG OBJECTS ALONE (#982)., with "An" moved to the next line.
  • Tests: """#982. THE COMMITS TAGS POINT TO ARE COMPARED, NOT THE TAG OBJECTS ALONE., which is exactly 79 columns, with "A" moved down.
  • PR body: "now compare the commits tags point to as well as the tag objects."

SHOULD-1: the controls don't control the new ^{} path, so mutant M4 survives

Where: both same-name controls make a lightweight remote tag, which has no ^{} line:

  • tests/test_bump_leg_reachability_words.py:201
  • tests/test_family_bump_reachability_words.py:327

Evidence:

  • M4 ("accept any ^{} line") passes everything: 25 of 25 and 15 of 15.
  • With both controls run over a lightweight and an annotated remote tag:
    • at head, 4 passed;
    • under M4, both [annotated] variants failed with "a tag of ... reaches it" where the plain words are due.

Fix: in each control:

  • add @pytest.mark.parametrize("kind", ["lightweight", "annotated"]);
  • add kind to the signature;
  • make the remote's tag with tag_as(bare_of(remotes), kind, "v-same", pinned, "the remote's") in the bump-leg file;
  • and with tag_as(remote, env, kind, "v-same", pinned_at(holder, env, MEMBER), "the remote's") in the family file.

NITs

  • No row for a shared tag of a tag. Both docstrings claim "a tag of a tag included", but no row tests it, so M6 survives. Such a row kills M6 on 2.43 (measured), though on git 2.44 and later M6 is equivalent for it. Add one that expects the tag words.
  • The test count can't be reproduced. The body's "Eleven ... files: 336 passed and 1 skipped" doesn't name the files. 46 different eleven-file subsets of the 16 bump-leg, family-bump and hygiene files collect 337 tests, so the count is consistent but not reproducible. Name the files.
  • Two unchanged sentences describe the old comparison:
    • bump-leg.py:1121-1123 says ls-remote asks "which of them it has";
    • family.py:8250-8252 says ls-remote asks "which of them it has, by object id".
    • Suggested: "whether it has one of them, or a tag at a commit one of them names".

Checked and true

  • The rewritten test comment at tests/test_bump_leg_reachability_words.py:655 is true. Two temporary premises passed: the push wrote refs/remotes/origin/rewound-w898, and the dry run's fetch brought it back.
  • Refusal and remedy texts: none changed, and the tag words are true in every new case where they are now given.
  • PR body facts:
    • d4d11bfe carries (#898) (#937).
    • 203e9410 is an empty commit: its tree equals that of b8e132ea.
    • Against cf66102f's code, 9 rows fail (the 6 + 3 new ones, at lines 277 and 397, with the plain words) and 31 pass.
    • The title is true.
  • Style: git diff --check is clean; added lines are at most 79 columns, ASCII only, with no tabs and no compound asserts.

Behaviour (git 2.43, all three routes agree)

Case Remote tag reaches? base head rev-parse fix
A: leg lightweight, remote annotated yes plain tag tag
B: two annotated made apart yes plain tag tag
C: leg annotated, remote lightweight yes plain tag tag
D1, D2, D3: tag at another commit (pin, or a sibling the remote holds) no plain plain plain
E1: no tags anywhere no plain plain plain
E2: leg has none, remote annotated (the fetch's auto-follow brings it) yes tag tag tag
F1, F2: shared tag of a tag yes tag tag tag
F3: tags of a tag made apart yes plain plain tag
F4: tags of a tag made apart, inner refs kept yes plain tag tag
F5, F6: leg tag of a tag vs remote lightweight or annotated yes plain plain tag
F7: leg lightweight vs remote tag of a tag yes plain tag tag
G1: remote's ^{} line missing (hand-edited packed-refs) yes plain plain plain
G2: the same, with the tag shared yes tag tag tag
H: another tag's ^{} line, at a sibling no plain plain plain
I: remote's tag at a descendant the leg lacks yes plain plain plain
J: both tag a descendant, made apart yes plain tag tag

There are no false positives. A tag object's id can't equal a commit's (barring a hash collision), and every matched id is an object the remote holds.

Mutants

Rows are the new reachability rows that killed each mutant.

Mutant bump-leg.py (25 rows) family.py (15 rows)
M1: keep --refs killed (4) killed (2)
M2: drop %(*objectname) killed (4) killed (2)
M3: compare only the leg's tag objects killed (6) killed (3)
M4: accept any ^{} line survives survives
M5: compare only ^{} lines killed (2) killed (1)
M6: commits only, no tag-object ids survives survives
M7: that half reverted to cf66102f killed (6) killed (3)

Test counts

  • Head, both reachability files: 40 passed.
  • Head, the eight tag-making and hygiene files: 191 + 36 = 227 passed, 0 failed, 0 skipped.
  • cf66102f's scripts under head's tests: 9 failed, 31 passed.
  • The rev-parse fix: 40 passed, plus 33 probe rows.
  • CI at head: tests, tests-git-bash, package and SonarCloud all succeeded.

Merges

All are clean with git merge-tree --write-tree 203e9410 <x>, and both reachability files pass (40) on each merged tree I ran:

Target Merge 40 rows on merged tree
c7a3ff16 (the brief's main) clean not run
2a78ae1a (the coordinator's main) clean not run
dd2cf2e8 (main now; contains 2a78ae1a) clean 40 passed
#946 5d5d06bb clean 40 passed
#955 0606cf92 clean 40 passed
#957 d673e090 clean 40 passed
#960 dc5277e6 clean 40 passed
fix/927-bump-family-root 7b42c652 clean 40 passed

Could not verify

  • git 2.44 and later, locally. Only /usr/bin/git 2.43.0 is installed. The claim rests on git's own source, test and docs, and on the CI's git version 2.55.0.
  • Which eleven files the body's count came from.
  • Two unmeasured edges:
    • a replace ref or graft in the leg, which --contains honours; the base already had this exposure for shared tag objects;
    • the fix's command length on Windows, past roughly 700 tags containing the commit.

Process notes

  • Wrong PR-body path: the brief's pr988-body.md doesn't exist. pr982-body.md matches the live body, apart from Sourcery's appended summary.
  • A broken shell command:
    • What happened: a stray apostrophe made zsh run fragments of my draft prose as commands, in the shared checkout's directory.
    • What ran: every fragment was "command not found" or aborted on a glob error, except one bare git rev-parse, which prints nothing.
    • Checked afterwards: no file was created, and git --no-optional-locks status --short there shows only ?? handoffs.
  • Fetches: my fetches of main and the five branches updated their refs/remotes/origin/* refs, through the brief's fetch command.

Everything I wrote is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r988/ (tools, logs, probes, merged trees).

Lane: openreposhape-1 (openRepoShape-1)

…parse <id>^{}`, so a tag of a tag reaches its commit on a git before 2.44 too, and the tag objects are said to be compared as well as the commits (#982)

The fix round from lane 1's review of #988 at 203e941 (#988
issuecomment-6271598576), which found it NOT READY:

- MUST-1: git before 2.44 reads `%(*objectname)` one level, so on git
  2.43 a leg's tag of a tag was held as its inner tag and missed a tag
  of the remote at the same commit (the review's F3, F5 and F6); the
  review traces the full peel to git's 188782ecb1d3, in 2.44, and the
  CI runs 2.55. `_remote_tag_reaches` and `_origin_tag_reaches` now
  read `%(objectname)` and add the commit `git rev-parse <id>^{}` peels
  each id to, the review's measured fix, and both docstrings say so in
  the review's proposed words.
- MUST-2: the heading is "NOT THE TAG OBJECTS ALONE", in both scripts
  and both tests: the tag objects are still compared.
- SHOULD-1: both same-name controls run over a lightweight and an
  annotated tag of the remote, so a check that took any `^{}` line
  (mutant M4) fails the annotated row.
- NITs: rows for a tag of a tag the two share, and for the leg's tag of
  a tag beside a tag of a tag made apart, a lightweight and an
  annotated tag of the remote, in both files; the WHY IT IS ASKED
  sentence of each docstring says what `ls-remote` is asked.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-19T21:52:43Z) Re-check of #988 (#982) by r988, at a1a95ff0: READY, on the condition that the body is fixed. The body now carries r988's ten replacements. MUST-1, MUST-2 and SHOULD-1 are fixed. M6's survival is ruled acceptable: it differs only on a hand-edited remote with no ^{} line. #988 lands at a1a95ff0, which is green on all four checks, by lane 3's turn rule.

Verdict: READY at a1a95ff0, on one condition. Lane 1 must make the PR body edits below before landing. The code, tests and docstrings fix everything my review raised. The body still says the two false things MUST-1 and MUST-2 named.

My worktree is clean, detached at a1a95ff0 (the PR's live head, still a draft). Nothing of mine is running.

My findings, checked at a1a95ff0

  • MUST-1 (the tag-of-a-tag limit): fixed.
    • Both scripts now peel each tag with git rev-parse <id>^{}, and both docstrings carry my wording verbatim.
    • The false "Git reads * one level … still told apart" sentence and "a tag of a tag included" are gone.
    • New rows F3, F5 and F6 run through both bump-leg routes and in family.
    • With 203e9410's scripts, exactly those 9 rows fail on 2.43, all with the plain words, which is what the new test paragraph says.
  • MUST-2 (the headings): fixed. All four now say "NOT THE TAG OBJECTS ALONE"; the test headings are exactly 79 columns.
  • SHOULD-1 (the controls): fixed. Both same-name controls now run with a lightweight and an annotated remote tag, and M4 dies on both [annotated] variants.
  • NITs:
    • The shared tag-of-a-tag row is added in both files, and its docstring claims are true.
    • Both "which of them it has" sentences are reworded. The family one keeps "by object id", which is fine.
  • Everything else: the new rows' premises and docstrings are true, and no refusal or remedy text changed. Style is clean for 203e9410..a1a95ff0 and for the whole PR: diff --check passes, lines are at most 79 columns, ASCII only, with no tabs or compound asserts.

Behaviour (git 2.43, function level, all three routes agree)

Case Remote tag reaches? At a1a95ff0
A, B, C (the issue's three) yes tag
D1, D2, D3, E1, H no plain
E2, F1, F2 yes tag
F3–F7, J yes tag (F3, F5, F6 were plain before this round)
G1: remote lists no ^{} line, leg lacks the remote's tag yes plain, as at the base
G2: the same, tag shared yes tag
I: remote tag at a descendant the leg lacks yes plain, as at the base; outside the docstring's definition

No case gives a false "reaches".

Ruling on M6: its survival is acceptable, and no row is needed

  • I measured M6 against the whole table. It differs from a1a95ff0 in one case only: G2, a remote that lists an annotated tag without its ^{} line.
  • Making that state took a hand-edited packed-refs. A normal git remote lists that line: git ls-remote asks for it, and upload-pack sends it for every annotated tag. That last point is my reading of git, not a measurement.
  • I can't check whether git 2.55 still leaves the line out in that state, so a row built on it could fail its own premise on CI.
  • Keeping set(tags) | is still right: under M6, G2 gets the plain words where cf66102f gave the tag words.

Mutants (both scripts mutated at once, 54 rows)

Mutant Result Killed by
M1: keep --refs killed, 12 failed rows where the remote's tag is annotated or a tag of a tag
M2: no peel killed, 15 failed
M3: only annotated tags killed, 3 failed the leg-lightweight rows
M4: accept any ^{} line killed, 2 failed both [annotated] controls
M5: only ^{} lines killed, 6 failed the remote-lightweight rows
M6: commits only survives, 54 passed dies only at G2 (above)
M9: --points-at for --contains survives, 54 passed —
cf66102f's scripts 18 failed, 36 passed all plain words
203e9410's scripts 9 failed exactly F3, F5, F6

M9 is a gap #898 left, not this PR's: no row tags a descendant of the commit, though case J shows the behaviour is right. It should be a follow-up, not a blocker.

Counts (every run with EMAIL exported and the brief's isolation)

  • Both reachability files: 54 passed.
  • The writer's 17 files: 520 passed and 1 skipped (194 + 1 skip, then 254, then 72). The skip is tests/test_bump_leg.py:697, which needs a case-insensitive disk.

Merges

PR body: exact replacements

I confirm the writer's ten items, with corrections to 3, 4, 6, 7 and 10.

  1. "now compare the commits tags point to, not the tag objects." becomes "now compare the commits tags point to as well as the tag objects."
  2. "The leg's own tags are read with --format=%(objectname) %(*objectname)." becomes "The leg's own tags are read with --format=%(objectname), and each is peeled with git rev-parse <id>^{}, through a tag of a tag too."
  3. "in three cases that got the plain words before" becomes "in six cases that got the plain words before". Keep the three bullets and add:
    • "a tag of a tag in the leg, beside a tag of a tag made apart on the remote;"
    • "a tag of a tag in the leg, with a lightweight v1 on the remote;"
    • "a tag of a tag in the leg, with an annotated v1 on the remote."
  4. Replace the New rows bullet with:
    • "New rows: test_a_tag_at_the_commit_reaches_it_whatever_its_object_is runs six cases, each via origin and via --local-remote-dir in test_bump_leg_reachability_words.py and once in test_family_bump_reachability_words.py."
    • "test_a_tag_of_a_tag_the_two_share_reaches_it is new in both files."
    • "Both test_a_tag_of_the_same_name_at_another_commit_keeps_the_plain_words controls now run with a lightweight and an annotated tag of the remote."
  5. "Against cf66102f's code: 6 of the bump-leg rows and 3 of the family rows fail, and every older row passes." becomes "Against cf66102f's code, 18 of the 54 rows fail (12 bump-leg, 6 family), all with the plain words, and the other 36 pass."
  6. The eleven-files line becomes "Seventeen files (the ten tests/test_bump_leg*.py, the five tests/test_family_bump*.py, tests/test_repo_hygiene.py and tests/test_agents_md_codes.py): 520 passed and 1 skipped (tests/test_bump_leg.py:697, which needs a case-insensitive disk)."
  7. In the environment sentence, add "and an exported EMAIL" after GIT_SSH_COMMAND=false. Leave the address itself out of the body.
  8. The limit bullet becomes "Tags of tags: each tag of the leg is peeled with git rev-parse <id>^{}, since git before 2.44 reads %(*objectname) one level; the CI's git, 2.55, peels it fully."
  9. Delete the bullet "The head's last commit is empty: …". The head is now a real commit.
  10. The merges bullet becomes "Merges: clean with main at 3864b18c, which carries family.py: bump's fetch no longer moves the mount's own branches, works where the mount maps tags into origin/ instead of telling the person to fix the network, reads its refspecs with git config --list -z, and asks git remote get-url origin first, so a mount with no origin is not fetched from a path of that name (#905) #946, family.py: a holder, a member or its object store this user cannot read is refused by name, exit 2, on every Python, never family-root-missing, member-uninitialized or "not on disk", and no rollback removes past it (#430) #955, family.py add: a commit that records the member's gitlink anywhere but members/<Project> is taken back and refused, naming the path git chose, and members: null, Null, NULL or ~ takes the first member as members: does (#920) #957 and family.py init: where the holder's own YAML reader refuses the manifest it would write, family-manifest-invalid names the value on the line it refuses, --org is '@x', and gives the reader's remedy, not the validator's (#929) #960, and with lane 1's fix/927-bump-family-root at 3df537a4. Both reachability files pass on each merged tree."

The title, the "Closes #982" line, the test-comment bullet and the review bullet are still true.

Could not verify

  • git 2.44 and later, locally. Only 2.43 is installed, so this rests on the earlier upstream evidence.
  • That upload-pack always sends the ^{} line. This is my reading of git, as noted under the M6 ruling, not a measurement.

Scratch is at /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r988/: the new mutator is tools/mutate2.py, the logs are logs/f-*.log and logs/merged-a1-*.log, and the probe results are probes/fn2.json (head) and probes/fn3.json (under M6).

Lane: openreposhape-1 (openRepoShape-1)

@sonarqubecloud

Copy link
Copy Markdown

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

sourcery-ai Bot commented Oct 19, 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 1 day and 20 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 46cf924 into main Oct 19, 2026
4 of 5 checks passed
@brettheap
brettheap deleted the fix/982-tag-objects branch October 19, 2026 22:04
brettheap added a commit that referenced this pull request Oct 23, 2026
…nt of the commit `--to` names, so a tag above it reads as one that reaches it and a tag below it keeps the plain words, and `--points-at` for `--contains` no longer survives (#1019) (#1107)

No test held that a tag at a DESCENDANT of the refused commit counts as a tag that reaches it, so `--points-at` in place of `--contains` survived in `bump-leg.py` and `family.py` (mutant M9 of #988's review, filed as #1019). This adds the rows the issue asks for to the two reachability test files, and no production code changes. Opened as a draft with no milestone: Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0") held it until the v1.2.0 tag, and it is now on the v1.2.1 milestone.

Measured on main `2a119f28` before any change, and checked again on `16f9a0e81` and `7e00834c` (the tag questions did not change, only their line numbers):

- `scripts/bump-leg.py` `_remote_tag_reaches` and `scripts/family.py` `_origin_tag_reaches` list the tags with `git for-each-ref --contains <commit> --format=%(objectname) refs/tags/`. No other copy of either exists (the `--contains` in `on_a_remote_branch` and `_on_a_remote_branch` asks about branches and is not this question).
- The mutant (`--contains` -> `--points-at` on that one line in each script) SURVIVES every row on main: `test_bump_leg_reachability_words.py` 34 passed, `test_family_bump_reachability_words.py` 20 passed, 54 rows, as the issue says.
- The production code is right: the new rows pass on unmutated main. So they do not "fail on main"; they fail on the mutant, which is the issue's own acceptance ("With `--points-at`, that row must fail"). No defect was found.

`tests/test_bump_leg_reachability_words.py`:
- `made_in_the_leg(project, name, on="HEAD")` takes an optional `on`, the commit the new commit is made on top of. Every existing call is unchanged.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: a commit `made` and a child `above` on top of it, on no branch of the leg's remote. The leg and the remote each tag `above` (`v1-w1019`), made apart, in three kinds: both annotated (the reviewer's case J), leg lightweight and remote annotated, and the reverse. Both runs (dry run and real) refuse `made` `bump-leg-commit-local-only` in the TAG words, through `origin` and through `--local-remote-dir` (3 kinds x 2 routes = 6 rows), with nothing moved and the leg's own tag as it was. The premises say the tag names the child, that no tag is AT `made` (`for-each-ref --points-at`), and, through #1087's `only_a_tag_reaches`, that a tag of the remote reaches it, no branch does and no remote-tracking ref of the leg does.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (x 2 routes). The commit named is the child, and the leg and the remote tag only its parent. The plain words stay, "a pin the rest of the world cannot fetch" included.

`tests/test_family_bump_reachability_words.py`:
- `made_on(holder, env, parent, name)`: a commit never pushed, made in the mount on top of `parent`, through `commit_in_mount` (which checks `parent` out first), so the holder stays clean.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: the same three kinds (3 rows), both `bump --dry-run` and the real `bump` refuse `member-to-local-only` in the tag words, with nothing moved and the mount's own tag as it was.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (1 row).

No assert joins conditions with `and` or `or`; each premise is its own assert. No README, docs, AGENTS.md or handbook text names these tests or their row counts (grep), so none changes.

| Mutant | Rows before (main's) | With these rows |
| --- | --- | --- |
| `--points-at` in `bump-leg.py` | survives: 34 of 34 pass | killed: 6 failed, 36 passed (all six descendant rows; the two controls pass, as they should) |
| `--points-at` in `family.py` | survives: 20 of 20 pass | killed: 3 failed, 21 passed (all three descendant rows) |
| `--merged` in `bump-leg.py` (extra: a check read the other way round) | survives: 34 of 34 pass | killed: 8 failed (6 descendant rows and both controls) |
| `--merged` in `family.py` (extra) | survives: 20 of 20 pass | killed: 4 failed (3 descendant rows and the control) |

The failure is the one the issue names: the plain words ("is reachable from no branch of ...") where the tag words ("and a tag of ... reaches it, but no branch ... does") are expected. Logs: `r-mut-points-at-bump.log`, `r-mut-points-at-family.log`, `r-mut-merged-bump.log`, `r-mut-merged-family.log` (and `mut-main-*.log` for the 34 and 20 on `2a119f28`).

On the pushed head `01fcb259`:
- git 2.43.0 (`/usr/bin/git`): the two test files, 66 passed (42 + 24; 34 + 20 before).
- git 2.43.0: the trio `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` and `tests/test_update_shape_kept_helper.py`, 329 passed.

On `1201680f` (the same two files on top of `16f9a0e8`, before `7e00834c`, whose three files no row of mine touches or runs):
- git 2.43.0: the trio alone, 170 passed; the two test files that import helpers from mine (`test_bump_leg_fetch_refspecs.py`, `test_family_bump_fetch_refspecs.py`), 102 passed; the two test files, 42 and 24 passed.
- git 2.47.1 (`$Q/v115/w764/git247/bin/git` first on PATH, `git --version` printed `git version 2.47.1`): the two test files, 66 passed.

No script changed, so no other test runs a changed script.

- `git merge-tree --write-tree HEAD origin/main` against a re-fetched `origin/main` (`7e00834c`, which is also the merge base): exit 0, tree `3a852e87fcfda41766ece8c1771e47689c622ad6`, equal to this head's tree. Against `16f9a0e8` before the last rebase: exit 0 too.
- Open PRs read with `gh pr list --repo opensoft/openRepoShape --state open --json number,headRefName,isDraft,files`: 8 (#1102, #1101, #1098, #1097, #1091, #1090, #1088, #1075). None touches `tests/test_bump_leg_reachability_words.py` or `tests/test_family_bump_reachability_words.py`, so there is no merge-tree to run against any of them.

- **The NIT.** The docstrings of the two descendant rows said "Every row before this one tagged the commit itself, so ... survived all 54 of them". 54 counts the two files together (34 + 20), several earlier rows tag nothing (the no-`origin` and prune rows), and a count goes stale with the next row.
- **The fix, docstrings only.** Each now says only what holds: "No row before this one tags a descendant of the commit: the tags in them are at the commit itself or below it, so `--points-at` in place of `--contains`, the mutant M9 of #988's review, survived every row of this file." No row, assert or helper changed. The 54 in "What still held on main" above is the two files together, as it says.
- **One commit on top of `01fcb259`, a fast-forward push, no rebase:** head `ee48b5fe9693ebda099d1c0aadaca7bdc16b91c1`, read back with `git ls-remote`. Its tree is `615e571bb62fd1cf481e4152278b0e3628bab059`.
- **Runs** (git 2.43.0, through `run9.sh`, `EMAIL=service@opensoft.dev`): `tests/test_bump_leg_reachability_words.py` 42 passed, `tests/test_family_bump_reachability_words.py` 24 passed. They ran on `7186f3cf`, the same tree as the pushed head; only the commit message was shortened after.
- **Merge-tree** against a re-fetched `origin/main` (`6da757cb`, merge base `7e00834c`): exit 0, tree `453566df33c886cf7fc2b1c39af02acbbff365dc`. Main moved by nine files, none of mine; its `scripts/bump-leg.py` change (9 lines added) leaves `_remote_tag_reaches` and its `--contains` line alone (now line 1214).

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1107: READY at `ee48b5fe`, the head that lands (issuecomment-6318124100), with no MUST or SHOULD and one optional test NIT that needs no round. CI (Brett Heap's ruling 27): H0 is the head ee48b5f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 7e00834; and the local gate passed on its predicted squash tree 2f7f587 over main 422a590: the bump_leg_reachability_words, family_bump_reachability_words, repo_hygiene, agents_md_codes, cli_reference sets, 237 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 #1019

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