Repository navigation
family.py and shape_materialize.py: the NEXT push line names the branch by its full refname on both sides, so a branch beginning with + or named like a tag pushes as itself, and a tracking branch whose last part is over 250 bytes, which git cannot lock, is refused by refuse_bad_branch before anything exists, --dry-run too (#689) - #813
Conversation
… shape_materialize.py: the NEXT push line names the branch by its full refname on both sides, so a branch beginning with `+` or named like a tag pushes as itself, and a tracking branch whose last part is over 250 bytes, which git cannot lock, is refused by `refuse_bad_branch` before anything exists, `--dry-run` too (#689) THE PUSH LINE. `_print_next` ended `add`, `bump` and `remove` with `git -C <holder> push -u origin <branch>`, the branch bare. A bare name is a refspec git reads for itself, and a leading `+` in one means FORCE: a holder on `+x`, with an older local `x` and somebody's commit on origin's `x`, was told a line that pushed the LOCAL `x` over origin's (`+ 5f19342...647e6d4 x -> x (forced update)`), exit 0, and never pushed the commit the act made; with no local `x` it failed `src refspec x does not match any`. A branch that shares its name with a tag (`v1`) failed `src refspec v1 matches more than one`, exit 1 (R421 O1, O4). It is now `refs/heads/<branch>:refs/heads/<branch>`, the spelling `init`'s own push uses (#421), written out on both sides and quoted whole by `shell_line`. Measured: it pushes the named branch whatever is checked out, and `-u` still sets `branch.<branch>.merge` and the remote. THE LENGTH. `git check-ref-format` has no length limit and `checked_value` allows 255 characters, so a tracking branch of 251 to 255 passed `--dry-run` and the real run died at the first commit with every remote made, `fatal: cannot lock ref 'HEAD': ... File name too long` (R421 O2). Git locks a branch by making `refs/heads/<last part>.lock`, and every other part is a directory, so `_branch_verdict` now asks `_branch_length_verdict` of a name git accepted: a last part over `NAME_MAX - len(".lock")` = 250 bytes, or any other part over `NAME_MAX` = 255, is refused with its own `Remediation:` (`BRANCH_LENGTH_REMEDIATION`), because the remedy for a name git judged sends the person to `git check-ref-format`, which accepts it. One helper, so `scaffold-project.py`, `family.py init` and `adopt-project.py` (`plan`, `check`, `execute`) all refuse it before anything exists. DECISION. The issue's direction says a part over 250; git measured here makes `<255 a's>/x` and refuses `x/<251 a's>`, so the 250 limit holds for the last part only, and the rows say so: 250 held, 251 refused, `<255>/x` held. TESTS. A new file holds each case, runs the printed line against a bare repository of the test's own, and holds the old bare spelling to the failure measured. The tests that read the line's last word move with it (`test_family_tracking_branch.py`, `test_family_switch_branch_exists.py`, `test_family_add_refusals.py`, and the sample line in `test_family_printed_quoting.py`); `test_tracking_branch_ref.py`'s corpus, which holds the helper to git's own verdict, swaps its 255-character name for 250, since the length is the one rule git does not state. Not touched: the same bare push in `bump-leg.py` and `update-shape.py` (#458's lines). Closes #689 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… destination in full (`HEAD:refs/heads/<branch>`), where the WIP commit said it spells the line the same way (#689) The NEXT line is `refs/heads/<branch>:refs/heads/<branch>`, the source in full as well, so it pushes the branch whatever is checked out when it is pasted. `init`'s push (#421, #643) is `HEAD:refs/heads/<branch>`: its destination is in full and its source is HEAD, which is the branch in a clone `init` has just made. The docstring's "the same way" was not true of the source. Docstring only; the test run on the merged bytes (3767 passed) ran the code as it stands, and the new file, the three tracking-branch and quoting files and the hygiene, CLI and AGENTS.md code tests were run again on these bytes. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR hardens family-generated push commands by using explicit heads refspecs, and adds early, byte-accurate validation for tracking branch names that Git accepts but the filesystem cannot lock, with shared remediation and comprehensive regression coverage. Sequence diagram for safe family branch publishingsequenceDiagram
participant Family as family.py
participant Shell as User shell
participant Git as Git remote
Family->>Shell: print git push -u origin refs/heads/<branch>:refs/heads/<branch>
Shell->>Git: push explicit heads refspec
Git-->>Shell: update the same branch
Shell-->>Family: upstream records refs/heads/<branch>
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:33:02Z) Lane 1's independent review of #813 at b5a7a7f: CHANGES NEEDED The code is right, and lane 4's ruling on the limit stands. What has to change is text: one false sentence in the PR body, and a remedy that every refused person reads, which says something untrue about how git stores a branch. There is also a fix the PR makes without saying so or testing it (adopt's The issue's lines
The limit: lane 4's ruling is UPHELD, and the case for it is stronger than the body saysMeasured on ext4 with git 2.43.0 (
The two readings differ in a case that is reachable with ASCII. Both tools complete with that name end to end, on main and on the merge: The issue's reading (M5) would refuse these working names. Do not take M5. Other disks:
Findings, ranked1. MUST: the PR body's reachability sentences are false. The repro is the
2. MUST:
Replace the constant's text with: The asserts 3. SHOULD: an unmentioned, untested fix to adopt's For
4. NICE: no tool-level row for the case the ruling rests on. Add a real scaffold or Correctness checks that passed
Tests and Sonar
TitleThe title is true and needs no change. A title that also names adopt's split branch would be fuller, but that is optional. For the checklist (outside #689)
Lane: openreposhape-1 (openRepoShape-1) |
…(each part before the last a directory, the last the file) and that git fails when it first writes it, and the split branch and the 253/x name are held by rows (#689) Lane 1's review of #813 at b5a7a7f, fix round 1. `BRANCH_LENGTH_REMEDIATION`, which every person refused a long name reads, said the last part is a directory as well as a file (it never is: for `x/<251 a's>`, `x` is the directory and the last part the file), said "the part the name above says" (the line above says it), and said "the first commit is where git fails", which is not so for adopt's split branch: on main `execute` failed in the extraction, after both leg repositories existed. The text is the review's, word for word. `_branch_length_verdict`'s docstring said the same untrue thing of the last part and is corrected alike. Rows added to tests/test_next_refspec_branch_length.py: - `adopt_branch:` set to 251 bytes and to `x/` + 251: `check` finds `plan-bad-adopt-branch` once, and `execute` refuses it before any leg exists (the legs' directory is never made). On main `check` said `plan ok` and `execute` made both leg repositories and died; the PR fixed this without a row holding it. - the 250 rows for `--dry-run` and a real run now also hold `<253 a's>/x` (255 characters, a first part over 250 that `checked_value` admits), which both tools complete with: the case the 255 for a part before the last rests on, which M5 (the issue's 250 throughout) would refuse. - the remedy's asserts also pin what it says of how git stores a name. Text only otherwise: no behaviour of the helper changes. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T13:13:20Z) Lane 1's re-verification of #813 at 15c3b5a: PR-READY Everything the review asked for is now in, and the new text is true. I re-ran it all on a local merge of
New since the first review
Lane: openreposhape-1 (openRepoShape-1) |
|
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 7 hours and 2 minutes by commenting |
|
main gained #748 (#662, `431c8cf8`), #731 (#692, `d66a9054`), #810 (#674, `d17564d9`), #809 (#688, `42aed788`), #816 (refs #684, `fa04d2cd`), #813 (#689, `f6d17bc5`) and #723 (#663, `bad05d21`) since 64ec47c. No conflict, and none of them touches README.md, the handbook, AGENTS.md, docs/ or the README cap's ledger, 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>
#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>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #689 (comment)
What changed and why
Three lines of #459's family list (R421 O1, O4 and O2), two changes:
scripts/family.py(_print_next) andscripts/shape_materialize.py(_branch_verdict, one new helper behind it). No pinned copy, template, digest or recorded blob changes, no--helptext, no AGENTS.md, README.md or handbook line: neither file is a pinned copy, and nothing the docs say is made false.The push line (R421 O1 and O4).
family.py add,bumpandremoveeach ended withgit -C <holder> push -u origin <branch>, the branch bare. A bare name is a refspec git reads for itself:+xwas toldpush -u origin +x, a FORCED refspec for the LOCAL branchx. With an older localxand somebody's commit on origin'sx, pasting it said+ ... x -> x (forced update): origin'sxwas overwritten and the commit the act made was never pushed. With no localx:src refspec x does not match any.v1, with a tagv1):error: src refspec v1 matches more than one, exit 1.The line is now
git -C <holder> push -u origin refs/heads/<branch>:refs/heads/<branch>, both sides in full (init's own push names its destination in full,HEAD:refs/heads/<branch>, #421, #643), so the line pushes that branch whatever is checked out when it is pasted. The refspec is ONE word, quoted whole byshell_line(#486), so a branch holding an apostrophe, a$or a;still reaches git as the name it is, and-ustill recordsbranch.<branch>.merge = refs/heads/<branch>andbranch.<branch>.remote = origin. Theupdate-shape.pyandbump-leg.pyhalves of the same bare push are #458's (on this headscripts/bump-leg.py:1732andupdate-shape.py:4658,:4689,:5294), and are not touched here.The length (R421 O2).
git check-ref-formathas no length limit andchecked_valueallows 255 characters, so a tracking branch of 251 to 255 passed--dry-runand the real run died at the first commit with every remote made (fatal: cannot lock ref 'HEAD': Unable to create '.../refs/heads/aaa....lock': File name too long)._branch_verdictnow ends with_branch_length_verdict, which refuses a name git accepted when a part is longer than the disk holds.refuse_bad_branchis the one helperscaffold-project.py,family.py initandadopt-project.py(planandcheck) ask, so all three refuse before anything exists, and--dry-runrefuses alike:The remedy is its own text (
BRANCH_LENGTH_REMEDIATION):BRANCH_REMEDIATIONwould send the person togit check-ref-format --branch, which accepts every such name.adopt-project.py'scheckalready prints a remedy that is notBRANCH_REMEDIATIONas it stands, so a plan edited to a long name is told the same words, once, underplan-bad-tracking-branch.adopt-project.py's split branch (adopt_branch:) is asked the same helper, socheckfindsplan-bad-adopt-branchfor a part over its limit andexecuterefuses it before any leg exists; on mainchecksaidplan okandexecutemade both leg repositories and died.Where I differ from the issue (it says 250 throughout; measured on current main, this disk: ext4/overlay,
git 2.43.0). The.lockfile is made for the LAST part only, so the limits are two:git branchcannot lock refx/+ 250 a'sx/+ 251 a'scannot lock ref/x(255 characters, whatchecked_valueadmits)/x/xcannot lock refRefusing every part over 250, as the issue says, would refuse
<253 a's>/x(255 characters, insidechecked_value), a name git creates and the scaffold andfamily.py initcomplete with. The count is BYTES (os.fsencode), the wayrefuse_unstorable_namescounts them, so 126é(252 bytes) is refused and 125 (250) is held. No tool hands the helper such a name: each runschecked_value, whose alphabet is ASCII, first, so the byte count matters only to a caller that skips it. The issue's own rows (250 accepted, 251 refused) hold, for a name of one part and for the last part of a longer one; the one case where the two readings differ is a NON-last part of 251 to 255 bytes, and through the tools it is ASCII and real:checked_valueadmits only[A-Za-z0-9._/@+~-], up to 255 characters, so a first part of 251 to 253 (<253 a's>/x, 255 characters) reaches the helper, andscaffold-project.pyandfamily.py initmake it end to end; the issue's reading would refuse it. A reviewer who wants 250 for every part changes one line (limit = NAME_MAX - (len(BRANCH_LOCK_SUFFIX) if last else 0)) and the rows namedfirst-part-255,middle-part-251,first-part-256and253-then-xin the new test file.Commits. Four: the WIP commit
fa513848as it was pushed (a ruleset blocks a rewrite, so this pull request's squash subject carries the final words), the merge of mainbf1f099b(clean, no conflict, none of its 29 commits touches_print_nextor_branch_verdict), a docstring correction (b5a7a7f1), and fix round 1 after lane 1's review (BRANCH_LENGTH_REMEDIATION's text, the adopt and tool-level rows, and this body's reachability sentences).Tests
New file
tests/test_next_refspec_branch_length.py, 51 tests:add,bumpandremove, in each of the issue's three states (+xover an older localx,+xwith no localx, a branch named like a tag): the line is typed as printed against a bare repository of the test's own, lands the branch at the act's commit, leaves somebody'sxalone, sets the upstream, and the holder'svalidate-family.pypasses. THE CONTROL: the old bare spelling is run against a copy of the same remote and fails or forces exactly as the issue measured, so the row cannot pass on a git that reads a bare name another way;+x,it's,a$b;c);git branchon this disk;scaffold-project.pyandfamily.py initwith 251 and 255,--dry-runand real, exit 2, the tool's own code, nothing made (no folder, no remote, no holder); 250, and<253 a's>/x(255 characters, a first part over 250 thatchecked_valueadmits: the row the ruling rests on), pass--dry-runin both and a real run with either makes every remote and pushes the branch;adopt-project.pyplanandcheckoftracking_branch:, andcheckandexecuteofadopt_branch:set to 251 bytes and tox/+ 251 (plan-bad-adopt-branchonce, in the helper's words, refused before any leg exists, the directory the legs go in never made). The remedy's rows pin what it says of how git stores a name (each part before the last a directory, the last the file, locked as<last part>.lock, git failing when it first writes the branch).Moved with the line, in five existing files:
tests/test_family_tracking_branch.py(arefspechelper, three assertions),tests/test_family_switch_branch_exists.py(the twopush[-1] == branch),tests/test_family_add_refusals.py(theNEXTconstant),tests/test_family_printed_quoting.py(its sample line) andtests/test_tracking_branch_ref.py(its corpus holds the names the disk holds:"a" * 255is"a" * 250).Gate, on the MERGED bytes (head
421e6cb2, mainbf1f099bmerged in), one background run, noGIT_ALLOW_PROTOCOL, frvenv first on PATH,GIT_CONFIG_NOSYSTEM=1, test identities: 56 files, 3767 passed, 22 skipped, 1 xfailed in 43:51, exit 0. The 56 files (the new file; the existing files that runfamily.py's verbs,scaffold-project.py,adopt-project.py's branch checks andvalidate-family.py; the three gatestest_repo_hygiene.py,test_cli_reference.pyandtest_agents_md_codes.py; and four files main added or changed after this work began that runfamily.pyor the scaffold:test_derived_id_refusals.py,test_branch_left_bump_leg_setup.py,test_naming_policy.py,test_free_plan_hint_reads.py):the 56 file names
Skips: 16 at
tests/test_family_init_values.py:219and 3 at:291(a template that quotes the placeholder orid:, each held by another test), 3 attests/test_shape_doctor.py:10429(the PowerShell line is printed only on a Windows host). The xfail istests/test_shape_doctor.py:4154, strict, until #735.The last commit (
b5a7a7f1) changes one docstring in_print_next(whatinit's own push spells) and no code. On those bytes, a second, narrow run: the new file,test_family_tracking_branch.py,test_family_printed_quoting.py,test_repo_hygiene.py,test_cli_reference.pyandtest_agents_md_codes.py: 293 passed, exit 0.Fix round 1 (
15c3b5ab, after lane 1's review): one background run on those bytes, noGIT_ALLOW_PROTOCOL: the new file (51 tests now),test_family_tracking_branch.py,test_tracking_branch_ref.py,test_adopt_branch_names.py,test_adopt_tracking_branch.py,test_scaffold_leg_path_collisions.py,test_shape_doctor.py,test_repo_hygiene.py,test_cli_reference.pyandtest_agents_md_codes.py: 946 passed, 3 skipped, 1 xfailed in 14:22, exit 0 (the skips aretest_shape_doctor.py:10429, the xfail:4154).adopt-project.pyitself is unchanged; the two adopt gates are there because the new file imports adopt's fixtures and holds itsadopt_branch:path.Mutants, each applied to the fix in a clone, the new file run, the file restored (sha256 checked against the committed bytes). M2 and M5 were run again on the final bytes (
15c3b5ab, 51 tests); M1, M3 and M4 ran on the earlier 46-test file, and the hunks they revert are unchanged since (git diff bf1f099b HEAD -- scripts/shows the helper, its constants and_print_nextonly):_print_nextprints the bare branch again_branch_length_verdict(re-run on15c3b5ab).lockallowance (last part limited to 255, not 250)15c3b5ab)All five are killed. M2's twenty now include the two
adopt_branch:rows (on mainchecksaidplan okfor a split branch of 251). M4 is killed by one row,126-two-byte-characters(252 bytes, 126 characters), so the byte count is held. M5 is the decision above: it fails the held rowsfirst-part-255andmiddle-part-251against the helper and againstgit branch, the words offirst-part-256(may be at most 255), and the three tool-level rows with<253 a's>/x(--dry-run, and a realscaffold-project.pyandfamily.py initrun); a reviewer who prefers the issue's reading makes its one-line change and moves those rows.What it cannot tell
os.name == "nt"(a Windows path of that length is refused whatever the tool says); the others make no name of that length on disk.NAME_MAX = 255(shape_materialize.NAME_MAX, the constantrefuse_unstorable_namesalready counts with), measured on ext4/overlay. A disk with a shorter file name limit (eCryptfs keeps 143) or a path limit that the holder's own location decides (Windows' 260 characters with long paths off) refuses names this helper holds. Neither is asked here, and neither is what the issue measured.update-shape.py apply --branchhas its ownchecked_branch(BRANCH_RE, up to 255, no length rule) and dies late in git's words (cannot lock ref ... File name too long), then rolls back clean (the same class as R421 O2; Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458). On a git withinit.defaultRefFormat=reftable(2.45 and later) git creates a name this helper refuses, so "not a branch name git will create" is false there; refusing is still right, since a clone using the files backend cannot hold it (not measured: this host has git 2.43). A branch part namedCON,NULorauxpasses git and the helper and fails on Windows. A branch holding"breaks under Windows PowerShell 5.1's native argument passing, and csh and tcsh expand!even inside single quotes; neither is new here.scripts/bump-leg.py:1732andupdate-shape.py:4658,:4689,:5294(numbers on this head) is Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458's (U674 has the tag case).Shared files
git merge-tree --write-treeof this head15c3b5abagainst main and each open pull request that touchesscripts/family.py,scripts/shape_materialize.pyor one of the test files above, at 11:57Z on 2026-10-17, each head read withls-remotefirst. Main is20004887, seven commits past thebf1f099bthis branch merged (one of them,1eeef3b6, is infamily.py:add's refusal of a member's own gitlink); it merges clean, so this PR needs no refresh merge yet.20004887fix/708-v110-regressions0bc29c945ascripts/shape_materialize.pyfix/684-family-holder-states772105aa57scripts/family.pyfix/766-spelled-paths0e516bba34scripts/family.py,scripts/shape_materialize.py,tests/test_family_tracking_branch.pyfix/693-local-remote-dir372196a7dcscripts/family.pyfix/688-family-init-values6958dfbcd3scripts/family.pyfix/686-bump-reachability7d7a69b143scripts/family.pyl3-785-non-ascii-pathsa1e2c9a894scripts/shape_materialize.pyfix/685-spellings-remediesac76467948scripts/family.pyfix/681-validate-family-remediesff7c59b8f1tests/test_family_switch_branch_exists.pyfix/692-raw-folder-pathsab55c29f52scripts/family.py,scripts/shape_materialize.py#670 landed at 11:38Z and is in main. #814 spells the same refspec in
bump-leg.py's NEXT line (#458) and asserts that line only; none of the other nine adds a line asserting thepush -u originline or quoting the remedy text, so none is made wrong by this PR's new spelling or text. The hunks here are_print_next(family.py) and_branch_verdict, its new helper and two constants (shape_materialize.py).Closes #689
🤖 Generated with Claude Code
Summary by Sourcery
Make generated family push commands unambiguous and refuse branch names that Git cannot materialize on disk before execution begins.
Bug Fixes:
+or colliding with tag names by printing fully qualified branch refspecs.Enhancements:
Tests: