Repository navigation
family.py: bump fetches every branch of the member's origin whatever the mount's refspec and drops the origin/ refs no ref of origin backs, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with no origin it has none instead of to fix the network (#686) - #795
Conversation
…tes): family.py: #686 bump reachability and the pruned fetch (#686) `bump`'s fetch is now `git fetch -q --prune --no-prune-tags origin +refs/heads/*:refs/remotes/origin/*`. That fetches every branch whatever the mount's refspec (R613 p07), and drops a remote-tracking ref whose branch the remote has deleted, on Brett Heap's word relayed by lane 5 (#473, 13:56:50Z): "prune the fetch the tool already makes" (#459 line 105). A commit only a tag of `origin` reaches is refused saying so, without "a pin the rest of the world cannot fetch" (p03). A mount with no `origin` is told it has none, in the note, in `member-to-local-only` and in `member-to-unknown`, instead of being told to fix the network (p10). Test state: the targeted set passed, 638 tests, over the bytes BEFORE the prune change. Over these bytes only tests/test_family_bump_reachability_words.py has run (10 passed). The full targeted set is still to be re-run, and the final commit message and PR are still to be written. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain merge of main (29 commits since this branch's base 4ce9045), clean: git reported no conflict and none was resolved by hand. Main's changes to scripts/family.py are all above line 2500 (the imports, the member URL and name helpers, the landing and identity helpers, the holder worktree, `init`); none is in `cmd_bump`, `_on_a_remote_branch`, `_has_origin`, `_origin_tag_reaches` or `_checked_on_a_remote_branch`, so no hunk of #686 moved. The targeted set passes over the merged bytes. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideUpdates Sequence diagram for bump reachability validationsequenceDiagram
participant User
participant Bump
participant Git
participant Origin
User->>Bump: cmd_bump(--to)
Bump->>Git: fetch --prune --no-prune-tags origin +refs/heads/*:refs/remotes/origin/*
Git->>Origin: fetch all branches
Origin-->>Git: branch refs and deletions
Git-->>Bump: fetch result
Bump->>Git: _on_a_remote_branch(member, commit)
alt branch reaches commit
Git-->>Bump: reachable
Bump-->>User: pin commit
else no branch reaches commit
Bump->>Git: _origin_tag_reaches(member, commit)
Git->>Origin: ls-remote --tags --refs origin
Origin-->>Git: remote tag refs
Git-->>Bump: tag reaches or does not reach
Bump-->>User: refuse member-to-local-only
end
Flow diagram for missing-origin bump refusalsflowchart TD
A["bump --to commit"] --> B["Fetch origin branches"]
B -->|fetch succeeds| C["Check origin remote-tracking branches"]
B -->|fetch fails| D["_has_origin(member)"]
D -->|origin exists| E["Use stale remote-tracking refs"]
D -->|origin missing| F["Refuse: branch reachability is unknown"]
F --> G["Give the mount back a remote named origin"]
C -->|branch reaches commit| H["Pin commit"]
C -->|no branch reaches commit| I["Refuse member-to-local-only"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-17T11:09:02Z) Lane 1's independent review of #795 at 444934d: CHANGES NEEDED The head contains current main
The issue's lines (measured)I ran the head's new test file against main's
F1 (should fix, and decide before v1.1.0): the prune drops refs that no branch backs, including ones the person's own refspec keeps
Repro, plain git, in an empty directory: export GIT_CONFIG_GLOBAL=/dev/null GIT_ALLOW_PROTOCOL=file # plus an author/committer identity
git init -q -b main s && git -C s commit -q --allow-empty -m one
git -C s commit -q --allow-empty -m review-me && git -C s branch -f review HEAD && git -C s reset -q --hard HEAD~1
git init -q -b main --bare R.git && git -C s push -q ../R.git main review:refs/pull/7/head
for run in main head; do
git clone -q "file://$PWD/R.git" M-$run && cd M-$run
git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch -q origin && git fetch -q origin refs/pull/7/head:refs/remotes/origin/kept-by-hand
if [ $run = main ]; then git fetch -q origin
else git fetch -q --prune --no-prune-tags origin '+refs/heads/*:refs/remotes/origin/*'; fi
echo "$run: exit $?; $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/ | tr '\n' ' ')"
cd ..
doneI also ran it end to end through
So a dry run deletes them too. This may be within Brett Heap's word. As lane 5 relayed it, the word is "prune the fetch the tool already makes; only remote-tracking refs are dropped", and these are remote-tracking refs. But the fetch that word was about was main's plain fetch, which kept them. Either way the texts are false at this head:
The explicit refspec never writes Options (lane 4's call, or Brett's):
F2 (should fix before close): the
|
| case | main | head |
|---|---|---|
| ordinary pushed commit | git fetch -q origin |
git fetch -q --prune --no-prune-tags origin '+refs/heads/*:refs/remotes/origin/*' |
| local-only commit, no tag | one fetch | one fetch |
| local-only commit that a mount-only tag contains | one fetch | one fetch and git ls-remote --tags --refs origin |
The one new call happens only in that refusal path, and the body says so. The refusal words in that case are the plain #613 words, byte for byte, as intended.
Byte for byte against main
Hashes and temp paths were masked; the words were compared exactly.
- Identical:
- an ordinary pushed commit (dry run exit 0, 949 bytes of stdout; real run exit 0, 250 bytes; empty stderr in both);
- an ordinary bump while another branch had been deleted on the remote (the same, with
-qkeeping the prune silent); - the plain local-only refusal.
- Only refs differ: in the deleted-branch case, the head also removed
refs/remotes/origin/pushed-<sha12>, which is the intended prune.
Tests
- I reverted two rows myself, on the two touched files (18 tests). Both match the writer's table:
- M6 (no
--prune): 1 failed (test_a_branch_deleted_on_the_remote_since_the_last_fetch_no_longer_counts). - M4 (
_has_originalways True): 4 failed (both p10 cells, the unknown id, the unparsable config).
- M6 (no
- The asserts are honest: full two-line refusals, nothing moved (
both_runs), the pin andvalidate-family.pyon the way out. - S1192, S3776 and S5778: nothing new. Sonar reports the two S9073 above and nothing else.
Runs (local merge = 444934d; git-filter-repo on PATH; no GIT_ALLOW_PROTOCOL)
- 181 passed, exit 0:
test_family_bump_reachability_words.py,_bump_to_local_only,_bump_to_a_tag,_bump_to_checks,_dry_run,test_refusal_default_remediation.py,test_cli_reference.py,test_agents_md_codes.py. - 153 passed:
test_family_bump_remove_rollback.py,test_non_utf8_git_reads.py,test_family_refuse_up_front.py.
Windows
Nothing to flag. There is no os.name path and no text-mode byte count. The config append is write_bytes, and the branch names (pushed-<sha12>, release) are legal on NTFS. tests-git-bash is green at the head.
Exact text replacements
- PR body, the bullet that starts "
--pruneremoves only what the refspec writes". Under option (a), replace it with:--pruneremoves every ref underrefs/remotes/origin/that names no branchoriginhas now, whoever wrote it, in the dry run too: a branch deleted on the remote, and also a ref the mount's own other refspecs keep there (+refs/pull/*/head:refs/remotes/origin/pr/*, say) or one fetched there by hand. Nothing outsiderefs/remotes/origin/is removed: the mount's own branches and tags stay, andorigin/HEADstays, dangling where it named a deleted branch. The test setsremote.origin.pruneTags=trueon the mount and checks them.
scripts/family.py:5949-5951: replace "--prunedrops only refs the refspec writes, underrefs/remotes/origin/;" with:--prunedrops every ref underrefs/remotes/origin/that names no branch oforigin, one another refspec of the mount or a hand fetch wrote there included;- "What it cannot tell": add this bullet (option a):
- Refs under
origin/that no branch backs. Both runs drop them: pull or merge-request refs a mount's own refspec keeps there come back on the person's next plaingit fetch; a ref fetched there by hand does not.
- Refs under
- Title: replace "and prunes the branches
originhas deleted" with:and drops every
origin/ref that no branch oforiginbacks - M7 row, optional: after "git 2.43 prunes no tag in an explicit-refspec fetch even with
pruneTags=true", add:; even
--prune-tagson the command line prunes none there, so M7 is an equivalent mutant
For the checklist (outside the issue)
--refmap=. A+refs/heads/*:refs/heads/*or mirror refspec in a mount makesbump's fetch force-overwrite local branches through git's opportunistic update, as main's did.--refmap=closes it (measured above). The same applies tobump-leg.py's fetch.- Lightweight against annotated tags.
_origin_tag_reachescompares unpeeled ids. A mount-made lightweight tag at the same commit as the remote's annotated tag of another name gets the plain words, as does a remote tag the mount lacks over a commit it holds. Comparing peeled commits withmerge-base --is-ancestorwould cover both. - A second prompt. Over HTTPS with no credential helper, the new
ls-remoteprompts for credentials again after the fetch has prompted once. This happens only in the tag refusal path. - An
originwith a refspec but no URL. Git treats the name itself as the URL (get-urlprintsorigin, exit 0), so_has_originsays yes, the fetch fails, and the words are the network ones. (Read from git'sremote_get, not probed.) - A path named
origin(already in the body): askingget-urlbefore the fetch would close it.
Lane: openreposhape-1 (openRepoShape-1)
…n under `origin/` and honours the exclusions they state, and the two S9073 asserts are split (#686) Lane 1's review of 444934d (issuecomment-6233185697), F1, F3 and F4. F1: `--prune` with the explicit `+refs/heads/*:refs/remotes/origin/*` deleted every ref under `refs/remotes/origin/` no branch of `origin` backs, the pull request refs a mount's own refspec keeps there (`+refs/pull/*/head: refs/remotes/origin/pr/*`) included, in a dry run too, where main's plain fetch kept them. The coordinator's ruling is the review's option (b): `_origin_fetch_refspecs` puts each configured positive `remote.origin.fetch` refspec whose destination is under `refs/remotes/origin/` on the command line after the branch refspec, forced, and prunes it only against itself. One that writes elsewhere (a mirror's `+refs/*:refs/*`) is never added. A configured negative refspec is passed as it stands: measured on git 2.43, git applies it only to a fetch with no refspec of its own, so `bump` fetched a branch the person had excluded. A ref put under `origin/` by hand, which no refspec maps, is still dropped. The refspecs are read with `git config --list`, the one form `test_a_dry_run_runs_no_git_command_that_writes` accepts in a dry run. F4: the code comment and the texts said `origin/HEAD` stays; it stays, dangling where it named a deleted branch (git's own prune), and no script reads it. F3: the two composite asserts SonarCloud lists (S9073) are split. Tests: the deleted-branch test gives the mount a pull request refspec and asserts `origin/pr/7` stays while `pr/8` (deleted on the remote) and a hand-made ref go, and that a `+refs/notes/*:refs/notes/*` refspec of the mount writes nothing; a new test pins the negative refspec. Three mutants of the new logic (no configured refspecs, negatives not passed, a refspec outside `origin/` added) are each killed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T13:15:45Z) Lane 1's re-verification of #795 at 7d7a69b: CHANGES NEEDED Option (b) works.
Neither needs much code. N1 needs one line, or texts only; F2 needs two issue comments. I tested a local merge of
N1: passing negative refspecs re-creates a p07-style untrue refusal, and makes the title falseThe setup is the PR's own new test,
So this is not a regression against main, but it is the very state #686 exists to correct. It is also inconsistent with p07: a positive narrowing (
F2 (before #686 closes, no code)Before ticking #459's three lines, post these on #458, or ask lane 5 to:
Post the For the checklist (new, contrived, not blocking)
Lane: openreposhape-1 (openRepoShape-1) |
…ranch the mount excludes counts like one a narrowed refspec leaves out and a narrowing refspec never decides reachability (#686) Lane 1's re-verification of 7d7a69b (issuecomment-6234322825), N1, and the coordinator's ruling, the review's option (a). N1: the fix round passed the mount's negative refspecs to the fetch, because git applies the configured ones only to a fetch with no refspec of its own. A commit pushed only to a branch the mount excludes (`^refs/heads/<branch>`) was therefore refused `member-to-local-only` with "reachable from no branch of <repository>", words untrue of a branch the remote has, in the very state #686 exists to correct, and it made the PR title false. It was also inconsistent with R613 p07: a positive narrowing (`+refs/heads/main:refs/remotes/origin/main`) is overridden by the first refspec and the commit pinned, while a negative one was honoured and refused. `_origin_fetch_refspecs` now skips every configured refspec that starts with `^`: one `continue` and a comment. Git ignores a configured negative refspec on a command line that names refspecs (measured, git 2.43), so skipping is enough. The docstring says so, and says what the skipped paragraph was for. It also says what lane 1 measured beside it, that a mount whose only refspec maps tags into `origin/` and a remote with a branch and a tag of one name fail this fetch with 128 where `git fetch origin` exits 0, left for #459's checklist. Test: `test_a_branch_the_mounts_own_refspec_excludes_is_not_fetched`, which expected the refusal, is now `test_a_branch_the_mounts_own_negative_refspec_excludes_still_counts` and expects the pin: the dry run prints the row, exit 0, the real run pins it, and the mount's exclusion is left as the person set it. A mutant that passes the negatives again fails it (the dry run exits 2). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T15:55:53Z) Lane 1's re-verification of #795 round 2 at cf5e64b: PR-READY Every earlier finding is resolved. Your Sonar claim holds, and nothing new blocks. I tested in a fresh clone, on a local merge of Earlier findings
Lane 4's Sonar claim (issuecomment-6235297147): confirmedThe PR has 2 open issues, both S3776:
New findingsNone that block.
Lane: openreposhape-1 (openRepoShape-1) |
…der check, and the unparsable-config state is kept as a row of its own (#686) #816 (fa04d2c) gave `bump` its `family-manifest-uncommitted` check, whose `git ls-files -m -z` in the holder reads each gitlinked mount's repository. A mount config git cannot parse (an unclosed `[remote "origin"` header) is now refused there, as git's failure of that command, before the fetch, so `test_a_mount_config_git_cannot_read_is_gits_own_failure` no longer reached `git remote get-url origin` on the merged tree (1 failed, 417 passed on 9ee0435). The test keeps its subject on a state that reaches the question: a `url` with no value under `[remote "origin"]`. Measured on git 2.43: the holder's `git ls-files -m -z` and `git status` exit 0, the mount's `git config --list` exits 0, and `git remote get-url origin` and the fetch both exit 128, "error: missing value for 'remote.origin.url'" and "fatal: bad config variable 'remote.origin.url' in file '<config>' at line 14". Both runs now print `git remote get-url origin`, exit 128 and git's output exactly, exit 2, with nothing moved (`both_runs`). The unclosed header is kept as `test_a_mount_config_git_cannot_parse_is_gits_own_failure_where_met`: git's own failure, exit 128 quoted, exit 2, dry and real alike, "bad config line", nothing about `origin`, the network or a credential, and the holder's gitlink, index, family.yaml, work tree and objects unmoved, without pinning which command fails first. Mutants on this tree, against this file and test_family_bump_to_local_only.py (20 tests): M5, `_has_origin` False on any failure, is killed (1 failed: the `url` row, member HEAD changed); M4, `_has_origin` always True, is killed (4 failed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#813 and #723, v1.0.0's quoted-header and plain-path-clone limits closed and the counts current to main bad05d2 (#563) Each entry is the one RV630 corrected in its pre-review of the B2 stage, put at its anchor, and checked against its squash on main: the `git merge-tree --write-tree` of main before the squash with the head the entry was written from equals the squash's tree for all seven. - #810 (#674, `d17564d9`): a new bullet, "`apply`'s lines, its branch and its headers.", after "Headers, branches and messages.". It closes v1.0.0's quoted `files:`/`shape:` header (v1.0.0.md:439) and `apply`'s half of the paths listed as staged (:433); `bump-leg.py`'s half waits for #746. - #731 (#692, `d66a9054`): appended to "Folders, asked before they are made.". - #813 (#689, `f6d17bc5`): appended to "`--tracking-branch`.". - #809 (#688, `42aed788`): a new bullet, "What `family.py init` records and commits as.", after "Names that are not UTF-8.". - #816 (part of #684, `fa04d2cd`): a new bullet, "More holder states, named.", after "family.py refuses before it writes.". #816 closes no issue; #684 keeps its reftable case and is on v1.1.1. - #748 (#662, `431c8cf8`), then #723 (#663, `bad05d21`): in the CI bullet, after #713's sentence. With #554 and #578, #748 closes v1.0.0's limit on fixtures that clone a just-pushed remote by plain path (:530). The counts, in the header comment, the counts paragraph, "What changed" and the open list: main `bad05d2`; 120 pull requests since `bfed143`; at 18:02Z, 3 of the `v1.1.0` milestone's 133 issues open and 130 closed. The open list is #563 (#630), #672 (#746) and #686 (#795); #684 and #695 (#738) are on v1.1.1. #746 and #795 have not landed, so neither has an entry yet. Nothing since 2000488 touches pyproject.toml, src/openreposhape/, install.sh or .github/workflows/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it closed as one clause, the list's and the known limits' markers made final, the triage lines named by their issues, and the counts current to main fc86720 (#563) - #746 (#672, `fc867207`): a new bullet, "`bump-leg.py`'s branch and its lines.", after "`bump-leg.py --dry-run` gives the real run's answer.". `git merge-tree --write-tree bad05d2 a5139a3`, the head #746 was gated and landed on, equals the squash's tree `bb920ac1`. The head the entry was written from, `4a014a79`, sat on #723's head before its squash and does not merge clean with main; `scripts/bump-leg.py` is one blob (`226e7b0d`) at `4a014a79`, `a5d473ee`, `a5139a32` and the squash, and the squash's own lines in each of its five files equal that head's, so the entry stands as written. - v1.0.0's limit on the paths listed as staged (v1.0.0.md:433) is one clause, in #746's entry: with #810 it closes. #810's half-clause is cut. `bump-leg.py --dry-run` over a pin its validators refuse (:440) stays a limit, which the dry run now says. - "What changed" runs to main `fc86720`, and says #795 is added if it lands next; Known limits' re-read marker and "as read so far" are cut. The counts marker stays as the release-body tool reads it. - Known limits name each triage by its issue: "the checklist triage of the L4 lines on #457 and #459" and "of the L1 lines on #457", where they named lanes 4 and 1 and D9, and each id reads "#457's L4-05", "#459's L4-24" and so on. - Counts in the four places: main `fc86720`; 121 pull requests since `bfed143`; at 19:08Z, 131 of the `v1.1.0` milestone's 133 issues closed, the open two #563 (#630) and #686 (#795). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-17T19:24:28Z) To lane openRepoShape-1: please re-verify #795's merge round 3, head
Lane: openreposhape-4 (openRepoShape-4) |
bump fetches every branch of the member's origin whatever the mount's refspec and prunes the branches origin has deleted, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with no origin it has none instead of to fix the network (#686)bump fetches every branch of the member's origin whatever the mount's refspec and drops the origin/ refs no ref of origin backs, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with no origin it has none instead of to fix the network (#686)
|
(lane openRepoShape-1, 2026-10-17T19:40:06Z) Lane 1's re-verification of #795 merge round 3 at 17bbbab: PR-READY The code is ready. One condition remains: CI's Linux I tested a local merge of 1. The merge is correct
2. Round-2 verdict still holds
3. Why the earlier gate failed, and that it passes nowCause. I ran round 2's So #816 ( The rework is honest. The
It does not pin which command fails first. Neither row has a composite assert. Runs on the merge, one mode, git-filter-repo on PATH, no
Every set exited 0: 1635 passed in all. 4. CI at
|
|
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 37 minutes by commenting |
|
main gained #795 (#686, `6f64eea2`) since fc86720. No conflict, and it touches no README.md, handbook, AGENTS.md, docs/ or README cap ledger file, so every handbook marker reads as before. README.md is 1474 lines, its cap; AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f64eea, RV630's lead and two named limits under Known limits, and the counts current to main 6f64eea (#563) - #795 (#686, `6f64eea2`): a new bullet, "`bump --to`, reachable or not.", before "family.py refuses before it writes.", the pending file's text as written. `git merge-tree --write-tree fc86720 <head>` equals the squash's tree `0f2d2ddd` for `17bbbaba` and for the final head `cf143c69`. For the head the text was written from, `cf5e64b3`, the merged tree differs from the squash only in `tests/test_family_bump_reachability_words.py`, so `scripts/family.py` is as the text says. - "What changed" runs to main `6f64eea`, with no word of a later pull request. - Known limits, RV630's words: the lead says the list is not all of v1.0.0's open limits, and names two this release worked on and left open, the `--into` sentence (#695) and `bump-leg.py --dry-run` over a pin its validators refuse (#746, #672). - Counts in the four places: main `6f64eea`; 122 pull requests since `bfed143`; at 20:15Z, 132 of the `v1.1.0` milestone's 133 issues closed, the one open #563 (#630). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd moves no branch of the leg's own, a commit only a tag of the remote reaches is told so instead of "never pushed", and a leg with no `origin` is told it has none instead of to fix the network (#898) (#937) `scripts/bump-leg.py`'s fetch now prunes the branches the leg's remote deleted and moves no branch of the leg's own, and its reachability refusals say what is true of a commit only a tag of the remote reaches and of a leg with no `origin` (#898, the twins of the three cases #795 fixed in `family.py`): 1. **The fetch** is `git fetch -q --prune --no-prune-tags --refmap= <remote> <refspecs>`. `_leg_fetch_refspecs` mirrors `family.py`'s `_origin_fetch_refspecs`: every branch, then each positive refspec of the leg's own `remote.origin.fetch` that writes under `refs/remotes/origin/`, forced. A branch deleted on the remote no longer counts, where a dry run used to pin a commit no branch reached any more. `--refmap=` stops the configured refspecs from moving anything else: with `+refs/heads/*:refs/heads/*` configured, a dry run used to force-move the leg's local `main` off an unpushed commit, or, with the leg on `main`, fail the fetch and answer from stale refs. Tags are still followed. 2. **A commit only a tag of the remote reaches** (`_remote_tag_reaches`, asked only after a fetch that worked) is refused `bump-leg-commit-local-only` in words that say a tag reaches it and no branch does, not "never pushed". 3. **A leg with no `origin`** (`_has_origin`, asked only when the fetch of `origin` fails, and never under `--local-remote-dir`) is told its mount has no remote named `origin`, and to give it back, not to fix the network or the credential. The refusals are built in a new `_unreachable_refusal`. No code is added or renamed, so `docs/cli.md`, `AGENTS.md` and the exit codes are unchanged. Because the fetch now carries the leg's own refspecs, two fetch failures are new: exit 128 where a refspec names a ref the remote no longer has (the person's own `git fetch origin` fails too), and on #905's U183 clash (`Cannot fetch both ...`, where `git fetch origin` exits 0). The note follows, nothing is pruned, `_leg_fetch_refspecs`'s docstring says so, and #905 holds both for both tools (#905 issuecomment-6253411781). The tag check compares tag objects, not the commits they point to, as `family.py`'s does: a lightweight `v1` in the leg against an annotated `v1` on the remote at the same commit gets the plain words. That is a follow-up for both tools. Review: lane 1's independent reviewer, not the writer, found `cea5210a` READY, its three new tests each failing under the mutant it was written for (#937 issuecomment-6253416253). Its first pass held one SHOULD, a record of the two fetch failures above, which the fix round makes. Tests, at `cea5210a` under a stub `gh` with a bogus token: `tests/test_bump_leg_reachability_words.py` has 19 cases. Every `tests/test_bump_leg*.py` gives 254 passed and 1 skipped (it needs a case-insensitive disk), and `tests/test_family_bump_reachability_words.py`, `test_repo_hygiene.py`, `test_cli_reference.py` and `test_agents_md_codes.py` give 160 passed. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `cea5210a` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving 6ccc682, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 12 test files that run `scripts/bump-leg.py`, `scripts/family.py`'s matching reachability words, and the docs and codes checks; 392 passed and 1 skipped. Closes #898 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #686 (comment)
What changed and why
family.py bump --torefuses a commit no branch of the member'soriginreaches (#613, D2 option 1). The rule stays. This PR makes the refusal's reason true of the mount in front of the person in the three states R613 found (#459, R613 p07, p03 and p10), and makes the fetch it reads from drop theorigin/refs nothing on the remote backs, a deleted branch first among them (#459 line 105, below). Every hunk is insidebump's fetch, its reachability check and its two refusals,member-to-local-onlyandmember-to-unknown. No flag,--helpline or doc sentence changes.Each case was re-measured on main
4ce90457before the change and on this head after it. Every probe ran on holders built withfamily.py initandaddinto local bare repositories.p07: a fetch refspec narrowed by hand.
bumpfetched with a plaingit fetch -q origin, which obeys the mount's ownremote.origin.fetch.remote.origin.fetch=+refs/heads/main:refs/remotes/origin/main, and a commit X that the remote'srefs/heads/featholds and that was fetched by hand, the refusal wasREFUSED member-to-local-only: --to X is in the member's own object store, in members/IRRS, but is reachable from no branch of InkRouter/IRRS, exit 2.originwith a URL and no refspec at all is the same state. That is whatgit submodule syncleaves on a detached mount, measured.cmd_bumpfetches+refs/heads/*:refs/remotes/origin/*by an explicit refspec, asbump-leg.pyfetches a leg. Thefetched = Falsepath is unchanged.validate-family.pypasses. The mount's configured refspec is left as the person set it.#459 line 105 (issuecomment-6203972266), on Brett Heap's word relayed by lane 5 (#473, 13:56:50Z), issuecomment-6217084172: "prune the fetch the tool already makes". The same fetch now carries
--prune --no-prune-tags, and adds no fetch of its own. Head444934d9pruned too much, as lane 1's review (issuecomment-6233185697, F1) measured, and head7d7a69b1passed the mount's negative refspecs, which lane 1's re-verification (issuecomment-6234322825, N1) measured to be wrong; what follows is the corrected rule. Lane 4 took the line onto this PR (#473, issuecomment-6217136405). It was not one of the issue's three lines: the issue left it out as needing a ruling, and the word is that ruling.bumppinned the commit, exit 0, with no ref of the remote keeping it.member-to-local-only, with nothing moved.--pruneremoves every ref underrefs/remotes/origin/that no refspec of the fetch maps a ref oforiginto, in the dry run too: a branch deleted on the remote, and a ref put there by hand. Nothing outsiderefs/remotes/origin/is removed: the mount's own branches and tags stay, andorigin/HEADstays, dangling where it named a deleted branch (git's own--prunedoes that, and no script readsorigin/HEAD). The test setsremote.origin.pruneTags=trueon the mount and checks them.origin/: GitHub's and GitLab's documented+refs/pull/*/head:refs/remotes/origin/pr/*(andmerge-requests/*). Main's plain fetch kept them, pruning only against its own refspecs, and the dry run deleted them too (measured by the review: after a dry runpr/7was gone). Now_origin_fetch_refspecsputs on the command line, after+refs/heads/*:refs/remotes/origin/*, each configured positiveremote.origin.fetchrefspec whose destination is underrefs/remotes/origin/. Each is forced with+, as the first is, so a remote-tracking ref that moved non-fast-forward fails nothing. A pull request the remote still has stays, and one it deleted goes, pruned against that refspec alone. Beyond the branches the first refspec is there for, that asksoriginfor nothing main's plain fetch did not.refs/remotes/origin/. A configured refspec whose destination is elsewhere (a mirror's+refs/*:refs/*,+refs/heads/*:refs/heads/*) is not added, so no refspec this fetch names writes the mount's own branches or tags. The test gives the mount+refs/notes/*:refs/notes/*and a notes ref on the remote, and the mount gets none._origin_fetch_refspecsleaves out every configured refspec that starts with^, as p07 has a positive one that leaves a branch out overridden by the first refspec. Measured on git 2.43: a configured^refs/heads/skipis applied bygit fetch origin, and ignored by a fetch that names refspecs of its own (skipcame back asorigin/skip), so skipping is enough. Head7d7a69b1passed the negatives on the command line to honour them, and a commit pushed only to an excluded branch was refusedmember-to-local-onlywith "reachable from no branch of InkRouter/IRRS", words untrue of a branch the remote has, in the very state family.pybump: a mount with a narrowed fetch refspec is refusedmember-to-local-onlyon a false detail, a commit only a tag holds is told "a pin the rest of the world cannot fetch", a mount with nooriginis told to fix the network #686 exists to correct. Now both runs find it on that branch, the dry run prints the row, exit 0, and the real run pins it. The mount's exclusion is left as the person set it, and a branch both excluded and deleted is pruned like any other (measured, under "What it cannot tell").git fetch origin. Measured: a non-glob refspec naming a ref the remote lacks, and two refspecs mapping to one destination, both exit 128 on main's plain fetch and here, sobumpsays its fetch failed, as it says any. One clashgit fetch originnever meets, a configured refspec that maps tags intoorigin/againstbump's own, is under "What it cannot tell". With noremote.origin.fetchat all (anoriginwith a URL and nothing else, p07) there is nothing to add. The refspecs are read withgit config --list, the one form ofgit configthattest_a_dry_run_runs_no_git_command_that_writesaccepts in a dry run, and which exits 0 for no refspec at all.--no-prune-tagsis there, and where it is only insurance (mutant M7 survives). Measured: main's PLAINgit fetch origin, withremote.origin.pruneandremote.origin.pruneTagsset true, deleted the mount's tags, a tag only the mount held among them. The explicit refspec keeps them, so p07's fix already closes that. Measured on git 2.43: an explicit-refspec fetch prunes no tag even withpruneTags=true, so removing--no-prune-tagsfails no test; even--prune-tagstyped on the command line prunes none there (measured:explicit --prune-tags + refspec: exit 0, tags: only-here, where the same fetch without the refspec deletes the tag), so M7 is an equivalent mutant, and no test can kill it on this git. The flag stays so that a later git that readspruneTagsinto an explicit-refspec fetch cannot delete a tag the person made. It costs nothing.p03: a commit only a tag of the remote reaches, which the mount holds too.
v2alone, andgit fetch origin tag v2in the mount.--to Yand--to <v2's id>both said "A pin the rest of the world cannot fetch is a family the rest of the world cannot bootstrap." Yet a freshgit cloneof the remote holds Y, and a fetch by sha exits 0.REFUSED member-to-local-only: --to Y is in the member's own object store, in members/IRRS, and a tag of InkRouter/IRRS reaches it, but no branch of InkRouter/IRRS doesRemediation: push it to a branch of InkRouter/IRRS and run the same command again:bumppins only a commit a branch of the member's remote reaches, and a tag is not a branch.--to <tag id> is an annotated tag, and the commit it names, Y,, as before._origin_tag_reaches):for-each-ref --contains <c> refs/tags/).git ls-remote --tags --refs originthen says which of those tagsoriginhas, compared by object id.bump --to: pins a commit only the member's mount holds, whichbump-leg.pyrefuses as local-only #613's words, which are true there. So does a listing that fails.p10: a mount with no remote named
origin.git remote rename origin upstreaminmembers/IRRS, a commit onupstream/featgotnote: could not fetch members/IRRS from its origin. Thenmember-to-local-only … reachable from no branch of InkRouter/IRRSfollowed, endingfix the network or the credential and run it again before believing it.member-to-unknown … make InkRouter/IRRS readable from here — if it is PRIVATE, checkgh auth statusor your SSH key._has_originasksgit remote get-url origin. Its exit 2 means "No such remote" and nothing else. Any other failure is git's, quoted, exit 2. The words with noorigin:note: could not fetch members/IRRS: it has no remote namedorigin; the commit must already be in its object store and on a branch of its remote as last fetchedREFUSED member-to-local-only: --to X is in the member's own object store, in members/IRRS, but members/IRRS has no remote namedorigin, so whether a branch of InkRouter/IRRS reaches it is not knownRemediation: give members/IRRS back a remote namedorigin, for InkRouter/IRRS, and run the same command again:bumpfetches the member's branches fromoriginand asks no other remote. If the commit is not pushed yet, push it to a branch of InkRouter/IRRS first.REFUSED member-to-unknown: --to <id> is not a commit members/IRRS holds, and members/IRRS has no remote namedoriginto fetch it from, so whether a branch of InkRouter/IRRS reaches it is not knownRemediation: push it first, to a branch of InkRouter/IRRS, if it is not pushed yet; give members/IRRS back a remote namedorigin, for InkRouter/IRRS, and run the same command again:bumpfetches the member's branches fromoriginand asks no other remote. A pin the rest of the world cannot fetch is a family the rest of the world cannot bootstrap.upstreamback tooriginlets the same dry run through, exit 0.originwhose fetch fails gets the same note and words as before, byte for byte.origingit cannot read now prints git's own failure,git remote get-url origin, exit 128, then exit 2. Main4ce90457told a mount config git cannot parse to "checkgh auth statusor your SSH key".urlthat has no value under[remote "origin"](a lineurlwith no=): the holder'sgit ls-files -m -zand the mount'sgit config --listexit 0; the fetch andgit remote get-url originboth exit 128,error: missing value for 'remote.origin.url'andfatal: bad config variable 'remote.origin.url' in file '<mount's config>' at line 14. Both runs quotegit remote get-url origin, exit 128 and that output, then exit 2.add,bumpandremovename a bisect, an empty rebase directory, sparse rules that hold no pattern, a merge conflicted infamily.yamland amembersfile HEAD holds and the work tree does not as what each is, with the way out that works there, and refuse afamily.yamlholding a change HEAD does not (#684) #816 (fa04d2cd), a config git cannot parse at all (an unclosed[remote "origin"header) is refused earlier:family-manifest-uncommitted'sgit ls-files -m -zin the holder reads the mount's repository and exits 128,fatal: bad config line 14 in file <mount's config>, so that command is the one quoted. It is still git's own failure, exit 2, in both runs alike.Decisions.
originnames no command.git submodule sync -- members/<P>was measured as a candidate and rejected. On a detached mount it writesremote.origin.urlonly, which this PR's fetch can use. On a mount standing on a branch whose remote isupstream, it rewritesupstream's URL and makes noorigin. Agit remote renameorremote addfits only one of the two states.member-to-local-onlyin the no-origincase, as it already does offline.bump-leg.py's twins are untouched. They are "somebody committed inside the leg's mount" for a tag-held commit, the stale-refs sentence for a leg with noorigin, and a fetch (+refs/heads/*:refs/remotes/origin/*) that does not prune. The issue names the first two as Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458's, and the third is the same file's. All three are Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458's file to change, and lane 4 has posted them there as one checklist line (Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458 issuecomment-6234361009).--helptext changes anddocs/cli.mdis not regenerated.Tests
New file
tests/test_family_bump_reachability_words.py, 12 tests. Each runs bothbump --dry-runandbump, asserting the exit code and the full two refusal lines, with nothing in the holder moved (both_runs), or the pin andvalidate-family.pywhere the run should pass.main-only refspec, and no refspec. The dry run passes, the real run pins, and the configured refspec is unchanged.member-to-local-only. The mount's own branch, its tag andorigin/HEADsurvive in a mount set to prune tags. The mount's own PR refspec keepsorigin/pr/7(which the remote still has),origin/pr/8(which it deleted) goes with the branch, and so does a ref put underorigin/by hand. A+refs/notes/*:refs/notes/*refspec of the mount writes no notes ref.^refs/heads/<branch>, the branch the commit was pushed to, and both runs still fetch it. The dry run prints the row, exit 0, the real run pins it, and the exclusion is left as the person set it. Round 1'stest_a_branch_the_mounts_own_refspec_excludes_is_not_fetchedexpected the refusal; it is nowtest_a_branch_the_mounts_own_negative_refspec_excludes_still_countsand expects the pin.--toas the commit, and as its annotated tag's own id. The way out, a push to a branch, then pins.v-sameon a commit only it holds, and the remote's ownv-sameat the pin. family.pybump --to: pins a commit only the member's mount holds, whichbump-leg.pyrefuses as local-only #613's words are kept, and the mount's tag is not clobbered.originrenamed, and removed. Each takes the way out (rename back, orremote add) and then pins.member-to-unknown: an id the mount lacks, with noorigin.originhas aurlwith no value (test_a_mount_config_git_cannot_read_is_gits_own_failure): both runs quotegit remote get-url origin, exit 128 and git's output byte for byte, exit 2, with nothing moved (both_runs). Until round 3 this test used an unclosed[remote "origin"header, which family.py:add,bumpandremovename a bisect, an empty rebase directory, sparse rules that hold no pattern, a merge conflicted infamily.yamland amembersfile HEAD holds and the work tree does not as what each is, with the way out that works there, and refuse afamily.yamlholding a change HEAD does not (#684) #816's holder check now meets first.test_a_mount_config_git_cannot_parse_is_gits_own_failure_where_met): git's own failure,THE EXACT COMMAND WAS:, exit 128 andbad config linequoted, exit 2, the dry run and the real run alike, nothing aboutorigin, the network or a credential, and the holder's gitlink, index,family.yaml, work tree and objects unmoved. It does not pin which command fails first.Lane 1's F3, two S9073 composite asserts at lines 284 and 373, are split into separate asserts.
tests/test_family_bump_to_local_only.py:local_onlygainstagged=.test_a_pin_add_at_made_from_a_tag_alone_is_refused_until_pushedis exactly the p03 state: the remote'sv-annotatedreaches the release commit, and the mount's clone brought it. So its one assertion moves to the new words.Targeted run, over the round-2 head
cf5e64b3: 409 passed, 0 skipped, exit 0, in 321 s, from one run oftests/test_family_bump_reachability_words.py,test_family_bump_to_local_only.py,test_family_dry_run.py,test_family.py,test_family_refuse_up_front.py,tests/test_repo_hygiene.py,tests/test_cli_reference.pyandtests/test_agents_md_codes.py, withgit-filter-repoon the path, noGIT_ALLOW_PROTOCOL, and theghguard oftests/conftest.py. Round 2 changes one function and one test, so the wider set of 18 files that passed 640 over7d7a69b1was not run again. CI's full Linux suite is the rest.Round 3, main
bad05d21merged (c03d3dfd), then17bbbaba. Lane 4's landing gate on the predicted squash tree9ee04358failed one test, 1 failed and 417 passed:test_a_mount_config_git_cannot_read_is_gits_own_failureexpectedgit remote get-url originand gotgit ls-files -m -z, from #816's check above. The merge is clean and its tree is9ee04358.17bbbabachanges that test file alone, as under Tests;scripts/family.pyis as merged. Gate over17bbbaba, one run of everytests/test_family*.py(28 files),tests/test_repo_hygiene.py,tests/test_cli_reference.pyandtests/test_agents_md_codes.py, with noGIT_ALLOW_PROTOCOL: 1283 passed, 19 skipped, exit 0, in 1114 s. The 19 skips aretest_family_init_values.py's, which skip by the template's quoting (16 at line 219, 3 at line 291).Mutants, each reverting one part, run against the two files above (19 tests; 20 in round 3):
git fetch -q origin(no refspec, no prune)if False:)add --attag test)ls-remote_has_originalways Trueurlwith no value)_has_originFalse on any failureurlwith no value: read as noorigin, the commit is found on the mount's ownorigin/ref from the push and the real run pins it,member HEAD changed)--prunemember HEAD changed)--no-prune-tagspruneTags=true. The flag is insurance, as above.origin/pr/7); the negative-refspec test no longer fails it, since no negative refspec is passedrefs/remotes/origin/is added toorefs/notes/reviewis written)7d7a69b1's logic)M9, round 1's "a configured negative refspec is not passed", is the code now, so it has no row. M2 and M3 ran on the code before the prune change, over lines no later change touches, and M7 on
444934d9. M4 and M5 ran again in round 3, onc03d3dfdwith17bbbaba's test, against the two files (20 tests); the unclosed-header test passes under both, as it should, sincels-filesfails before_has_originis asked. M1, M6, M8, M10 and M11 ran on round 2's code,cf5e64b3, each against the two files above (19 tests).What it cannot tell
origin, the remote cannot be asked, and nothing is pruned. A tag-held commit gets family.pybump --to: pins a commit only the member's mount holds, whichbump-leg.pyrefuses as local-only #613's words with the stale-refs sentence. A commit that only a branch deleted since the last fetch still reaches, in the mount's stale refs, is pinned after the note that the fetch failed, as family.pybump --to: pins a commit only the member's mount holds, whichbump-leg.pyrefuses as local-only #613 decided for an offline bump. The mount's own tags cannot say whose they are.refs/pull/<n>/head, say) is not asked, unless the mount's own refspec fetches it underorigin/, which_on_a_remote_branchthen counts, as it did. Where an unfetched one holds the commit, "cannot fetch" is still said.origin/by hand. No refspec maps it, so both runs drop it, and nothing brings it back.remote.origin.fetchis a mirror's+refs/*:refs/*or+refs/heads/*:refs/heads/*still has its local branches updated by git's opportunistic update when a fetch names refspecs of its own, as main's plain fetch updated them.--refmap=would stop it; it is left for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist, with thebump-leg.pytwin. Measured on git 2.43, with+refs/heads/*:refs/heads/*configured and an unpushed localfeat: main's plain fetch, this fetch andbump --dry-runend to end each movedfeatto the remote'sfeat, and this fetch with--refmap=left it.origin. In a mount with no remoteoriginthat holds a repository at the pathorigin(a leg mounted there, say), git readsoriginas that path, and the fetch succeeds against it.refs/remotes/origin/<its branches>and gives family.pybump --to: pins a commit only the member's mount holds, whichbump-leg.pyrefuses as local-only #613's plain words.get-urlbefore the fetch would close it, at one git call per bump. The issue asks it after a failed fetch, so that is left for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist.bump's own (lane 1's first new checklist item). A mount whose only refspec maps tags intoorigin/(+refs/tags/*:refs/remotes/origin/*), and a remote with a branch and a tag of one name:git fetch originexits 0, and this fetch exits 128,fatal: Cannot fetch both refs/heads/feat and refs/tags/feat to refs/remotes/origin/feat, because the mount's tags refspec rides along withbump's own branch refspec.bumpthen says it could not fetch the member from its origin, and a refusal that follows tells the person to fix the network or the credential. Measured on this head, in a holder built withinitandadd: git's own fetch with the refspecsbumpbuilds exits 128 with that message, andbump --dry-runprintsnote: could not fetch members/IRRS from its origin. The sentence above, "two refspecs mapping to one destination … exit 128 on main's plain fetch and here", does not cover it, because here the clash is withbump's own refspec. Left for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist.git config --listsplits a multi-line value of any key into lines (lane 1's second item)._origin_fetch_refspecsreads the mount's refspecs fromgit config --list. A value with a newline in it, an alias holding a lineremote.origin.fetch=+refs/heads/zz:refs/remotes/origin/zz, comes out as a line of its own, and is read as a refspec the mount does not have: measured,git config --get-all remote.origin.fetchdoes not list it and--listdoes. If its destination is underrefs/remotes/origin/it goes on the command line.git config --list -zsplits exactly, and it has--listin its argv, which is all the dry run's writer check (test_a_dry_run_runs_no_git_command_that_writes) asks. Contrived, and left for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist.7d7a69b1it was one: with^refs/heads/Bconfigured and B deleted on the remote, git never pruned the excludedorigin/B, so it still counted, and a commit only B had held was pinned (measured:origin/Bkept, dry run exit 0). Negative refspecs are skipped now, so the prune treats B as any other branch. Measured on this head with the same state:origin/Bis dropped and the dry run is refusedmember-to-local-only, exit 2, with the plain words, which are true there.Shared files
scripts/family.pyis not a pinned copy. This head is17bbbaba, on the mergec03d3dfdof mainbad05d21, overcf5e64b3,7d7a69b1and444934d9. Of the open PRs in round 2's table below, #731, #809, #813 and #816 have since landed in main and are in this head. #814's headef6db40dwas read again in round 3:git merge-tree --write-treefrom this head mergesscripts/family.pyclean and conflicts intests/test_dry_run_parity.pyandtests/test_non_utf8_git_reads.py, files #795 does not touch, where #814's head conflicts with mainbad05d21too. The other open PRs were not listed again in round 3. Round 2's table, fromcf5e64b3, heads read from GitHub withgh pr list --json headRefOid:merge-tree --write-tree24a977c0fix/692-raw-folder-pathsab55c29ffix/688-family-init-values6958dfbcfix/689-next-push-refspec15c3b5abfix/766-spelled-pathsef6db40dfix/684-family-holder-statesfdc90a6eCloses #686
🤖 Generated with Claude Code
Summary by Sourcery
Make
family.py bumpbase branch reachability checks on a complete, pruned view of the member’s origin and provide accurate remediation messages for tags, stale refs, and missing remotes.New Features:
family.py bumpfetch all branches from a member’soriginwhile preserving relevant configured remote-tracking refspecs.originremote from other fetch and configuration failures.Bug Fixes:
Enhancements:
Tests: