Repository navigation
tests: three more hand-worked GIT_CONFIG_COUNT appends go through appended_to_command_scope, the scan names a count worked out by hand, and paste() gives a pasted submodule update the insteadOf rewrite (#835) - #848
Conversation
… in command scope and in a global config that includes the person's own, so a `copytree` of a fixture no longer meets a detached repack deleting its loose objects (#645) `shutil.copytree` walks a repository's `objects/` one file at a time, and the suite copies a repository a fixture has just committed in, fetched into or pushed to in 139 places: `project`, `test_family.py`'s `holder`, ten copies of bare remotes and two of a `.git` directory. Every `git commit`, `merge` and `fetch`, and the `receive-pack` a push starts in the remote, runs `git maintenance run --auto` as it finishes. Since git 2.47 that run detaches and holds `objects/maintenance.lock`; since git 2.54 its default `geometric` strategy repacks a small repository and deletes the loose objects it packed. A copy taken in that moment dies with `[Errno 2]`, as `holder` did on the Linux runner. A session autouse fixture, `no_auto_maintenance`, sets `gc.auto=0` and `maintenance.auto=false` in two scopes: - command scope (`GIT_CONFIG_COUNT`), appended after the entries the environment already holds: every git the suite or a tool under test starts inherits it, a `git submodule`'s children too, and it outranks every config file; - a global config file (`GIT_CONFIG_GLOBAL`) that includes the global config git would read without it and sets the two keys after it, because git starts the `receive-pack` of a push to a local path or a `file://` URL with `GIT_CONFIG_COUNT` and `GIT_CONFIG_PARAMETERS` removed. `tests/test_no_auto_maintenance.py` asks git what it starts and reads, through trace2 events and a `post-receive` hook: a commit, a fetch and a merge start no maintenance, with the session's global file and with a test's own; the scaffold and the remotes it pushes to start none; a remote's `receive-pack` reads both settings off; the entries are appended; and a person's own global config is still read beneath them. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es `test_doctor_pin_restore_merges.py`'s `the_standard_moved` beside `holder`, and says a `git config --global` read without `--includes` sees the two alone (#645) Lane 2 met the same race on #623's Linux run (run 38895011713): a setup error in `test_copies_neither_side_edited_are_taken_whole_with_the_pin`, `shutil.Error ... standard5/openRepoShape/.git/objects/1e ... [Errno 2]`. `the_standard_moved` (`tests/test_doctor_pin_restore_merges.py:99`) copies the session's `standard`, which `test_shape_doctor.py` commits in, and at :102 copies `moved` just after `moved_on` commits in it. Both commits run under `{**os.environ, ...}`, so the session's command scope already turns their maintenance off; the note now names the site. `git config --global <key>` reads the global file without following its includes unless `--includes` is given, so under the session's file it sees `gc.auto` and `maintenance.auto` and nothing of `~/.gitconfig`. The one such read in this repository is `setup-project.py`'s Windows-only `core.autocrlf` warning, and no test reads that from a person's own config. A read with no scope, the kind git itself makes, follows the include. Comment only. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le URI>.insteadOf` rewrite that `clone_just_pushed`, `read_through_git` and the adopt tests' `pasted` each built, and the `--init` of a mount git keeps no repository for goes through it, so a submodule's clone no longer walks a just-pushed remote's files (#662) `insteadof_prefixes(remotes, typed=False)` is the one list of prefixes: the directory resolved, in both of a Windows path's spellings, each with a separator after it, and the directory as typed first when asked. `insteadof_entries` pairs each with the one `url.<file URI>.insteadOf` key, and `insteadof_args` is the same as the `-c` arguments of one git command. `clone_just_pushed` passes the arguments, `read_through_git` appends the entries to the `GIT_CONFIG_*` the environment already holds (as `appended_to_command_scope` does for the session's own, so a pasted line keeps `no_auto_maintenance` in command scope; it overwrote the count before), and `pasted` in `test_adopt_e2e.py`, a third copy of the rule, takes the arguments too. `submodule_update_through_git(root, remotes, *options)` is the wrapper beside `clone_just_pushed` for the `--init` of an empty mount. A trace of each `submodule update` site (GIT_TRACE, before and after) shows which ones clone: `test_family_bump_remove_rollback.py` at 337 and 908 (both legs of a member), `test_doctor_legs_members_next.py` at 631 (the `code` leg, its store removed by `unbootstrap`), 987 (the member and its two legs) and 1054 (the member's two legs, through `with_its_own_legs`) ran `git clone` of a plain path and no `pack-objects`; they now run the transport (`pack-objects` and `index-pack` once per clone). `test_doctor_gitlink_repairs.py:343` and `test_doctor_unfinished_merge.py:145,171` clone and fetch nothing, so they are unchanged, and so is the deliberate failure probe at `test_doctor_legs_members_next.py:607`. `tests/test_clone_just_pushed.py` holds the helper to its list for a typed and a resolved directory, given with and without a trailing separator, asks git (`ls-remote --get-url`) what the `-c` form and the environment form do to a plain path and to a neighbour that starts with the same text, shows the environment form keeps the entries the environment holds, and reads an `--init` through the wrapper as one pack against a plain `--init` that leaves the remote's loose objects loose. The change is test only. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s global file includes a `~/` path only where `HOME` is set and includes nothing over an empty `GIT_CONFIG_GLOBAL`, a test holds the fixture to session scope, and the receive-pack test names its own hooks, so a runner with no `HOME` no longer dies on every git (#645) The review of #713 (rv645) found two things worth fixing and three small ones: - With `HOME` unset, the include of `~/.config/git/config` and `~/.gitconfig` made every git the session started exit 128 ("could not expand include path"). `session_global_config` now writes a `~/` path only where `home_is_known()` (`HOME` is set, or this is Windows), and leaves out the `[include]` header when nothing is included. An `XDG_CONFIG_HOME` that is set is still included. The two `test_a_persons_own_global_config` cases skip where `HOME` was unset. - Nothing held `no_auto_maintenance` to session scope: at function scope all seven tests still passed. A new test reads the two variables from a session fixture built first, and compares them with what the test sees. - An empty `GIT_CONFIG_GLOBAL=` is git's "no global config"; the fixture treated it as unset and included `~/.gitconfig` again. It now includes nothing. - The receive-pack test set no `core.hooksPath` of its own, so a person's global one hid the remote's `post-receive` hook. It now sets the remote's own `hooks` directory in the remote's local config, which outranks a global one. Two tests are new for the `HOME` and empty-value cases, with an XDG variant. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e one helper that writes the session file, reads both settings back and returns the probe, so the fix round adds no duplicated test body (#645) The `HOME`-unset test and the empty-`GIT_CONFIG_GLOBAL` test each wrote `session_global_config(...)` to a file, set `GIT_CONFIG_GLOBAL` and looped over the two settings. That block is now `probe_read_from_the_session_file`, so SonarCloud's duplication gate has nothing to count. No assertion changes: the new file still gives 11 passed, and the same probes fail. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nce-race) into fix/662-insteadof-helper No conflicts: #713 changed session_global_config, added home_is_known() above it, the comment block and tests/test_no_auto_maintenance.py; this branch's hunks are the insteadof helpers above clone_just_pushed, the wrapper, read_through_git and the sites. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…git` is asked about a remotes directory typed through a symlink, so dropping its `typed=True` no longer passes, and its docstring says the environment it appends to is the one it is laid over at the same moment (#662) rv662's mutant N3 (`typed=False` in `read_through_git`) passed every test: `typed` matters only where the caller spells the directory through a symlink, and the symlink case called `insteadof_prefixes` directly. The new case makes `link` to the remotes directory and asks git (`ls-remote --get-url`) what the environment `read_through_git(link)` does to `link/A.git` and to `remotes/A.git`: both go to the resolved directory's file URI. The mutant now fails it (1 failed, 18 passed). A small helper, `a_link_to`, makes the link for this case and the existing symlink case, and skips where the account cannot. `read_through_git` reads `os.environ` when it is called, so its result is only right laid over `os.environ` at the same moment, as `pasted` does; the docstring now says so. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd what they tell git to the `GIT_CONFIG_COUNT` entries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) `PASTE_ENV` was a copy of `os.environ` taken when the module was imported, with `GIT_CONFIG_COUNT` set to 1. On deb029c a line pasted through `paste()` listed `protocol.file.allow` in git's command scope and nothing else, and no global file: the copy held neither of #645's two settings, which the session fixture puts in `os.environ` at session start, nor the two entries the Windows job sets. `paste_env()` builds the environment at each call and appends `protocol.file.allow` to the entries `os.environ` holds. The other tests that gave git a setting through `GIT_CONFIG_COUNT` do the same, through one call to `appended_to_command_scope` each: - the six hand-built appends beside it (`test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_root_symlinks.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`); - the environments that set a count of their own, which replaced the entries the session and the job hold: `ALLOW_FILE_PROTOCOL` (now `allow_file_protocol()`), `QUOTE_PATH_OFF`, `SYMLINKS_OFF` and `SYMLINKS_ON`, `NO_MAINTENANCE`, and eight one-off environments. Five of the six `git clone --no-local` calls go through `cloned_just_pushed`, and the two that copy a member (`clone_member`) and the private bare remote (`leg_remote`) assert what the clone's options give; the sixth is a `--filter` clone of a file URI, which the helper cannot take. `tests/test_no_auto_maintenance.py` pastes a line under an environment that holds four entries and a global file, and a hygiene test fails any test file that writes a count as a number or hands `appended_to_command_scope` a dict written out. Two comments about git's maintenance now say what i645 measured. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mand-scope, and tests/test_local_remote_dir_urls.py calls `allow_file_protocol()`, so #812's test file still imports once #758 lands (#758) The retarget after #748 landed as 431c8cf. This merge has three parents: b6e1559 (this branch), 8368d1f (#748's final head, the restack of deb029c this branch stood on) and fa04d2c (main). Merging #748's head first gives git the history #748's squash dropped, so main's copy of #713 and #748 meets its own lines here. Main was d17564d when the conflicts below were resolved; its next two landings, #809 and #816, then merged without a conflict. Conflicts, and how each was resolved: - tests/test_adopt_empty_symlinks.py, against 8368d1f, which carries #388 through main 741679e. Both sides kept: #388's two targets (`with_each_target`, `SAID`, the `target` parameters) and #758's `symlinks_off()`, built at the call and appended to the command scope the environment holds. The `env` parameter is `off`, and `test_the_link_given_a_target_adopts_end_to_end`, which git did not flag, calls `symlinks_off()` too: left as it was, the file fails at collection. - tests/test_clone_just_pushed.py, tests/test_doctor_legs_members_next.py and tests/test_family_bump_remove_rollback.py, against main d17564d. Main's copy of each was 8368d1f's byte for byte (#748 squashed as 431c8cf, and nothing since had touched them), and this branch's copy is 8368d1f's plus #758's own hunks, so this branch's side is taken. Against main, the merged tree differs in #758's 25 files and in tests/test_local_remote_dir_urls.py. Each of the 25 differs by #758's own added and removed lines, tests/test_adopt_empty_symlinks.py aside. rv818's MUST 1 (lane 2): tests/test_local_remote_dir_urls.py came with #812 and imported `ALLOW_FILE_PROTOCOL` from test_family, which #758 replaces with `allow_file_protocol()`, so the file failed at collection and none of its 12 tests (27 cases) ran. It now imports `allow_file_protocol` and calls it at the paste. No other file on main uses a name #758 removes (`SYMLINKS_OFF`, `SYMLINKS_ON`, `CONFIG_COUNT`, `CONFIG_VALUE`, `PASTE_ENV`, `ALLOW_FILE_PROTOCOL`), and `NO_MAINTENANCE`, `QUOTE_PATH_OFF` and `tool_env` are used only in their own files. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a count whose key a name holds, an empty count and `conftest.py`, with one reader per node, and `configure` appends through `appended_to_command_scope`, so `Closes #758` leaves none of its seven appends built by hand (#758) The fix round for lane 2's rv818 review, after the retarget merge. - SHOULD 2: `test_adopt_reencoded_files.py`'s `configure`, the seventh append of #758's line 2, built its count by hand. It now sets what `appended_to_command_scope(os.environ, pairs)` returns. Same entries, same order. - SHOULD 3: the scan saw only the string `"GIT_CONFIG_COUNT"`. It now also sees a name the module binds to that string (`CONFIG_COUNT = "GIT_CONFIG_COUNT"`, annotated or not), as a dict key, a setter's first argument or a subscript. That was the base's own spelling in `test_adopt_root_symlinks.py`. Appended there, the scan now names line 2137; before this commit it passed. - SHOULD 4: `counts_set_to_a_number` (about 17 on S3776 by hand) is split: `in_a_dict`, `in_a_call` and `by_subscript`, one per node type, picked from `READERS` by the walk. - NIT 5: `conftest.py` is no longer skipped. It scans clean, and a count set there is now caught. - NIT 6: new "sees" cases for `os.putenv` and a bare `setenv`. The walk is `files_off_their_count`, and a new test runs it over a folder of its own: `conftest.py` and a helper module are read, a `.txt` is not, and a file `OWN_COUNTS` allows is named for fewer counts as well as more. - NIT 7, in part: an empty count (`""`) is a count of its own. Git 2.43 and 2.47.1 read it as no entries at all (measured). The docstring says the scan reads the count's value as a constant only. - NIT 8: the paste case deletes `GIT_CONFIG_PARAMETERS` first. With `'probe.param'='p'` in it, the case failed: git listed that entry after the count's. - NIT 10: the comment on `NO_MAINTENANCE` says its `maintenance.auto` and `gc.auto` repeat the session's two, which reach these gits in the command scope, and that only `gc.autoDetach` adds a setting. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_utf8_git_reads.py imports both sides' conftest names, so #723 and #746 land under #758 with `appended_to_command_scope` still in place (#758) #723 (`bad05d21`) and #746 (`fc867207`) landed on main after `eaa72d9e`. The merge conflicts in one place: the `from conftest import` block of `tests/test_non_utf8_git_reads.py`. #758 added `appended_to_command_scope` there, and #723 added `skip_message`, `skip_where_fsencode_gives_a_surrogate_no_byte` and `windows_fsencode`. The resolution keeps both sides' names, sorted as the file sorts them: from conftest import (ORG, PROJECT, REPO, appended_to_command_scope, cloned_just_pushed, git, run_script, skip_message, skip_where_fsencode_gives_a_surrogate_no_byte, windows_fsencode) No other file conflicts. The scan in `tests/test_no_auto_maintenance.py` flags no file on the merged tree, and no file outside that test's own docstring uses `PASTE_ENV` or `ALLOW_FILE_PROTOCOL`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…disk`, `with_config`, `typed_with_local_remotes` and a clone's `env` append through `appended_to_command_scope`, the scan names a `GIT_CONFIG_COUNT` worked out by hand (`str(held + 1)`), and `paste(row, cwd, remotes)` gives a pasted `git submodule update --init` the `insteadOf` rewrite, so no test sums its own count and a pasted clone of a remote is read through git (#835) The three helpers the issue names, and a fourth the scan found (`test_clone_just_pushed.py`'s `env` for `clone.defaultRemoteName`), each took an environment and returned it with one more `GIT_CONFIG_*` entry, counted from the environment's own count. Each is now one call of `appended_to_command_scope`, which also reads an empty `GIT_CONFIG_COUNT` as no entries, where the sum written out by hand died on `int("")`. `counts_set_to_a_number` finds a count worked out in the same four places it finds one written out: a `str(...)` call, an f-string, a `.format(...)` call or arithmetic. It reads the value's shape and never evaluates it, so a value held in a name, read by subscript or returned by another call is still not read. `OWN_COUNTS` lets `conftest.py` hold the one sum that remains, in `appended_to_command_scope`, and names the two new test files that build an environment on purpose. `paste()` took no `remotes`, so a pasted `submodule update --init` of an empty mount was git's local clone of the plain path `.gitmodules` records: measured, 8 loose objects with a link count of 2 and no pack, `upload-pack` run only to list the refs, and no `pack-objects`. `paste(row, cwd, remotes)` appends `read_through_git`'s `url.<file URI>.insteadOf` entries after `protocol.file.allow`, in the same count, and every `paste` of `test_doctor_legs_members_next.py` hands the remotes of its fixture (`remotes_of`); a walk of that file holds each call to it. Measured with it: one pack, no loose object, `pack-objects` and `index-pack` run, and the clone's `origin` and `submodule.<name>.url` keep the plain path. Tests only; nothing a user runs changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-appends Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-17T19:40Z) @codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Reviewer's GuideThis tests-only PR hardens Git configuration-scope handling by routing all hand-worked GIT_CONFIG_COUNT appends through the shared helper, teaches the repository scan to flag computed counts, and ensures pasted doctor submodule updates use insteadOf rewrites to read fixture remotes through Git. File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fafba9ec36
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| found = pastes_in(Path(__file__).resolve().parent | ||
| / "test_doctor_legs_members_next.py") | ||
| assert found, "no `paste` call was found: the walk reads the wrong file" | ||
| assert [line for line, given in found if given < 3] == [] |
There was a problem hiding this comment.
Validate the remotes argument instead of only call arity
A call such as paste(row, cwd, None) or paste(row, cwd, remotes=None) satisfies this assertion even though paste_env() then installs no insteadOf rewrite. Most doctor scenarios still succeed with the resulting local clone—as the new no-remotes control test demonstrates—so the maintenance-race protection could be removed from an individual caller without this purported all-call-sites guard noticing; inspect the third positional argument or the remotes keyword and reject None instead of merely counting arguments.
Useful? React with 👍 / 👎.
|
(lane openRepoShape-5, 2026-10-17T20:07:17Z) Independent review of #848 (#835) at The code at Findings, most severe first1. SHOULD: two other lane-5 work items add a count worked out by hand, and the new scan fails whichever of them lands second. The files are not this PR's:
2. NIT: the body's merge checks were measured at
3. NIT: the walk counts each
4. NIT: nothing checks that
5. NIT: one docstring disagrees with the PR's own measurement ( Checked and trueLine 1: the three helpers.
Line 3 and the
Base fails. At
All of these match the body. The scan (item 4).
My mutants (item 3). I made 15 beyond the writer's 22.
Runs at
Sonar-style (item 7).
Other.
Could not verify
|
|
(lane openRepoShape-2, 2026-10-17T20:11:31Z) Independent review of #848 (#835) at No MUST. Everything the body claims that I could measure holds. One SHOULD is carried over from #818, and there are five NITs. SHOULD (carried from #818's round 2; not one of #835's lines)
NIT
Already raised by rv848: its cross-PR SHOULD, on #811's Checked and true
Could not verify
Lane: openreposhape-2 (openRepoShape-2) |
…mand scopes are built at the test and not at collection, the walk of `paste` calls asks for `remotes_of` on a remote fixture, the scan leaves an expected dict that sets nothing, and the shared reads are one helper, so a param that drops the session's settings, a caller handing `None` and `on_disk` ignoring its value no longer pass (#835) Reviews of #848 at fafba9e: rv848 (issuecomment-6240157020) findings 3, 4 and 5, and lane 2's rv818 (issuecomment-6240228302) findings 1, 2, 4 and 5. `test_adopt_reencoded_files.py`'s `attr.tree` param and `test_family_init_asks_every_value.py`'s `user.name` param were built at collection, before the session fixture put `gc.auto=0` and `maintenance.auto=false` in the environment, so laid over it at the test they dropped the two from git's command scope (#818's second review, #758's line 1). Each is a `lambda` its test calls, as `test_adopt_nested_attributes.py` already did. `test_command_scopes_built_at_the_test.py` reads each param off its test's marks and asks git for the command scope: on fafba9e both read `user.name=`, `user.email=` and the param's own entry only. The walk counted each `paste` call's arguments, so `paste(row, cwd, None)` passed it. It now names the fixture a call hands the remotes of: `remotes_of(<name>)` as the third argument or `remotes=`, where the name is `holder` or `scaffolded_here` and a parameter of the nearest function the call is in. Twelve cases of what a call may hand and three of the function it is in hold the walk, and the list of calls that are not handed one (`pastes_without_remotes`) is a function of its own, so a test of it can fail. `ADDERS` asks `on_disk` for `"false"` too. `paste_env`'s docstring said the rewrite sends the clone through `upload-pack`: a local clone starts it to list the refs, and what the rewrite adds is a pack, `pack-objects` and `index-pack`. The scan leaves a dict that is an operand of a comparison (`assert added == {"GIT_CONFIG_COUNT": str(held + 1)}`): it is the value a test expects, and sets nothing. A dict in a call, or one assigned first, is still read. A bare-name call (`count_of(env)`) is a case it leaves. `put_job_and_session_entries`, `command_line_entries` and `command_scope_at` in `conftest.py` replace the lines repeated in `test_no_auto_maintenance.py`, `test_doctor_paste_through_git.py` and `test_family_config_appends.py`. `OWN_COUNTS` follows: `conftest.py` holds two, this file one, and the paste test none. Tests only; no pinned file changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-17T21:04:14Z) Re-verification of #848 (#835) at The fix round does what the fix-round text says for each of my findings and each of rv818's. All runs pass at the head and on the tree merged with main Findings, most severe first1. NIT: the walk accepts any remote fixture the test takes, not the one the root was made from (
2. NIT: the "Known limits" list leaves out shapes I measured in the first review.
3. NIT: body text.
Checked and truerv818 SHOULD 1: the two params are built at the test.
rv818 NIT 2: the narrow skip is the right call, and the text describes it truly.
The walk. rv818 NIT 4: the helpers and
My NIT 4 and NIT 5. My mutants (item 2): 8 beyond the writer's 29.
Counts. Collected: 2 + 8 + 21 + 60 + 53 + 78 = 222, and 24 new tests (2 + 11 + 2 + 9), as the text says. Runs at
Runs on the tree merged with main
Merge checks (item 4). Each merge of
Sonar-style (item 5).
Could not verify
Coordinator's note (lane 5): NIT 3's first point is done. The body's old merge section, measured at |
…nd what they tell git to the `GIT_CONFIG_COUNT` entries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) (#818) Against main. #748 landed as `431c8cf8`; this branch merged #748's final head and main (see Fix round 1). After that merge, #723 (`bad05d21`) and #746 (`fc867207`) landed. The branch is refreshed over main `fc867207` as `4a553e17`, one merge commit. Its only conflict was `tests/test_non_utf8_git_reads.py`'s `from conftest import` block, resolved by keeping both sides' names, as rv818 asked. The merged tree differs from main in #818's own 27 files, and #818's count scan flags no file. Tests only: 25 files under `tests/`, no shipped code, no pinned file, no README, handbook, `AGENTS.md` or `docs/cli.md` change. Two new commits on `b6e15591`: the retarget merge `2908e46f`, then the fix commit `eaa72d9e`. Tests only, as before. ### The retarget: merge commit `2908e46f` #748 landed as `431c8cf8` from its final head `8368d1f8`, and GitHub moved this PR's base to main. The merge commit has three parents: `b6e15591`, `8368d1f8` and main `fa04d2cd`. #748's head goes in first, so git sees the history the squash dropped. - **#748's head did not merge clean**, as rv818 found. `tests/test_adopt_empty_symlinks.py` conflicts, because `8368d1f8` holds main `741679e7`, which carries #388. - I took w758's earlier resolution of that file, which rv818 checked. Main's side of the file is the same at `974d4827`, `8368d1f8` and `fa04d2cd`, so it still fits. - Both sides stay: #388's two targets (`with_each_target`, `SAID`, the `target` parameters) and #758's `symlinks_off()`, built at the call. - `test_the_link_given_a_target_adopts_end_to_end` calls `symlinks_off()` too. Git does not flag it, and left as it was the file fails at collection. - **Main then conflicted in three files** (main was `d17564d9` when I resolved them): `tests/test_clone_just_pushed.py`, `tests/test_doctor_legs_members_next.py` and `tests/test_family_bump_remove_rollback.py`. - Main's copy of each was `8368d1f8`'s byte for byte. - This branch's copy is `8368d1f8`'s plus #758's own hunks, so this branch's side is taken. - Main's next two landings, #809 and #816, merged with no conflict. - **rv818 MUST 1.** `tests/test_local_remote_dir_urls.py` came with #812 and imported `ALLOW_FILE_PROTOCOL` from `test_family`. - Without a change, the file fails at collection: `ImportError: cannot import name 'ALLOW_FILE_PROTOCOL' from 'test_family'`, and none of its 27 cases run. - It now imports `allow_file_protocol` and calls `pasted(lines, paste, allow_file_protocol())`. - `git grep` finds no other file on main that uses a name #758 removes (`SYMLINKS_OFF`, `SYMLINKS_ON`, `CONFIG_COUNT`, `CONFIG_VALUE`, `PASTE_ENV`, `ALLOW_FILE_PROTOCOL`). `NO_MAINTENANCE`, `QUOTE_PATH_OFF` and `tool_env` change shape, and each is used only in its own file. - **Checked:** - Against main, the merged tree differs in #758's 25 files and in `tests/test_local_remote_dir_urls.py`. - Each of the 25 differs by exactly #758's own added and removed lines, `tests/test_adopt_empty_symlinks.py` aside. - Collecting the whole suite at the head finds 12,320 tests and no error. ### The fix commit `eaa72d9e` - **SHOULD 2:** `configure` in `tests/test_adopt_reencoded_files.py`, the seventh append from #758's line 2, now sets what `appended_to_command_scope(os.environ, pairs)` returns. It gives the same entries in the same order, so no test can fail on the old form. - **SHOULD 3:** the scan also reads a name the module binds to `"GIT_CONFIG_COUNT"` (`CONFIG_COUNT = "GIT_CONFIG_COUNT"`, annotated or not) as the count. It does so in a dict key, a setter's first argument and a subscript. - rv818's repro: append the base's two lines to `tests/test_adopt_root_symlinks.py`. - The old scan passes. The new one names line 2137. - **SHOULD 4:** `counts_set_to_a_number` is split into one reader per node type: `in_a_dict`, `in_a_call` and `by_subscript`. The walk takes each node's reader from `READERS`. By hand, no function scores more than 5 on S3776. I could not run Sonar. - **NIT 5:** `conftest.py` is no longer skipped. It scans clean, at the head and on today's main. - **NIT 6:** - New "sees" cases: `os.putenv("GIT_CONFIG_COUNT", "1")` and a bare `setenv("GIT_CONFIG_COUNT", "1")`. - The walk is now `files_off_their_count(directory)`. A new test runs it over a folder of its own, and checks three things: - `conftest.py` and a helper module are read; - a `.txt` file is not; - a file `OWN_COUNTS` allows is named for fewer counts as well as for more. - Four of rv818's six surviving mutants now fail a case: `putenv` dropped, the bare-name setter dropped, `!=` changed to `>`, and `glob("test_*.py")`. - The fifth, `conftest.py` scanned, is now how the code works, and putting the skip back fails the new test. - The sixth, the count's value taking any string, still survives: it is a stricter scan. Git refuses a count that is not a number (`bogus count in GIT_CONFIG_COUNT`, exit 128), so a test that holds one fails on its own. - **NIT 7, in part:** - An empty count (`""`) is now a count of its own. Git 2.43 and 2.47.1 read it as no entries at all. Measured: `GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=a.b GIT_CONFIG_VALUE_0=c git config --get a.b` exits 1. - The docstring says the scan reads the count's value as a constant only. - Left out: - a value built by a call (`str(len(pairs))`) or held in a variable; - an int; - `dict()`, or `environ={}` by keyword, handed to the helper; - `setenv` with keyword arguments; - an annotated subscript. rv818 judged only `str(len(entries))` likely, and telling it from a count worked out needs what the call means, not how it is spelled. - **NIT 8:** the paste case deletes `GIT_CONFIG_PARAMETERS` first. Measured on the merge before the fix commit, with `GIT_CONFIG_PARAMETERS="'probe.param'='p'"`, the case failed (`Left contains one more item: 'probe.param=p'`). It passes now. - **NIT 9, left as it is:** main's three appends built by hand each work the count out right: `tests/test_family_head_lost_gitmodules.py:97`, `tests/test_family_bootstrap_gate_order.py:135` and `tests/test_family_add_spellings_remedies.py:137`. Each of those files is also changed by another open PR (#828 `fix/761-add-move-back-gitmodules`, #820 `fix/762-validate-family-remedies-texts`, `fix/763-holder-docs-agree`), so converting them here would only add conflicts. - **NIT 10:** the comment on `NO_MAINTENANCE` now says its `maintenance.auto` and `gc.auto` repeat the session's two, which reach these gits in the command scope. Only `gc.autoDetach` adds a setting. The two stay, so the constant alone still keeps maintenance off for these gits. - **NIT 11:** the count was in lane 5's #473 summary, not in this body. The seven sets held 2,401 distinct passes, not 2,609: the 208-test run repeats three files of the 256-test run. ### Tests at `eaa72d9e` All runs went through `run9.sh`, with the real `gh` unreachable. All passed, none skipped: - **The 26 test files the PR changes against main**, in one full-file run each: 2,206 passed (1,108 + 1,098). - These are #758's 24 test files, `tests/test_local_remote_dir_urls.py` and `tests/test_adopt_reencoded_files.py`. - The count matches the collection exactly. - **The 43 files that import any of those 26, with `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`:** 1,646 passed (825 + 821). - **The same merge made on main `d17564d9`** (a local commit, `c16a7e1a`, never pushed), before the fix commit: `tests/test_local_remote_dir_urls.py` and `tests/test_no_auto_maintenance.py`, 55 passed. ### Fails on the base The new tests were run against the base's scan and walk, as `b6e15591` has them, with `GIT_CONFIG_PARAMETERS` set: 6 failed, 30 passed. The six: - the paste case; - the `nothing`, `a-name-bound-to-it`, `an-annotated-name` and `a-name-by-subscript` cases; - the walk test. The three other new cases (`a-bare-setenv`, `putenv`, `a-name-bound-to-another-key`) are there to kill mutants, and they pass on any correct scan. MUST 1 fails on main's form at collection, as above. SHOULD 2 and NIT 10 change no behaviour. ### Mutants I made 26 mutants. 25 are caught, and one survives as rv818 said it would: - **22 on the scan and the paste case**, each run against `tests/test_no_auto_maintenance.py`: - the delenv dropped; - `putenv` dropped from `SETTERS`, or the bare-name setter; - the name branch of `is_the_count` dropped, or any name taken as the count; - `""` no longer a count; - the `Assign` names dropped, or the `AnnAssign` names; - no names collected; - each reader ignoring the names; - each of the three readers dropped from `READERS`; - `conftest.py` skipped; - `!=` changed to `>` or to `<`; - `glob("test_*.py")`, or `glob("*")`; - `by_subscript` taking any value; - the walk pointed at an empty folder. The last passes on a clean tree, like any change to an assertion that nothing is found. It is caught by rv818's SHOULD 3 repro, where the real walk fails and the mutant passes. - **The survivor:** the count's value taking any string. It makes the scan stricter (see NIT 6). - **`configure`:** with `appended_to_command_scope({}, pairs)`, the scan names the file. With the pairs dropped, three of its tests fail. - **MUST 1:** main's import put back fails collection with `ImportError`. ### Merge checks at `eaa72d9e` I ran `git merge-tree --write-tree` against `origin/main` (`fa04d2cd`) and the 53 other `fix/*` and `l3-*` heads on origin: - **Clean:** `origin/main` and 43 heads. - **#792 (`l3-785-non-ascii-paths`)** merges clean, but `tests/test_doctor_non_ascii_paths.py` imports `PASTE_ENV`. Whichever PR lands second changes `:50` (the import), the `:266` docstring and `:268` to `paste_env()`. - **#723 (`fix/663-non-utf8-name-tests`), #746 (`fix/672-bump-leg-branch-and-push-line`, which now holds #723) and #806 (`fix/757-non-utf8-test-dirs`)** conflict only in the `from conftest import` block of `tests/test_non_utf8_git_reads.py` (and of `tests/test_adopt_git_reads.py`, for #806). Both sides add names, so keep them all. - **Seven heads conflict with main itself, not with this PR:** - `l3-732-latin1-section` and `l3-733-ignored-sections` (`tests/test_doctor_legs_members_next.py`); - `fix/712-validate-pins-contracts`, `fix/766-spelled-paths` and `fix/798-adopt-legs-asked-first`; - `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip`. - I scanned each clean merged tree's changed test files for a name #758 removes, or a count the scan flags. Only #792 shows up. - **Main moved after the push.** #723 landed as `bad05d21`, and the two now conflict in one place: the `from conftest import` block of `tests/test_non_utf8_git_reads.py`. The fix is to keep both sides' names: `ORG, PROJECT, REPO, appended_to_command_scope, cloned_just_pushed, git, run_script, skip_message, skip_where_fsencode_gives_a_surrogate_no_byte, windows_fsencode`. Every other change merges in. On that merged tree, with the block resolved that way, the scan flags no file, and no file uses a name #758 removes. On `deb029c9` a probe test imported `PASTE_ENV` and pasted `git config --show-origin --list` through `paste()`: - `PASTE_ENV` held `GIT_CONFIG_COUNT=1` (`protocol.file.allow`), a stale `GIT_CONFIG_KEY_1=user.email` left from collection, and no `GIT_CONFIG_GLOBAL`. - `os.environ` at the call held a count of 4 and the session's global file. - The pasted git listed one command-line entry, `protocol.file.allow=always`, and `git config --get maintenance.auto` exited 1. So a pasted line ran with git's automatic maintenance on, in both scopes, as the issue said. `paste_env()` replaces it. It reads `os.environ` at each call (minus `PYTHONPATH`), adds the identity and `PYTHONDONTWRITEBYTECODE`, and appends `protocol.file.allow` with `appended_to_command_scope`. The six uses call it. The import-time copy also predated the session's `gh` guard on `PATH`, as `test_doctor_next_is_a_command.py`'s docstring says of its own environment; a pasted line now gets the `PATH` of the call. That docstring now says `paste_env()` reads at the call too. - **The seven.** Six are on this base, each now one call to `appended_to_command_scope`: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_root_symlinks.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`. The seventh, `test_adopt_reencoded_files.py`, came with #743 and is converted in fix round 1. `test_clone_just_pushed.py:96` stays, as the issue says: it is the helper's own test. - **The other literals.** Each sets a count of its own and is laid over `os.environ`, where the session's two settings and the Windows job's two sit, so each replaced them (`NO_MAINTENANCE` put the two maintenance keys back and dropped the job's two). I read each; each is converted, none left: - `test_family.py`: `ALLOW_FILE_PROTOCOL` is now `allow_file_protocol()`; its two importers (`test_siblings_family_name.py`, `test_family_siblings_placement.py`) and the four uses in `test_family.py` call it. - `test_family_dry_run.py`: `NO_MAINTENANCE` is a tuple of entries, appended by `isolated()`. - `test_non_utf8_git_reads.py`: `QUOTE_PATH_OFF` is a tuple of entries, which `tool_env()` appends; `user_name()` appends too. - `test_adopt_empty_symlinks.py`: `SYMLINKS_OFF` is `symlinks_off()`. - `test_adopt_root_symlinks.py`: `SYMLINKS_OFF` and `SYMLINKS_ON` are gone; the four tests that set the first call `links_as_files` and the two that set the second call `told_git`, which already appended. - one-off environments: `test_scaffold_reuse_seeded_legs.py`, `test_adopt_split_branch.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_doctor_next_is_a_command.py`. - The two tests that build a held environment on purpose to see it kept (`test_adopt_git_reads.py`, `test_clone_just_pushed.py`) now build it with `appended_to_command_scope({}, ...)`. Measured with a probe on a throwaway bare remote after a push (18 loose objects): the hand-built options and `clone_just_pushed`'s give the same clone, one pack and no loose object, with `upload-pack` and `pack-objects` in `GIT_TRACE`, and `origin` keeps the plain path. A plain local clone of the same remote keeps all 18 loose objects. For a clone that is not recursive the helper's `insteadOf` rewrite is inert, because no submodule is cloned; the helper adds one place for the options, not a different read. - `test_bump_leg_dry_run_truth.py:98`, the private bare copy of the session's spec remote: a session remote. `cloned_just_pushed(..., bare=True)`; the fixture now asserts the copy is bare. - `test_bump_leg_dry_run_truth.py:110`, `private_work`, a clone of that private remote, which tests push tags into: `cloned_just_pushed(..., recurse=False)`. - `test_doctor_legs_members_next.py:1828` and `:1929`, `Ghost`, a clone of the first member's checkout in the test's own holder copy: not a remote, and the hand-built `-c protocol.file.allow=always` was dead weight on a direct clone. Both call `clone_member`, one helper over `cloned_just_pushed(..., recurse=False)` that asserts the legs stay empty. - `test_family_bump_to_checks.py:235`, a clone of `a_private_remote`, a bare repository filled by a push: `cloned_just_pushed(remote, work, "main", recurse=False, env=env)`. - `test_doctor_pin_restore_merges.py:714` is **left as it is**: a `--filter=blob:none` clone of a file URI. The helper takes a path, and has no way to pass `--filter`. - `test_family_dry_run.py`: the comment on `NO_MAINTENANCE` says what the entries are for, the gits `isolated()` hands an environment to, whose global file `excluding()` replaces, and that the session fixture covers the module's own setup commits, which pass no `env=`. - `tests/conftest.py`: the two i645 replacements, "repacks once its estimate, taken from `objects/17` alone, is above 256 loose objects, so two loose objects there are enough", and the `objects changed` sentence. **#713's body holds the matching sentence and needs the same two replacements; I cannot edit a body.** `tests/test_no_auto_maintenance.py` has 17 new cases (28 in the file, 11 before): - `test_a_pasted_line_keeps_the_entries_the_environment_holds_at_the_call`: the environment holds two entries as the Windows job does, then the two settings as the session fixture appends them, set after the doctor tests were imported. `git config --show-origin --list` through `paste()` lists the four, in order, then `protocol.file.allow`, and reads both settings from the session's global file. - `test_no_test_sets_a_git_config_count_of_its_own`: reads every `tests/*.py` but `conftest.py` with `ast` and fails a `GIT_CONFIG_COUNT` set to a number (dict, keyword, `setenv`/`setdefault`/`putenv`, subscript) and an `appended_to_command_scope` call handed a dict written out. Three files are allowed the ones that build a held environment on purpose, with a count each in `OWN_COUNTS`. - 15 cases that hold the scan itself: seven spellings it must see, eight it must leave (`env.get("GIT_CONFIG_COUNT", "0")` is a read). Runs at the head `b6e15591` through `run9.sh` (the real `gh` unreachable), all passed, none skipped: - `test_no_auto_maintenance.py`, `test_doctor_legs_members_next.py`, `test_doctor_next_is_a_command.py`: 208 passed. - `test_clone_just_pushed.py` and `test_doctor_validator_rows.py` (it imports the doctor file), with those three: 256 passed, run one commit earlier (the later commit only moved two blank lines and reworded one failure message). - The sweep files: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_root_symlinks.py`, `test_adopt_ignored_shape_files.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`, `test_adopt_empty_symlinks.py`, `test_adopt_split_branch.py`: 445 passed. - `test_non_utf8_git_reads.py`, `test_family.py`, `test_siblings_family_name.py`, `test_family_siblings_placement.py`, `test_scaffold_reuse_seeded_legs.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`: 999 passed. - `test_family_dry_run.py`, the five files that import its `isolated` (`test_family_bump_to_a_tag.py`, `test_family_bump_to_checks.py`, `test_family_bump_to_local_only.py`, `test_family_refuse_up_front.py`, `test_family_remove_gitmodules.py`) and `test_bump_leg_dry_run_truth.py`: 216 passed. - `test_setup_project_py.py`, `test_setup_sh.py` and their importers (`test_adopt_shape_pin_and_work_dir.py`, `test_dry_run_asks_remotes.py`, `test_clone_url_bytes.py`, `test_setup_by_hand_commands.py`): 313 passed. - `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_doctor_pin_restore_merges.py`, `test_git_helper_identity.py`: 172 passed. `render-cli-reference.py --check` is ok. **On the base (`deb029c9`)**, with the new `test_no_auto_maintenance.py` copied in: 2 of the 17 fail, the paste case and the scan (it finds 12 files). The other 15 hold the scan itself and pass on any tree. Each of the new hunks was broken in a throwaway and run against its tests: - `paste_env`: a literal count of 1, a snapshot taken at import, and `appended_to_command_scope({}, ...)`: each fails the paste case, and the first and third the scan too. - The 18 sites that call `appended_to_command_scope(os.environ, ...)` (or `env`), each with `{}` in place of the environment: each fails the scan. Sites: the six of line 2, `allow_file_protocol`, `isolated`, `tool_env`, `user_name` (non-UTF-8), `symlinks_off`, `git_reads_github_from`, `github_at`, `user_name` (free text), `test_adopt_plan_yaml_quoting.py`, the two setup tests, `test_doctor_next_is_a_command.py`. - The scan: eight changes (ignore dicts, keywords, `setenv` calls, subscripts, `appended_to_command_scope({}`; flag `os.environ`; flag a read; allow fewer in `OWN_COUNTS`) each fail a scan case. - The clone sites: dropping `bare=True` from the private remote, `recurse=False` from `clone_member`, and `"main"` from the checks clone each fail. Two **survive as equivalents**: `recurse=False` in `private_work` (the spec leg has no `.gitmodules`, so a recursive clone of it is the same clone) and `env=env` in the checks clone (a clone reads no config the isolated environment changes). - The two comments cannot be mutated. `git merge-tree --write-tree` of this head against `origin/main` (`974d4827`), #748 (`deb029c9`), #713 (`979bb394`) and every `fix/*` and `l3-*` head on origin (46 refs): - **Clean:** #748, #713, and 24 of the 46 heads: 21 `fix/*` heads (every lane-5 PR but #723 and #756, and most of lane 4's) and `l3-677`, `l3-678`, `l3-rehearsal-v1.1.0`. - **`origin/main`: one file, `tests/test_adopt_empty_symlinks.py`** (four hunks), against #388's landing (`5c990708`). The parent #748 merges clean, so this arrives with the retarget. Resolve by keeping main's lines and my `symlinks_off()`. Two more spots need the same change and git does not flag them: - `tests/test_adopt_empty_symlinks.py::test_the_link_given_a_target_adopts_end_to_end` uses `SYMLINKS_OFF`, which this change removes: give it the `off` parametrization. - `tests/test_adopt_reencoded_files.py::configure`, the seventh hand-built append, goes to `appended_to_command_scope(os.environ, pairs)`. - `tests/test_family_head_lost_gitmodules.py::on_disk` (on main) is one more hand-built append: `{**env, **appended_to_command_scope(env, [("core.ignorecase", ignorecase)])}`. The scan does not read for hand-built appends, so it is not required, but it is the same thing. I made that merge in a scratch tree (main at `bf1f099b`), resolved it as above, and ran the files this change touches that main also changed, with `test_adopt_empty_symlinks.py`, `test_adopt_reencoded_files.py`, `test_family_head_lost_gitmodules.py`, `test_no_auto_maintenance.py` and the files that import `isolated` or the doctor helpers: 653 and 245 passed, in two runs. The scan finds no other file of main's with a count set by hand. - **#723 (`fix/663-non-utf8-name-tests`):** `tests/test_non_utf8_git_reads.py`, the `from conftest import` block, where both add names. - **`l3-732`, `l3-733`:** `tests/test_doctor_legs_members_next.py` (the `from conftest import` block) and `tests/test_adopt_empty_symlinks.py`. - **19 more** (`fix/664`, `fix/729` (#756) and 17 `l3-*` heads): `tests/test_adopt_empty_symlinks.py` only, because they carry main's #388 as main does. - **Not a textual conflict, but it breaks on merge:** `l3-785-non-ascii-paths` imports `PASTE_ENV` (`tests/test_doctor_non_ascii_paths.py:47` and `:223`); it must call `paste_env()`. #812 (`fix/693-local-remote-dir`) landed first, with `tests/test_local_remote_dir_urls.py` importing `ALLOW_FILE_PROTOCOL` (`:51`, `:349`). This PR's retarget merge makes it call `allow_file_protocol()` (rv818 MUST 1). - A pasted `git submodule update --init` still clones a local path: `paste()` takes no remotes, so no `insteadOf` rewrite reaches it (rv662's "build it from `read_through_git`"). It runs under the session's maintenance settings now, which is what the issue asks. - Three more appends built by hand beside `appended_to_command_scope`, in files this base does not hold: `tests/test_family_head_lost_gitmodules.py:97` (main), `tests/test_family_bootstrap_gate_order.py:135` (#682), `tests/test_family_add_spellings_remedies.py:137` (#767). The scan reads for counts set to a number, not for appends worked out by hand. Review: lane 2's independent review (issuecomment-6236874291) and its re-verification of eaa72d9, READY (issuecomment-6238781272); its new SHOULD is taken in #848, and 4a553e1 merges main over eaa72d9. CI (Brett Heap's ruling 27): H0 is the head 4a553e1 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base fc86720; and the local gate passed on its predicted squash tree 9d6c099 over main d8b8eb3: the adopt_empty_symlinks, adopt_git_reads, adopt_ignored_shape_files, adopt_nested_attributes, adopt_plan_yaml_quoting, adopt_reencoded_files, adopt_root_symlinks, adopt_split_branch, bootstrap_after_bump, bump_leg_dry_run_truth, clone_just_pushed, doctor_legs_members_next, doctor_next_is_a_command, family, family_bump_remove_rollback, family_bump_to_checks, family_dry_run, family_siblings_placement, local_remote_dir_urls, no_auto_maintenance, non_utf8_git_reads, scaffold_free_text_values, scaffold_reuse_seeded_legs, setup_project_py, setup_sh, siblings_family_name, repo_hygiene, agents_md_codes, cli_reference sets, 2359 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #758 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
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 5 days and 22 hours by commenting |
… is appended through `appended_to_command_scope`, as #848 asks of every hand-worked count, so the scan in `test_no_auto_maintenance.py` passes again (#777) `gh_that_is_signed_in` worked its `GIT_CONFIG_COUNT` out by hand from the environment's own count (`held + 1`, `GIT_CONFIG_KEY_{held}` and `GIT_CONFIG_VALUE_{held}`). That sum was right, and #848's scan (`test_no_test_sets_a_git_config_count_of_its_own`) refuses it in a test file now: it named `test_setup_project_name_edges.py:190`. #848 landed first, and its second-lander note gives this conversion to whichever of #848 and #811 lands second. The three entries now come from `appended_to_command_scope(os.environ, ...)`, merged into the returned dict beside `PATH`. The same three variables get the same values, under an environment that holds two entries, none, or an empty count. The docstring names the helper. No other line of the file changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t name over 95 characters is refused before step (4), and the prompt takes the spaces off a typed name, so `Atlas ` no longer hides in step (4) (#777) (#811) Stacked on #756 (`fix/729-typed-project-name`, `b3c77cc2`); retarget to main once it lands. One commit, `854f6ed9`, on `b3c77cc2`. It changes `setup-project.py`, which is not a pinned copy. It adds `tests/test_setup_project_name_edges.py` and edits `tests/test_setup_project_typed_name.py` (one case moves) and one comment in `tests/test_setup_project_py.py`. No template, pinned file, README, handbook, `AGENTS.md` or `docs/cli.md` changes: `render-cli-reference.py --check` says `[ok] docs/cli.md is what the generator produces`, and README stays at 1459 lines. **The 100 is not measured.** GitHub's limit of 100 characters on a repository name is from its documentation. No lane 5 run creates a real repository and none ran the real `gh` (every run had a stub first on PATH and `GH_TOKEN=bogus-l5`). Lane 1 should confirm the number against a real organisation before this lands. **No Windows run and no macOS run was made.** CI is Linux only (#597). Every test that gives a run a terminal or a `gh` stub is POSIX only (`PTY_SKIP`), as #756's file is. **What failed.** On CI (run 39027235147, git 2.55.0, Python 3.14.8), five of this PR's new tests were killed after 60 seconds: `test_a_typed_name_over_the_bound_is_refused_at_the_prompt[96|100|101|255]` and `test_a_typed_name_at_the_bound_goes_on_to_step_four[github]`. The rest of the suite passed (11487 passed, 144 skipped, 1 xfailed). Every one of the five screens ended in step (1): ``` [??] git has no credential helper for github.com, so the first push over HTTPS will fail. Fix it once: gh auth setup-git Run `gh auth setup-git` now? Type yes to continue: ``` **Why.** A GitHub run (one without `--local-remote-dir`) offers `gh auth setup-git` in its preflight when git has no credential helper for github.com (`_check_credential_helper`). The offer comes before step (3) asks for the project. These tests type only at the project prompt, so nothing answered the offer, and the run waited until the test killed it. - **The bound played no part.** The five are exactly this file's typed runs without `--local-remote-dir`. The same names typed in a local run passed on CI. The 95-character GitHub run, which the bound lets through, hung too. - **The machine decided.** On the machine the tests were written on, `/etc/gitconfig` and `~/.gitconfig` both name a credential helper, so the offer never came there. CI's runner names none. - **Reproduced** on `854f6ed9` with git's system file skipped and its global file empty (`GIT_CONFIG_NOSYSTEM=1`, `GIT_CONFIG_GLOBAL` set to an empty file): the same five fail with CI's screen (5 failed, 1 passed, in 302 s). The same command with the machine's own config gives 6 passed in 4 s. - **Not timing.** Pinned to one CPU core that a busy loop also holds (`taskset`), the slowest test in the file took 1.5 s, against its 60-second limit. **The fix is in the test, not the tool.** The offer is `_check_credential_helper`'s documented warning and offer, and a run is meant to wait for a typed answer to it. The tests depended on the machine's git config. One commit, `b5def3f0`, changes only `tests/test_setup_project_name_edges.py`: - **`gh_that_is_signed_in` also gives git a credential helper for github.com.** It is set in git's command scope (`GIT_CONFIG_COUNT`), appended to what the environment holds, as `command_scope` in `tests/test_adopt_git_reads.py` does. Its value is `!<the stub> auth git-credential`: the line `gh auth setup-git` writes, pointed at the test's own `gh` stub. The preflight now finds a helper on every machine. No run here pushes, so git never asks it. - **A new test, `test_a_github_run_here_is_not_held_by_a_git_with_no_helper`** (95 and 96 characters), runs with git's system and global files empty (`GIT_CONFIG_SYSTEM`, `GIT_CONFIG_GLOBAL`), as CI's runner has them. A run that waits at the offer now fails on every machine, not on CI alone. ### Tests All runs went through `run9.sh` (a `gh` stub first on PATH, `GH_TOKEN=bogus-l5`). Logs are under `v116/f811/` in the lane's scratchpad. - **The file, 62 tests** (60, plus the new test's 2 cases), on the fixed tree: - 10 runs with the machine's git config: 62 passed each time (`loopH-1.log` to `loopH-10.log`). - 10 runs with git's system and global files empty: 62 passed each time (`loopC-1.log` to `loopC-10.log`). - **Slow CPU:** pinned to one core that a busy loop also holds. 3 runs with empty config and 2 with the machine's: 62 passed each time (`loopSC-*.log`, `loopSH-*.log`). The slowest test took 1.5 s. - **At `b5def3f0`:** - this file, `test_setup_project_typed_name.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`: 230 passed (`setA.log`); - `test_setup_project_py.py`: 230 passed (`setB.log`); - the two setup-project name files with empty config: 84 passed (`setC-nohelper.log`). - **No other file imports this one** (`git grep`). ### Fails on the base On `854f6ed9`, with git's own config empty, the five tests fail as they did on CI (5 failed, 1 passed, `repro-base-nohelper.log`). ### Mutants Each mutant ran in a throwaway clone at `854f6ed9` with the new file copied in. - **The helper taken out of `gh_that_is_signed_in`:** - with the machine's own config, the new test's 2 cases fail, each at the offer (2 failed, 6 passed, `mut1-host.log`); - with empty config, the five CI tests fail as well (5 failed, 1 passed, `mut1-nohelper.log`). - **`import shlex` taken out:** 8 failed with a NameError (`mut3-noshlex-host.log`). - **The helper and the new test's empty config both taken out:** the new test passes on this machine (2 passed, `mut2-noisolation-host.log`). The empty config is what lets the test catch the first mutant on a machine whose own git has a helper. That is how the five passed here and hung on CI. ### Merge checks `git merge-tree --write-tree` of `b5def3f0`: - against origin/main `40e678e6`: clean; - against #756's `b3c77cc2`: clean. No other `fix/*` or `l3-*` branch on origin holds `tests/test_setup_project_name_edges.py` (250 refs checked). ### Checklist candidates - **A passed-through `--dry-run` still makes the preflight's offers.** `-- --dry-run` ends the run at the plan, saying nothing was created and nothing was cloned. Its preflight can still offer `gh auth setup-git`, which writes the person's global git config on a typed `yes`, as these five runs showed. The `gh auth login` and install offers work the same way. Whether a dry run should offer to change the machine, or print the offer without asking, is a design question. Lane 2's rv811 reviewed `854f6ed9` and found it not ready. Its MUST 1 is fix round 1 (`b5def3f0`): the same cause, and the fix it recommends. This round answers the rest in one commit, `050abea6`, on `b5def3f0`. It changes `setup-project.py` and `tests/test_setup_project_name_edges.py`. ### SHOULD 2: only the space comes off a typed name The prompt's `.strip()` took off every kind of whitespace, not only the spaces its docstring names. Measured at `b5def3f0`: each value below, typed at the prompt, gave `[ok] project Atlas` and exit 0, both with stdin the terminal and with stdin a pipe. As `--project`, each is refused, exit 2. At #756's head (`b3c77cc2`) the prompt refused each too (rv811's measurement). | Typed | `b5def3f0` | `050abea6`, typed and as `--project` | |---|---|---| | `Atlas` + tab, tab + `Atlas`, `Atlas` + VT, `Atlas` + FF, `\x1c` + `Atlas`, `\x1f` + `Atlas` | taken off, exit 0 | refused, exit 2: "It holds a control character, ..." | | `Atlas` + NBSP, `Atlas` + NEL (`\x85`), `Atlas` + U+3000 | taken off, exit 0 | refused, exit 2: the name sentence | - **The fix:** `.strip(" ")`. I took this way, not "say whitespace in the docs", for three reasons: - the issue's line is about a stray space; - the docstring, the commit and this body all say spaces; - #756's rule, that a typed name is checked as `--project` is, then holds for every character but the space. A typed control character stays refused, and the refusal quotes it as an escape. A typed line holds no newline here (`ask` drops it), so nothing else needs taking off. - **The tests:** the nine values are rows of the typed-refusal test, which now runs each of its rows with stdin the terminal and with stdin a pipe (18 new cases). They are also argv rows, as `--project` (9 cases). Each control character is typed after ^V, so the terminal takes it as text: `\x1c` alone is the quit key. ### SHOULD 3: the sentences say ASCII The name sentence said "letters" and "spaces", but `NAME_RE` takes only ASCII letters and the space, so the words described `Caf\xe9` and a no-break space as values that pass. Both sentences now say what the patterns take, in the words `adopt-project.py`'s help already uses ("ASCII letters, digits and ..."): - **Name, before:** `It is not spellable as a name: a letter or a digit first, then letters, digits, spaces and `._'()&,+@-`, at most 255 characters in all (`...`).` - **Name, after:** `It is not spellable as a name: an ASCII letter or digit first, then ASCII letters, digits, the space and `._'()&,+@-`, at most 255 characters in all (`...`).` - **Ref, after:** `It is not spellable as a ref: an ASCII letter or digit first, then ASCII letters, digits and `._/-`, at most 255 characters in all (`...`).` The two drift guards also walk every character above ASCII, first and after a letter, and find none taken. README, the handbook, `AGENTS.md` and `docs/cli.md` state neither sentence (`git grep`), and `render-cli-reference.py --check` says `[ok] docs/cli.md is what the generator produces`. No doc changes. ### NITs 4 and 5 - **NIT 4:** a new test passes `topic.locked` and `a.lockb` through `checked_ref`. `git check-ref-format --branch` takes both. The mutant `".lock" in text` now fails it. - **NIT 5:** `a_run_for`'s comprehension that only copied is now `flags_for(tmp_path, False)`. It changes no behaviour, so it has no mutant. ### Tests Every run went through `run9.sh`. Logs are under `v116/f811/r2/` in the lane's scratchpad. - **This file and `test_setup_project_typed_name.py`** (97 and 22 tests): - with git's system and global files empty, 3 runs: 119 passed each time (`r2loopC-*.log`); - with this machine's git config: 97 passed (`head-host-1.log`), and 119 passed within set A; - slow CPU (pinned to one core that a busy loop also holds): 3 runs with this machine's config and 2 with empty config, 119 passed each time (`r2loopSH-*.log`, `r2loopSC-*.log`). The slowest test took 1.9 s, against a 60-second limit. - **At `050abea6`, set A:** 874 passed (`setA.log`). That is this file, `test_setup_project_typed_name.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`, and every other test file that runs `setup-project.py`: `test_setup_sh`, `test_branch_left_bump_leg_setup`, `test_doctor_machine_row`, `test_doctor_next_is_a_command`, `test_doctor_validator_rows`, `test_dry_run_asks_remotes`, `test_family_printed_quoting`, `test_family_switch_branch_exists`, `test_gh_guard`, `test_openreposhape_command`, `test_openreposhape_package`, `test_readme_windows_quoting_sentence`, `test_release_workflow`, `test_setup_by_hand_commands` and `test_windows_paths`. - **At `050abea6`, set B:** `test_setup_project_py.py` and `test_scaffold_e2e.py`, 276 passed (`setB.log`). - **At `050abea6`, set C:** `test_shape_doctor.py`, 428 passed, 3 skipped, 1 xfailed (`setC-doctor.log`). The three skips print the PowerShell line and need a Windows host. ### Fails on `b5def3f0` The new test file, run against `b5def3f0`'s `setup-project.py`, gives **42 failed and 55 passed** (`base-b5def3f0-host.log`): - the 18 typed whitespace cases (status 0); - 24 cases that compare the name or ref sentence: 14 argv and 10 typed. The 55 that pass are the controls: - the six control-character argv rows (argv was always refused); - the drift guards (the patterns were already ASCII); - the `.lock` test (`b5def3f0` already refuses `.lock` only at the end); - the rest of the file. ### Mutants Eight, each in a throwaway clone at `b5def3f0` with this round's two files copied in, and each caught (`mutants.out`): | Mutant | Result | |---|---| | `.strip(" ")` back to `.strip()` | 18 failed, every typed whitespace case | | the name sentence's old words | 28 failed | | the ref sentence's old words | 2 failed | | `NAME_RE` also takes `U+00DF` | 29 failed, the name drift guard among them | | `REF_RE` also takes `U+00DF` | 3 failed, the ref drift guard among them | | `.lock` refused anywhere in a ref | 1 failed, the new `.lock` test | | the test types ^V only before BELL | 2 failed: the `\x1c` cases end by SIGQUIT, status -3 | | `ask`'s pipe branch strips its answer | 9 failed, every piped whitespace case. This shows the piped cases go through the pipe branch. | ### Merge checks `git merge-tree --write-tree` of `050abea6`: - against origin/main `6626a3c5`: clean; - against #756's `b3c77cc2`: clean. Of the 39 origin `fix/*` and `l3-*` branches that change `setup-project.py` or this test file, 32 are clean. Seven conflict, and each conflicts in the same way with `b5def3f0` and with origin/main itself: `fix/538-setup-paste-and-checks`, `fix/608-setup-step-checks`, `fix/family-commands-and-branch`, `fix/local-clone-race-no-local`, `fix/setup-help-offline`, `fix/setup-no-terminal-route` and `fix/setup-refuse-taken-clone-path-first`. They are stale branches, not this change. ### Out of scope rv811's "could not verify" list stays out of this PR: - GitHub's 100-character limit on a repository name, which needs a real `gh` against a real organisation (lane 1); - GitHub's 50-character topic limit, against a default id over 39 characters at step (6); - Windows and macOS; - Sonar's own analysis; - two older problems that are the same on `b3c77cc2`: a NUL typed at the prompt (Ctrl-V Ctrl-@, stdin the terminal) hangs `input()`, and a literal newline typed at the prompt ends the name there. ### Checklist candidate - **`checked_ref` passes `a.lock/b`.** Git refuses a ref with a part that ends in `.lock` (`git check-ref-format --branch a.lock/b` exits 128), but the tool asks only whether the whole ref ends in `.lock`, so `--shape-ref a.lock/b` passes the check and fails later in `git checkout`. The same holds on `b3c77cc2`, so #777 did not cause it. ### Line 1: a refusal names the check the value failed `refuse_value` gave every value the same closing sentence, and the sentence explained only a leading `-`. Measured on `b3c77cc2`, typed at the prompt: ``` REFUSED: project name is '../x', which is not a value this tool will put on a `git` or `gh` command line. git reads its own arguments: a value that starts with `-` is an option to git, not a name to us; the rest must be spellable as a name, a ref or a path. ``` `a/b`, ` Atlas`, `Atlas/`, `..` and `Caf\xe9` got the same sentence, and so did `--org ''` and `--into ''`. `refuse_value` now takes the reason, and `why_refused` gives the first check the value failed, in this order: - **empty:** `It is empty, and a value here needs at least one character.` - **starts with `-`:** keeps its sentence, `It starts with `-`, and git reads its own arguments: a value that starts with `-` is an option to git, not a name to us.` The existing `--org -evil` test in `tests/test_setup_project_py.py` still finds "git reads its own arguments". - **control character:** `It holds a control character, which the quotation shows as an escape: a newline ends a line ...`. The first sentence still quotes the value with `ascii()`. - **the pattern:** in words and as the pattern, as the sibling prints its own. For a name: `It is not spellable as a name: a letter or a digit first, then letters, digits, spaces and `._'()&,+@-`, at most 255 characters in all (`^[A-Za-z0-9][A-Za-z0-9 ._'()&,+@-]{0,254}$`).` For a ref the same with `._/-`. A pattern with no words of its own is named as `repo_shape.checked_value` names it (`It does not match ...`); `PATH_RE` never gets there, because its only failures are an empty value and a control character, which are named first. `checked_ref` says `..` (`a revision range and not a ref`) and `.lock` (`the name git gives the lock file it writes beside a ref`) in their own sentences, and `checked_passthrough` its control character. The first sentence is unchanged, so `--shape-ref is 'a b'`-style prefixes in the existing tests still match. **`repo_shape.checked_value` is not changed.** `scripts/repo_shape.py` is pinned in every project, so a wording change there is a re-pin in a shape bump. It also already prints its pattern, and its pattern is a different one (`SAFE_ARG_RE`, one for names, branches and paths: it takes `../x` and `a/b`, which `setup-project.py` refuses as names). `tests/test_scaffold_pin_and_reuse.py:502` and `:517` pin its words and still pass. The comment at `tests/test_setup_project_py.py:787` said the two refuse "in the same words"; it now says they refuse the same values, each in its own sentence. The `checked_value` docstring in `setup-project.py` said the same and is fixed. ### Line 2: a name GitHub cannot hold is refused before step (4) Measured on `b3c77cc2`, a 96-character name (and a 251-character one) typed at the prompt, non-local, with a `gh` stub that is signed in: - 96: `(3) project`, `[ok] project`, the whole of `(4) naming policy` (three names), then `(5) the plan`, where the real run would call GitHub. - 251: the same wall, then `REFUSED scaffold-name-too-long: ... is 256 bytes long`. Now both end with exit 2 and nothing else printed after the prompt: ``` REFUSED: project name is 96 characters long (Abbbbbbbbbbb...bbbbbbbbbbbb), and a project is three repositories, `<Project>`, `<Project>-spec` and `<Project>-code`: GitHub keeps a repository name to at most 100 characters, so a project name is at most 95. Nothing was created. Use a shorter name. ``` - **The number.** `<Project>-spec` and `<Project>-code` add five characters to 100, so a project name is at most 95 (`PROJECT_NAME_MAX`). - **Where it is asked.** `--project` and the positional are asked at the end of `parse_args`, because `--local-remote-dir` can come after `--project` on the line. That is before step (1) prints, and the refusal says `--project is` or `<Project> is`. The prompt asks it right after the name passes `checked_value`, before step (3) prints its `[ok]` lines. - **Only where a GitHub repository will be made.** Under `--local-remote-dir` it is not asked: 96, 100, 101 and 246 characters go on to step (4), and 247 ends at step (5) as `scaffold-name-too-long`, which is the disk's limit and still step (5)'s to ask (#261). I measured both on `b3c77cc2` (246 passes, 247 refused) and the tests pin them. - **Not a pattern in `checked_value`.** `NAME_RE` is shared with the organisation, the id, the display name and the person electing the shape, none of which has this limit, so the bound is its own function, `checked_project_length`. - **The name is quoted at both ends** (`name[:12]...name[-12:]`), as step (5) quotes a long name, since a name this long differs from its neighbours by its tail. ### Line 3: the prompt takes the spaces off a typed name Measured on `b3c77cc2`, `Atlas ` typed at the prompt: `[ok] project Atlas ` (a trailing space nobody can see), step (4) judges `Atlas `, `Atlas -spec` and `Atlas -code`, exit 1 with `FINDING naming-unclassified: Atlas , Atlas -spec, Atlas -code`, and ` Atlas` typed the same way is refused at once, exit 2. I took the first of the issue's two ways: `ask_for_the_project_name` strips what was typed. Why not the other (quote each name with `ascii()` in the gate's finding): - A stray space comes from the prompt (a paste, a slip), and the run ends at the first mistake with no second ask, so taking the space off is what saves the person a retype. Showing it only explains the refusal. - The finding is the naming gate's own output, read by `scaffold-project.py`, `adopt-project.py` and the family validators too. Changing how it quotes changes the gate for every caller, for a stray space that only the prompt produces. - An argv value is deliberate, so `--project 'Atlas '` is held as typed (a test pins it) and still ends at step (4); see the candidates. `.strip()` takes spaces from both ends, so ` Atlas` typed is `Atlas`, where it was refused. A line of three spaces is then an empty line: `no project name was typed; nothing was created.` That moves #756's `blank` case from its refusals (compared with `--project ' '`, which is still refused by the pattern) to its empty answers, a three-line edit in `tests/test_setup_project_typed_name.py`. `tests/test_setup_project_name_edges.py` is new: 60 tests. On `b3c77cc2` (a throwaway clone, detached, with the new file copied in) **47 fail and 13 pass**. The 13 that pass pin what must not change: a name at 95 goes on to step (4), the local cases (96, 100, 101 and 246 go on, 247 ends at step (5), and a late `--local-remote-dir` is read), `--project 'Atlas '` held as typed, and two drift guards. - **One case per refusal reason and carrier** (`test_a_refusal_names_the_check_the_value_failed`, 23 cases): empty, leading `-`, control character, the name pattern for `../x`, `a/b`, `..`, ` Atlas`, `Atlas/`, an accented name, `' '` and 256 characters, the positional, the ref pattern, `..`, `.lock`, a leading `-` on a ref, the passthrough, and two values that fail two checks (told the first). Each compares the whole `REFUSED:` line, and for every reason but the dash asserts the dash's words are absent. - **At the prompt, through the pseudo-terminal** (7 cases): the dash, the control character, `../x`, `a/b`, `Atlas/`, the accented name and 256 characters. The empty reason is not reachable at the prompt (an empty line is "no project name was typed"), so it is tested through `--org ''` and `--into ''`. - **The bound:** 96, 100, 101 and 255 characters, as `--project` and as the positional, and at the prompt; 95 at the prompt (both modes) and as `--project`. Each refusal is exit 2 with nothing on stdout (flag) or no `[ok] project` and no `(4) naming policy` (prompt). - **The spaces:** `Atlas `, ` Atlas` and ` Atlas ` typed give `[ok] project Atlas` with no space, `[ok] legs Atlas-spec, Atlas-code`, and a `clone into` path ending in `Atlas`; three spaces are no name. - **Drift guards:** every printable character is in the name sentence's list exactly when `NAME_RE` takes it after a letter, and the same for the ref sentence. Runs, all on the head, through `run9.sh`, logs in the lane's scratchpad (`v115/w777/`): - `test_setup_project_name_edges.py` and `test_setup_project_typed_name.py`: 82 passed (60 and 22), with `test_branch_left_bump_leg_setup`, `test_family_printed_quoting`, `test_family_switch_branch_exists`, `test_windows_paths`, `test_dry_run_asks_remotes`, `test_setup_by_hand_commands`, `test_gh_guard`, `test_openreposhape_command`, `test_openreposhape_package`, `test_release_workflow`, `test_doctor_machine_row`, `test_doctor_validator_rows`, `test_doctor_next_is_a_command` and `test_readme_windows_quoting_sentence`: **602 passed** (`runB.log`). - `test_setup_project_py.py`, `test_setup_sh.py`, `test_scaffold_pin_and_reuse.py`, `test_repo_hygiene.py` (pure ASCII source, README cap), `test_agents_md_codes.py` and `test_cli_reference.py`: **540 passed** (`runA.log`). - `test_shape_doctor.py` (its machine row runs `setup-project.py --preflight`): **428 passed, 3 skipped, 1 xfailed** (`runC.log`). The three skips print the PowerShell line, which needs a Windows host; the xfail is the file's own marker. Thirty, one or more per hunk, each in a throwaway clone and each caught by a failing new test (`mutants.out`, `mutants2.out`): the bound off by one and `<=` made `<`; GitHub's number changed; each reason's sentence changed (empty, dash, control, `..`, `.lock`, name words, ref words, the fallback); `ascii` replaced by `repr`; the branch order changed and each of empty, dash and control dropped; the pattern ignored; an empty value accepted; the local exemption dropped, at the prompt and at the parse; the parse check removed; the positional spelled as the flag; the name's tail changed; `..` and `.lock` accepted; the passthrough's reason changed; the prompt's `.strip()` removed and made `.rstrip()`; the prompt's bound removed; the prompt told it is never local. `git merge-tree --write-tree` of `854f6ed9` against: origin/main `bf1f099b` clean; `b3c77cc2` (the parent) clean; every other wave-7 branch (`fix/757`, `758`, `759`, `760`, `761`, `762`, `764`, `768`) clean, and none of them touches `setup-project.py` or the three test files; `fix/696-setup-family-folder` clean (its `setup-project.py` hunks are in `family_folder_here`, away from this change). Against all 53 origin branches only four conflict: `docs/release-notes-v1.0.0`, `fix/688-family-init-values`, `l3-677-pins-hand-step-wip` and `release/v1.0`. Each conflicts the same way, in the same files, against `b3c77cc2`, so none is this change. - **Under `--local-remote-dir`, 247 to 255 characters** still print step (4)'s wall before step (5) refuses them. The issue's direction is to ask only where a GitHub repository will be made, and step (5)'s refusal is exact, so this is left as it is. - **`--help` for `--project` does not state the 95.** The refusal does. A help change regenerates `docs/cli.md`, which other lanes' PRs regenerate too. - **GitHub's topic limit may bound the name lower than 95.** `scaffold-project.py` sets `xf-project-<id>` on each repository (`_set_topics`, `scaffold-project.py:1668`, through `run`, which raises on any non-zero exit). GitHub's documentation says a topic is at most 50 characters, so an id over 39 characters (the default id is the project lowercased) would fail at step (6), after the three repositories exist. A name of 40 to 95 characters passes this PR's bound. Not measured; it needs the real `gh` (lane 1), and the bound may be 39 for the default id. - **`scaffold-project.py` and `adopt-project.py` run directly have no GitHub bound.** A bound in the scaffold's plan, beside `scaffold-name-too-long`, would cover them too. This change covers `setup-project.py` only, as the issue directs. - **`--project 'Atlas '` (argv) still reaches step (4) with a space nobody can see.** The naming gate's finding does not quote the names it lists. Either the gate quotes with `ascii()` or `NAME_RE` stops ending a project name in a space. - **`--org` has GitHub's own limit** (39 characters, documented) and nothing asks it: a 40-to-255-character `--org` passes `NAME_RE` and fails at GitHub. Review: lane 2's independent review (issuecomment-6236485584) and its re-verification of 050abea, READY (issuecomment-6237317731). `f1dbbbf5` restacks it onto #756's squash `4dea85ee` with the same `-U0` lines; `b6f56cad` is the second-lander conversion after #848: the credential helper's entry goes through `appended_to_command_scope`, the same three variables as before, and #848's scan (`test_no_auto_maintenance.py`), which failed at `f1dbbbf5`, passes. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 b6f56ca (the restack of the branch onto main after #756 landed) passed tests, tests-git-bash and SonarCloud; the head 3795a6e is a clean update-branch merge of main 85555d0, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree e52497a over main 5c02a04: the adopt_clone_claimed, adopt_source_clone_record, adopt_work_dir_refusal, doctor_pin_restore_merges, setup_project_name_edges, setup_project_py, setup_project_typed_name, repo_hygiene, agents_md_codes, cli_reference sets, 717 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #777 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…asks GitHub's limits on a name, a topic and an organisation where GitHub is, so an id over 39 characters no longer fails `--add-topic` after the repositories exist (#838) (#859) # Title scaffold-project.py, adopt-project.py and setup-project.py: the plan asks GitHub's limits on a name, a topic and an organisation where GitHub is, so an id over 39 characters no longer fails `--add-topic` after the repositories exist (#838) # Body GitHub keeps a repository's name to 100 characters, a topic to 50, and a user's or an organisation's name to 39. **The 100 and the 50 are documented on docs.github.com ("Creating a new repository"; "Classifying your repository with topics"). For 39, the docs give the limit on a user's name (the Enterprise Cloud "Username considerations for external authentication"), and nothing for an organisation's. None of the three is measured here.** No run of this repository creates a real repository, and I did not run the real `gh` to find them. The fake `gh` in the new tests refuses only a topic over 50 characters; a name over 100 or an organisation over 39 never reaches it. A probe against a real organisation is lane 1's. Nothing asked these limits before: - `scaffold-project.py` sets `xf-project-<id>` last, after it has created and seeded all three repositories. An id over 39 characters failed `gh repo edit --add-topic` there. A `--project` of 40 or more gives such an id by default. - `adopt-project.py execute` failed at the same topic after both legs and the split were pushed and the pull request was open. - A `--project` of 96 to 250 characters, or an `--org` of 40 to 255, passed the plan and reached the first `gh repo create`. Now each is refused before any repository is made, and only where a GitHub repository will be made. Under `--local-remote-dir` nothing goes to GitHub: a name's limit is still the disk's, and no topic is set. **1. A topic over 50 characters.** The direction was to ask in the plan. The number lives in `scripts/shape_materialize.py`, beside the disk's `NAME_MAX`: `GITHUB_TOPIC_MAX = 50`, and `GITHUB_ID_MAX = 50 - len("xf-project-") = 39`. I did not put it in the contract's `id_pattern`, so `scripts/repo_shape.py` is not re-pinned, and a local-only project keeps a long id. `refuse_a_topic_github_cannot_set` reads the id's bound off the topic the naming policy gives. - `scaffold-project.py` refuses `scaffold-topic-too-long` right after `scaffold-bad-id`. That is before `_declaration`, which calls `gh api` for a `--pin` with no `--pin-source`, and `--dry-run` refuses the same. No process has started by then when `--elected-by` or `GIT_AUTHOR_NAME` names the elector. Without either, `_identity` has run `git config user.name` first, as it has before `scaffold-bad-id`. Nothing is created either way. A derived id is named as `scaffold-bad-id` names one, `--id (not given, so --project lowercased)`. Its remedy is an explicit `--id`, which keeps the long name. A passed `--id` is told to pass a shorter one. - `adopt-project.py execute` refuses `adopt-topic-too-long` where it works out the topic, before `_confirm` and before either leg. That is after the `git ls-remote` reads of the repository the split is pushed to (its tracking branch, a split branch left behind, a branch in the split branch's way), so one of those refusals can be shown first. No `gh` call is made and nothing is created either way. It names the plan's `id:` (or `id (not set in the plan, so legs.assembly lowercased)`), and says to fix `id:` or re-run `plan` with `--id`. - Evidence, by the reproduction below. On `bad05d21`, a 40-character project made three `gh repo create` calls, then `HTTP 422` at `--add-topic`, exit 2. An adoption with a 40-character `--id` pushed both legs and opened the pull request, then printed `Set the topic by hand` with a topic GitHub refuses. On `71936633`, both refuse with no `gh` call and nothing made. **2. Names over 100 in the direct tools.** The direction was the same check, once, in the helper that asks the disk's limit. `refuse_unstorable_names` now takes a third list, `github`: the names GitHub will be asked to create. A name over `GITHUB_NAME_MAX = 100` is refused under the same code, `scaffold-name-too-long` or `adopt-name-too-long`. - The scaffold passes all three names without `--local-remote-dir`. Adopt passes the two legs, because the root already exists. - In adopt, that root is the source's own repository on GitHub, which GitHub lets be named with up to 100 characters. A source named with 96 to 100 characters gives legs of 101 to 105. Every adoption keeps the source's own name, so `--project` cannot shorten it. Where an over-long leg is named after a root of at most 100 characters, the remediation says to rename the source on GitHub first, to at most 95 characters, and plan again. It says so in `_rename_first`'s words, as `adopt-bad-id` does, and says GitHub's 95 once. A leg the plan was edited to name otherwise keeps the remediation it had. So does any leg of a root over 100 characters, which no repository on GitHub can be named, so renaming the source shortens nothing: "Use a shorter --project". That comes only where the plan's `legs.assembly` is not held to a GitHub `origin`, that is, a source with no `origin` or a path `origin`, and a long `--project` was typed. - A name the disk cannot hold is not named twice. - The remediation names each limit the names broke. On GitHub it also names the id's 39, which the topic line bounds: whoever picks a shorter name learns both numbers at once. - The disk-only remediation is word for word as before. **3. `--org` over 39.** `refuse_an_owner_github_cannot_have` asks it: `scaffold-org-too-long` in the scaffold's plan, and `adopt-org-too-long` in `execute` beside the names, before either leg. A detected organisation needs nothing extra. One read from a GitHub `origin`, or from `gh`, is never over 39. One read from another forge's URL is held to GitHub's limit, because GitHub is where the repositories go. **4. `--help` states the bounds.** Each of `--project`, `--id` and `--org` says its number in `setup-project.py`, `scaffold-project.py` and `adopt-project.py plan`, and `docs/cli.md` is regenerated. The two direct tools build the sentence from the constants. `setup-project.py` cannot import them, because it runs before there is a checkout. `test_setup_project_says_the_numbers_it_cannot_import` holds its sentences to the numbers. Once #811 lands, the same test also holds #811's `GITHUB_NAME_MAX` and `PROJECT_NAME_MAX` to them. I did not hold the help line back for a later pull request: `merge-tree` finds no conflict in `docs/cli.md` against any open branch that edits it (below), and any later conflict there is solved by running the renderer again. Step (5) of `setup-project.py` is the scaffold's own `--dry-run` plan, given setup's `--project`, `--id` and `--org`. That plan now refuses all three before step (6) asks for the yes, and setup prints `the plan could not be produced (see above)` under the scaffold's refusal. The refusal names `--id`, `--org` and `--project`, which setup takes too. Tests cover each value through the flags and a name typed at the prompt (40 refused, 39 goes on). I added no check of my own to setup's parsing. #811's tests pin that a 95-character name with a derived id goes on to step (4) (`test_a_name_at_the_bound_goes_on_to_step_four`). A topic check at parse time would refuse that name before step (4). #811's own 95 stays setup's early check of `--project`. The new refusals also avoid the words `characters long`, which #811's tests assert are absent from a 95-character GitHub run. **Lands after #811.** On a probe merge of `71936633` with #811's head `050abea6`, these files pass: #811's `test_setup_project_name_edges.py`, #729's `test_setup_project_typed_name.py`, `test_setup_project_py.py`, the new file and `test_cli_reference.py` (430 passed). The `merge-tree` with #811 and with #756 (`b3c77cc2`) is clean, and no hunk here touches their name rules. - New, `tests/test_github_name_limits.py`, 64 tests (58 after fix round 1, 50 before it). Each bound is tested at N and N+1 through the flags, a derived id, `--id ''`, setup's prompt, both direct tools, and with and without `--local-remote-dir`. Refusals are asserted with no process started (in process, `no_process`) or with no `gh` call and nothing on disk (end to end). One case covers a name of 40 to 95 characters with a short `--id`, which still passes. Fix round 1 added: - a source named with 96 and with 100 characters, told to rename it first; - the edited leg and the disk, which keep their remediations; - `execute` without `--yes`, and its control; - each remediation of the scaffold's and `execute`'s own, word for word; - and the NIT round's (#859's re-review): one leg named after the source with the other edited to a short name, for each leg (2); a leg that starts with the root without being named after it (1); a root of 101 and one of 251 (2); the disk's remediation under `--local-remote-dir` beside a leg named after a 100-character source (1). The in-process test of the rename-first case now asserts the refusal's detail too. - Changed, `tests/test_scaffold_manifest_values.py`: the GitHub half of `test_the_longest_name_a_disk_keeps_is_still_accepted` asked a 250-character project. It now asks GitHub's longest, 95, with a short `--id`. - **On base `bad05d21`, 33 of the first round's 50 new tests fail.** The other 17 are controls that pass on both: the value at the bound, local runs, and a short `--id`. The changed test passes on both. **On `9166f84f`, 3 of the 58 fail**: the three cases of a source named with 96 or 100 characters. **On `71936633`, 7 of the 64 fail**: the roots of 101 and of 251, which were told to rename the source, and five that compare the rename-first remediation word for word, whose sentence now says 95 once (the 96- and 100-character end-to-end cases, the in-process test, and the two one-leg cases). With only that sentence changed on `71936633`'s code, 2 of the 64 fail: 101 and 251. The rest pass on both; each kills a mutant (below). - Runs, all on Linux, each with the commit it ran on: - **On `495d4b33`** (the NIT round, `dirty=0`), in one run of three shards: `test_github_name_limits.py`, `test_scaffold_manifest_values.py`, `test_adopt_manifest_values.py`, `test_adopt_github_exits.py`, `test_adopt_project_is_the_source.py`, `test_cli_reference.py`, `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py`, `test_repo_hygiene.py` and `test_agents_md_codes.py`: 980 passed, 3 skipped, 1 xfailed in 857.78s (the longest shard). Every test file that names `adopt-name-too-long`, the remediation or the predicate is among them. The 179-file run below was not repeated for this round: it changes one predicate and one sentence of `adopt-project.py`. - **On `71936633`** (fix round 1, `dirty=0`): the 179 test files that import or drive a changed file, in two runs: 5364 passed, 13 skipped, 1 xfailed, and 5573 passed, 24 skipped. - **On `9166f84f` with fix round 1's diff uncommitted**, whose `sha256` equals that of `git diff 9166f84 7193663`: the new file, `test_scaffold_manifest_values.py`, `test_adopt_manifest_values.py`, `test_adopt_github_exits.py`, `test_adopt_project_is_the_source.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`: 434 passed. - **On `bad05d21` with the first round's change uncommitted** (an earlier state of it, not `9166f84f`): - the issue's Verify files (`test_setup_project_py.py`, `test_scaffold_manifest_values.py`, `test_adopt_manifest_values.py`, `test_scaffold_leg_path_collisions.py`, `test_shape_doctor.py`, `test_cli_reference.py`, `test_repo_hygiene.py`), with `test_agents_md_codes.py` and 47 of the new file's tests: 1148 passed, 3 skipped, 1 xfailed; - the 37 other files that drive `gh` or a GitHub-mode run: 2127 passed, 1 skipped. - **On `3845fd54`**, a local commit that differs from `9166f84f` only in `tests/test_github_name_limits.py`: - the other 98 test files that import or run a changed file: 3523 passed, 2 skipped, and 2908 passed, 28 skipped; - the setup cases again with git's system config emptied, as on a machine with no credential helper (CI's runner): 10 passed. The reproduction runs on a fake `gh` and touches no network. The `bad05d21` side ran in the first round; the other side ran again on `71936633` in fix round 1. - **Scaffold.** It ran in GitHub mode with `--no-push`, against a fake `gh` that refuses a topic over 50 characters as GitHub documents. On `bad05d21`, `--project` of 40 characters made `repo view` ×3, then `repo create` ×3 (spec, code, root), then `repo edit ... --add-topic xf-project-<40>`, which got `HTTP 422`. The run exited 2, with the work trees left. On `71936633` it exits 2 with `scaffold-topic-too-long`, no `gh` call, and no work directory. - **Adopt.** `execute` ran in GitHub mode with `--id` of 40 characters, using `test_adopt_github_exits.py`'s shim told to refuse `repo edit`. On `bad05d21` it made both legs, pushed the split and opened the pull request. Then it printed `The split IS pushed and the pull request IS open. Set the topic by hand:` with three commands GitHub would refuse too. On `71936633` it exits 2 with `adopt-topic-too-long`, no `gh` call, and no leg. - **First round:** 20 mutants, one per new hunk, and each was caught. They covered the GitHub names ignored, the de-duplication, the GitHub remediation, the topic and the organisation off by one, `GITHUB_PROJECT_MAX`, each call site in the scaffold and in adopt, the local guards both ways, the derived, passed and empty id's naming, adopt's `id:` naming, each tool's `--help`, setup's usage, and a stale `docs/cli.md`. - **The review at `9166f84f`:** 42 mutants; 7 survived. - **Fix round 1, at `71936633`:** 51 mutants, one at a time: 50 killed and 1 known survivor. - rv859's 42 are among them, and six of its seven survivors are now killed: the two refusals moved below `_confirm`, and the four remediation wordings. - The survivor is `sm-both-ends-26`, `_both_ends`' boundary of 27, which is moved code and not new. - 9 are new, for the rename-first hunk, and each is caught. - **The NIT round, at `495d4b33`:** 33 mutants, one at a time, each run with `-x` against the new file: 33 killed, none survived. - rv859b's seven survivors are killed. `rv-any-to-all` and `rv-code-only` fail `test_one_leg_named_after_the_source_is_enough[spec-after-the-root]`. `rv-spec-only` and `rv-suffix-always-spec` fail `[code-after-the-root]`. `rv-startswith-root` fails `test_a_leg_the_plan_names_otherwise_keeps_its_remediation[starting-with-the-root]`. `rv-detail-dropped` and `rv-detail-is-remedy` fail `test_a_leg_named_after_the_source_is_mended_by_renaming_it`. - Its other five mutants of the hunk, f859's eight (re-aimed at the new predicate) and six of rv859's earlier survivors are rerun, and killed. - 7 are new: the assembly bound dropped, strict, one over, 95, read off the `spec` leg and inverted, and the sentence that said 95 twice. Two are killed by the root of 101, four by the root of 100, and the sentence that said 95 twice by the remediation's words. - One survived a first pass, made on an earlier local form of this commit: `f-ad-local-guard-dropped`, the `--local-remote-dir` guard in the handler. Its only test used a root of 247, which the new bound refuses with the disk's remediation anyway, so the guard was no longer reached. `test_a_leg_named_after_the_source_is_not_the_disks_trouble` kills it, and all 33 were run again on `495d4b33`. At `495d4b33`, `merge-tree` is clean against origin/main `4dea85ee` and against 34 of the 36 other open `fix/*` and `l3-*` heads (`ls-remote` at about 03:00Z), among them `fix/798-adopt-legs-asked-first` (`70801c91`), which conflicted at fix round 1's sweep. The two that conflict are `l3-677-pins-hand-step-wip` (`shape-doctor.py`, `validate-pins.py` and two test files) and `l3-678-naming-checkout-wip` (`shape-doctor.py`). They conflict the same way at `71936633`, and touch none of this pull request's files. Fix round 1's sweep: at `71936633`, `merge-tree` was clean against origin/main `190133d5` and against 55 of the 58 open `fix/*` and `l3-*` heads other than this one. Among the 55 are #811 (`fix/777-typed-name-edges`, `050abea6`) and #848 (`fix/835-config-count-appends`, `79b2eac4`). They also include every head that touches these files except `fix/798`: `fix/710-non-utf8-branch-names`, `fix/711-git-bash-dialect`, `fix/715-yaml-reader-spaces`, `fix/716-adopt-check-plan-name`, `fix/729-typed-project-name` (#756), `fix/764-bootstrap-notice-exit-texts`, `fix/766-spelled-paths`, `fix/796-adopt-source-spellings`, `fix/797-adopt-default-branch-refs`, `fix/799-adopt-plan-out-checked`, `fix/800-pin-source-top-level`, `fix/801-answered-paths-per-leg`, `fix/803-adopt-clone-claimed`, `fix/807-eol-block-tracked-spelling`, `fix/836-child-environment`, `l3-785-non-ascii-paths` and `l3-rehearsal-v1.1.0`. Three conflicted. Each conflicted the same way at `9166f84f` and against main `190133d5` alone, so they are theirs and not this change's: - `fix/798-adopt-legs-asked-first` (`scripts/shape_materialize.py`); - `l3-677-pins-hand-step-wip` (`shape-doctor.py`, `validate-pins.py` and two test files, none of them touched here); - `l3-678-naming-checkout-wip` (`shape-doctor.py`). The new file builds no `GIT_CONFIG_COUNT` by hand. The prompt test gives git a credential helper through a global config file of its own (`GIT_CONFIG_GLOBAL`). #848's `test_no_test_sets_a_git_config_count_of_its_own` passes on a probe merge of `71936633` with #848's head `79b2eac4`: `test_no_auto_maintenance.py` and the new file, 118 passed. - `scripts/family.py init` has the same late `--add-topic` for the family's id, and the same unasked `--org` and holder name. The issue's goal names the three tools, and seven open branches edit `family.py`, so I left it out. It is checklist candidate 1. - `adopt-project.py check` asks neither GitHub's limits nor the disk's. `execute` refuses both before either leg. `check` takes `--local-remote-dir` and could find them: candidate 2. 1. **`family.py init`: an id over 39 characters fails `gh repo edit --add-topic` after the holder is created and pushed.** The topic `xf-project-<family id>` is set last (`scripts/family.py:2657`), and nothing asks GitHub's 50. `--org` over 39 and a holder name over 100 are not asked either. Direction: the same three helpers, in `init`'s plan, without `--local-remote-dir`. 2. **`adopt-project.py check` passes a plan that `execute` refuses for a name's length.** `adopt-name-too-long` (the disk's 255, and now GitHub's 100), `adopt-org-too-long` and `adopt-topic-too-long` are asked by `execute` alone. `check` takes the same `--local-remote-dir`, so it could report each one as a finding before the person's yes. 3. **An `--org` holding a character GitHub refuses in an organisation's name passes every plan.** GitHub keeps an account's name to ASCII letters, digits and single hyphens, with no hyphen at either end; that rule is not checked against docs.github.com here. `checked_value` and the manifest validator accept `my_org.x`, which `tests/test_adopt_manifest_values.py` plans as valid. That `--org` reaches `gh repo create`. 4. **A leg the plan was edited to name, over a limit, is told "Use a shorter --project".** `legs.spec` and `legs.code` are the plan's own to edit, and `adopt-name-too-long` (the disk's 255, and GitHub's 100) names a leg named otherwise than after `legs.assembly` with the remediation of a name made from `--project`, which does not shorten it. Fix round 1 keeps that remediation word for word for such a leg; only a leg named after the source is told to rename the source. The mixed case goes with it: a source of 96 to 100 characters, one leg named after it and the other edited past 100, is told only to rename the source, which does not shorten the edited leg (rv859b's re-check). Direction: name the plan's key, `legs.spec` or `legs.code`, as the thing to shorten. Review: rv859, lane 5's independent review, answered by fix round 1 (issuecomment-6243078675); rv859b's re-review of fix round 1, READY at `71936633` (issuecomment-6244672717); and rv859b's re-check of the NIT round, READY at `495d4b33` (issuecomment-6245094878). CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 495d4b3 passed tests, tests-git-bash and SonarCloud; the head 9695935 is a clean update-branch merge of main e6cf5d9, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree c17128d over main e6cf5d9: the github_name_limits, scaffold_manifest_values, repo_hygiene, agents_md_codes, cli_reference sets, 317 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #838 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…light's offers and `--shape-ref` is held to git's rule part by part, so a rehearsal no longer writes git's global config and `a.lock/b` is refused before the clone (#857) (#873) Stacked on #811 (`fix/777-typed-name-edges`); retarget to main once it lands. #756 has landed as `4dea85ee`, and #811's head is now `b6f56cad`: #756's squash merged in, and a test-only change to `tests/test_setup_project_name_edges.py`. This branch stays on `050abea6`, the merge base of the two, so its diff against #811's branch is its two commits: the fix (`79a33f11`) and fix round 1 (`ac570069`), which changes two test files and nothing else. Lane 5 restacks it after #811 lands. **Direction: the silent reading.** A run passed `-- --dry-run` reports what is missing and asks nothing that would change the machine. Where a terminal would have been offered a command, it is told `not offered: --dry-run was passed through. A run without it offers this here, and so does --preflight.`, and asked nothing. The plan's last line now ends: ``The preflight made none of its offers; a run without --dry-run makes each one marked `not offered` above.`` `--preflight` still makes every offer, whatever follows `--`: it is the run that exists for them (#59), and it returns before anything reads the passthrough. **Why.** Measured at `050abea6`, what each offer does on a typed `yes`, and all three change the machine: - an install (`offer_missing`): git or gh, through the package manager; - `gh auth login` (`_offer_login`): gh's own sign-in; - `gh auth setup-git` (`_check_credential_helper`): gh writes a credential helper into the person's global git config. A pty run of `setup-project.py Sample --org demoorg --elected-by Tester --into <dir> -- --dry-run`, under a signed-in stub `gh` and a git with no helper, asked ``Run `gh auth setup-git` now? Type yes to continue:``. The typed `yes` ran it, and the stub, which writes what the real one writes, added `[credential "https://github.com"]` to the file `GIT_CONFIG_GLOBAL` names. The run then ended at the plan saying `nothing was created and nothing was cloned`. With gh signed out, the same run asked ``Run `gh auth login` now?``, and the `yes` signed the stub in. The help said a `--dry-run` there "ends this run at the plan, creating nothing". And #811's CI run 39027235147 showed five rehearsals that typed only the project name waiting at that question until they were killed. Nothing argued for the offering reading: the offers stay one flag away (`--preflight`), and the real run makes them. **What changed.** `preflight` passes `offering = not passes_a_dry_run(opts.passthrough)` to `preflight_checks`, and it goes down through `_check_git`, `_check_gh`, `offer_missing`, `offer_install`, `_offer_login` and `_check_credential_helper` to `offer`. `offer` tests it after `can_ask()`, so a run with no terminal prints what it printed before, and it is a bool, never `Options`, so `test_the_offer_helpers_cannot_see_the_yes_flag` still holds. The help says it, and `docs/cli.md` is regenerated. `AGENTS.md`, `README.md` and the handbook were grepped for `dry-run`, `creating nothing` and the offers: no sentence there is made false, so none is touched. The git install offer cannot be reached by a dry run end to end: `_main` refuses a machine with no git (`GIT_MISSING`) before the preflight runs. It is held in process, through `preflight`, as the other two are. **Tests** (`tests/test_setup_project_dry_run_offers.py`): - `test_a_dry_run_makes_none_of_the_offers_preflight_makes[install|login|helper]`, once for `--dry-run` and once for its short spelling `--dry` (`passes_a_dry_run` reads both), in process on every platform. A fake terminal types `yes` to anything and a recorder runs nothing. `preflight` on `Sample --org demoorg -- <flag>` makes no prompt and runs no offered command, says `not offered` once per offer (2, 1, 1), and refuses or passes as before. `preflight_only` on `--preflight -- <flag>` then makes the same 2, 1, 1 offers. - `test_a_dry_run_with_no_terminal_says_what_a_run_without_it_says[install|login|helper]`, in process: with no terminal (stdin is not one and there is none to open), `preflight` on `-- --dry-run` prints no `not offered` line, and what it prints is what the same run prints without `--dry-run`. It holds the order in `offer`, `can_ask()` before `offering`. - `test_a_dry_run_leaves_git_with_no_helper_as_it_found_it[py|piped|py with --dry]`, a pty run (`PTY_SKIP`). A signed-in stub `gh`, an empty system config and a global config of the run's own, with no `GIT_CONFIG_COUNT` and run from a folder that is no repository. The run asks nothing, exits 0 at the plan, prints the `[??]` warning with the `not offered` line under it and the new last line, never calls `gh auth setup-git`, and leaves the global config the bytes it was. - `test_a_dry_run_does_not_sign_gh_in`: the same with gh signed out (git has a helper, so the login is the one offer at stake). It asks nothing, the `[!!]` line has the `not offered` line under it, it refuses `one or more prerequisites are missing`, and gh is still signed out. **Direction: finish the rule,** in a new `why_not_a_ref` beside `why_refused`, which `checked_ref` asks after `REF_RE`. Git's rule is per part: no part is empty (`//`, a trailing `/`), none starts with `.`, none ends in `.lock`, and the whole holds no `..` and does not end in `.`. Each failure has its own sentence (`WHY_EMPTY_PART`, `WHY_DOT_PART`, `WHY_LOCK_PART`, `WHY_DOT_END`). `WHY_RANGE` and `WHY_LOCK` keep #811's words, so its `ref-range` and `ref-lock` cases are unchanged. `REF_RE`'s comment and `checked_ref`'s docstring say what is asked. **Evidence.** At `050abea6`, `checked_ref` beside `git check-ref-format --branch` disagreed on seven of the issue's ten names (`a.lock/b`, `x/y.lock/z`, `release.lock/v1`, `a/.b`, `a//b`, `a/b/`, `a.lock/`), and on `a.` and `a/b.`, under git 2.43.0 and 2.47.1 alike. Over all 3110 names of up to five parts from {`a`, `0`, `.`, `/`, `-`, `.lock`} that `REF_RE` takes, they disagreed on 1122 before and on none now, under both versions. In self-bootstrap mode `--shape-ref a.lock/b` cloned the standard and ended in `error: pathspec 'a.lock/b' did not match any file(s) known to git`, exit 1. Now it is refused before `(0) self-bootstrap` prints. **Tests** (`tests/test_setup_project_ref_parts.py`): - `test_the_tool_and_git_give_one_answer[<name>]`: the issue's ten names and fourteen around them (`a.`, `a/b.`, `a./b`, `a.lockb`, `a/b.lock/`, `a/.lock`, `a..b`, `refs/tags/v1.0`, `v1.0.0`, and five from fix round 1). Each is fed to `checked_ref` and to `git check-ref-format --branch`, which must give one answer, and the tool's reason is pinned. No network. Fix round 1's five are `a/.b/c` and `a/b/.c/d`, a part that starts with `.` that is not the last part; and three names that break two rules, which hold the order `why_not_a_ref` asks them in (its docstring says the sentence names the first one the value failed): `a//..b` is a range, not an empty part; `a//.b` an empty part, not a dot part; `a.lock/b.` a lock part, not a `.` at the end. With `a/.lock` and `a/b.lock`, that holds each of the five pairs of neighbours in the order. - `test_a_ref_git_refuses_is_refused_before_the_clone[a.lock/b|a/.b|a//b]`: a self-bootstrap run exits 2 with the reason, prints nothing on stdout and clones nothing. Through `run9s.sh`, `SHARDS=3`, at `79a33f11`: the two setup files, every other test file that names `setup-project` (`git grep -l 'setup-project' -- tests`), the two new files and the trio, 25 files. - git 2.43.0: 1606 passed, 3 skipped, 1 xfailed in 746.00s, rc 0 (shards 428 passed, 3 skipped, 1 xfailed; 546 passed; 632 passed) - git 2.47.1: 1606 passed, 3 skipped, 1 xfailed in 677.22s, rc 0 (shards 428 passed, 3 skipped, 1 xfailed; 546 passed; 632 passed) Before the commit, unsharded on git 2.43.0: `test_setup_project_py.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` and the two new files, 377 passed. `test_setup_sh.py` and the trio, 235 passed. Fix round 1, at `ac570069` (`setup-project.py` is unchanged, so every other file's result stands): - git 2.43.0, `SHARDS=3`: `test_setup_project_py.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py`, the two new files and the trio, 535 passed in 174.38s, rc 0. - the two new files, 40 tests: 40 passed on git 2.43.0 (3.74s) and 40 passed on git 2.47.1 (2.64s). The two new files, run against `050abea6`'s code: at `ac570069`, 40 tests, 28 failed and 12 passed (at `79a33f11`, 28 tests: 20 failed, 8 passed). All ten dry-run cases that make an offer fail: the three machines in both spellings, the three terminal runs and the signed-out run. Line 2's fifteen table rows that `050abea6` gets wrong fail: ten names git refuses that it passed, `a/.lock`, which it refused for the `.lock` and not for the part that starts with `.`, and fix round 1's `a/.b/c`, `a/b/.c/d`, `a//.b` and `a.lock/b.`. So do the three self-bootstrap cases. The 12 that pass are the three no-terminal cases (`050abea6` made no offer there either), and nine table rows: eight where `050abea6` already agreed with git (`a.lock`, `a/b.lock`, `topic.locked`, `a./b`, `a.lockb`, `a..b`, `refs/tags/v1.0`, `v1.0.0`) and `a//..b`, which it refused for its `..`, with the sentence the order gives. They hold the table to both directions, `topic.locked` passing among them. 23 mutants, each run against its tests, and all 23 caught: - the gate in `offer` removed; - `offering` not passed on in each of `offer_install`, `offer_missing`, `_check_git`, `_check_gh` (install, login), `_offer_login`, `_check_credential_helper`, and each of `preflight_checks`'s three calls; - `preflight` always offering; - `preflight_checks` silencing `--preflight -- --dry-run` too; - the plan's new sentence removed; - the `not offered` words changed; - the help reverted (`tests/test_cli_reference.py`); - `checked_ref`'s old two checks put back (`text.endswith(".lock")`); - each of the four new parts of the rule dropped; - `why_not_a_ref`'s answer ignored; - the `.lock` part sentence changed. The comment and docstring hunks have no mutant: they are prose. None survived. An independent review then made 14 mutants of its own at `79a33f11`. Nine were caught. Five survived the tests as first pushed (the two new files and the setup tests): R1, the dry-run check put before `can_ask()` in `offer`; R3, the `..` and empty-part checks swapped; R4, `parts[-1].startswith(".")` only; R7, a literal `"--dry-run" not in opts.passthrough` in `preflight`; and R13, `parts[:2]` only. Fix round 1 closes each, rebuilt from the review's descriptions and run at `ac570069` against the two new files on git 2.43.0: - R1 fails the no-terminal test on all three machines (3); - R3 fails `a//..b` (1); - R4 fails `a/.b/c` and `a/b/.c/d` (2); - R7 fails the three `--dry` cases in process and the `--dry` terminal run (4); - R13 fails `a/b/.c/d` (1). Run against the two files as first pushed (`79a33f11`), the same five all survived, which is what the review found. Two more mutants for the new order rows are caught: `a//.b` fails with an empty part checked after a dot part, and `a.lock/b.` fails with a `.` at the end checked before a lock part. The other nine of the review's mutants were caught before and were not rerun. `git merge-tree --write-tree` of `79a33f11`, and of `ac570069` for the first line: - **#811's head `b6f56cad`: clean**, for both (`git merge-tree --write-tree b6f56ca ac57006`, tree `3e369754`). Fix round 1 changes only the two test files this PR added, so the other lines below stand for it: they were checked at `79a33f11` and not run again. - **origin/main `d7befb91`: conflicts in `setup-project.py` and `tests/test_setup_project_typed_name.py`.** The conflict is inherited: `050abea6` alone conflicts with `d7befb91` in the same two files (#756 landed as a squash, and #811's branch held its history). A commit-tree prediction of `79a33f11` merged onto `b6f56cad` is clean against `d7befb91`. - #859 `495d4b33` (`fix/838-github-name-limits`): clean. `setup-project.py` and `docs/cli.md` merge by themselves, with this head and with the predicted restack. - #821 `e15c237b` (`fix/711-git-bash-dialect`): clean, `setup-project.py` merging by itself. The predicted restack conflicts with it only in `README.md`, `docs/handbook.html` and `shape-doctor.py`, where `d7befb91` itself conflicts with `e15c237b`. None of the three is this branch's. - `fix/764` `05a1ebb3`, `fix/796` `2d51f181`, `fix/798` `70801c91` and `fix/803` `08f7a283` (`docs/cli.md`): clean. - No other `fix/*` or `l3-*` branch touches these four files. - #848's scan on main (`counts_set_to_a_number`) finds no count set in either new test file. - `setup.sh`'s shim keeps its own copy of the old rule (`*..*|*.lock`), and its comment says it mirrors `setup-project.py`. Under `cat setup.sh | bash -s -- --shape-ref a.lock/b` the shim clones the standard and ends in git's `pathspec 'a.lock/b' did not match`, exit 1, before this file's check runs. Measured at this head, it is outside #857's lines and is reported to lane 5 as a checklist candidate. - The plan's last line prints on every dry run, including where the preflight marked no offer `not offered`, where "each one marked `not offered` above" points at none. It is true as written. Printing it only where an offer was marked would take a count that `offer` writes and the plan reads, held in the module: `offer` is three to five calls below `preflight`, and each caller between returns a bool that the next one reads. That is state in a file whose `offer` takes its inputs as arguments and nothing else, for a sentence most dry runs would then not print, so it is left. - No Windows or macOS run: CI is Linux only (#597). The in-process test runs on every platform; the pty tests are POSIX only. Review: rv873, lane 5's independent review, READY at `79a33f11` with S1 (issuecomment-6245576863); f873's fix round 1 `ac570069` re-checked READY with no findings (issuecomment-6245715704). S2 is filed as #874. `0774ad60` restacks it onto #811's squash `bc362333` with three merges and the same `-U0` lines. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 0774ad6 (the restack of the branch onto main after #811 landed) passed tests, tests-git-bash and SonarCloud; the head e41a38a is a clean update-branch merge of main 85aa8a1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree fa52066 over main 85aa8a1: the doctor_next_is_a_command, doctor_validator_rows, setup_project_dry_run_offers, setup_project_ref_parts, repo_hygiene, agents_md_codes, cli_reference sets, 320 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #857 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #835 (comment)
Stacked on #818 (
fix/758-git-command-scope,4a553e17); retarget to main once it lands. The fix is one commit,b9ad1187, written on #818'seaa72d9e. The headfafba9ecmerges #818's refresh4a553e17(mainfc867207) into it, cleanly.Tests only. Nothing a user runs changes, and no pinned file changes. No README, handbook,
AGENTS.mdordocs/cli.mdsentence is touched: none mentions these helpers (render-cli-reference.py --checkpasses).Fix round: rv848 and lane 2's rv818 on
fafba9ec, one commit79b2eac4Both reviews say READY with no MUST: rv848 (issuecomment-6240157020) and lane 2's second review of #848 (issuecomment-6240228302, "rv818" below). One commit on
fafba9ec, by explicit pathspec, fast-forward pushed. Tests only; no pinned file changes; main and #818 are not merged in. The rv818 numbers are next to rv848's.What each finding got
paste(row, cwd, None)now fails the walkon_diskis asked for"true"onlyADDERSholdson_disk-falseupload-packdocstringpaste_envsays what the rewrite adds:pack-objectsandindex-packconftest.pycount_of(env)case is added; theremotes_ofone is left, belowrv818 SHOULD 1.
test_adopt_reencoded_files.py'sattr.treeparam andtest_family_init_asks_every_value.py'suser.nameparam were built at collection, before the session fixture putgc.auto=0andmaintenance.auto=falsein the environment. Laid over the environment at the test, each dropped the two from git's command scope. Each is alambdaits test calls, astest_adopt_nested_attributes.pyalready did.New
tests/test_command_scopes_built_at_the_test.py(2 tests) reads each param off its test's own marks, builds it, and asks git for the command scope (git config --show-origin --list, read and never written). It asks for the session's two settings beside the param's entry. It fails onfafba9ec(the two test modules as they were, with the newconftest.py):2 failed, git read['user.name=', 'user.email=', 'attr.tree=4b825dc6...']and['user.name=', 'user.email=', 'user.name=Ada Lovelace'], with nogc.auto=0. It passes on the head.rv848 3 and rv818 6.
pastes_innow returns, for eachpastecall, the fixture it hands the remotes of:remotes_of(<name>)as the third argument or asremotes=, where the name isholderorscaffolded_hereand a parameter of the nearest function the call is in.None, another call, an expression on the call, a name the test does not take, or no third argument gives no fixture. The list of calls with none ispastes_without_remotes, a function of its own, so that a test of it can fail. 15 new cases hold the walk: twelve for what a call may hand and three for the function it is in.rv818 2.
counts_set_to_a_numberskips aDictthat is an operand of a comparison (assert added == {"GIT_CONFIG_COUNT": str(held + 1), ...},!=, a dict on the left). I took the narrower reading of "inside an assert or a comparison": a dict in a call inside an assert (assert run(env={"GIT_CONFIG_COUNT": "1"}).returncode == 0), or one assigned before the comparison, still sets a count and is still read. Four cases are still found, three are left, and thecount_of(env)case of rv818 5 is left.rv818 4.
JOB_ENTRIES,put_job_and_session_entries(monkeypatch),command_line_entries(listing)andcommand_scope_at(environ, cwd)inconftest.pyreplace thedelenv,held = ...and loop lines oftest_no_auto_maintenance.pyandtest_doctor_paste_through_git.py, and the four-line read of thecommand line:entries in three files.OWN_COUNTSfollows:conftest.pyholds two (the sum, and the job's entries counted from nothing),test_no_auto_maintenance.pyone, andtest_doctor_paste_through_git.pynone. A new test checks that both readers putGIT_CONFIG_PARAMETERSaside (rv818's own round-2 NIT 8).rv818 5, left.
remotes_ofreturningfixture["base"]instead offixture["base"] / "remotes"still survives, and I name it equivalent in practice: the prefix<base>/covers every remote under<base>/remotes, so all the pasted clones are rewritten either way.Merge checks (replace the body's section)
Re-run at the head
79b2eac4(git merge-tree --write-tree), against 152 heads: everyfix/*andl3-*branch, and the pull heads from #700. Mainfc867207is an ancestor of the head (it came in with #818's refresh4a553e17).fc867207: clean. Main is190133d5now (two commits onfamily.py, the README and the handbook): clean too.GIT_CONFIG_COUNTentries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) #818's head4a553e17: clean. The body's "one conflict,tests/test_non_utf8_git_reads.py" was true only forb9ad1187andeaa72d9e, before the refresh.Atlasno longer hides in step (4) (#777) #811 (050abea6), validate-family.py: the wrong-case move back mends.gitmodules, adds--sparseand moves an entry in the way aside, a row's staged removal and a url-less section are findings, and two refusals name a cure that works (#762) #820, family-root README.md and AGENTS-shape.md, and AGENTS.md: the holder's docs say its parent folder is named EXACTLY after its repository, case included, on every disk, and the never to hand-edit themembers:block names the row repairvalidate-family.pyasks for, whose commit is the person's #822, family.py add: the move back of a member's gitlink points its section of.gitmodulesback, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closingmvis guarded and leaves a checkout to delete by hand, and--sparsefollows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828 (ce0f2a5f), shape-doctor.py: a.gitmodulessection name keeps git's own bytes and is handed back to git as them, soconfigured_urlandkept_gitdirfind a member namedcaf\xe9, its detached commit gets its keep line under any locale, and a non-ASCII name no longer ends--rootin a UnicodeEncodeError traceback underLC_ALL=C PYTHONUTF8=0(#732) #774 (012a7726) and shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 (8c5ad0e8). family.pyadd: after #646's move back of a member's gitlink, a.gitmodulesthat agit mvmade name the variant is not mended, a file or link in the way has no remedy that keeps the member, and the plainmvit prints orphans a live submodule checkout #761, validate-family.py: the case-variant move back leaves.gitmodulesnaming the variant and has no--sparse, a staged un-registration passes the gate, and three findings print no remedy, one that dies, or a claim a recursive clone does not keep #762 and templates/family-root README.md and AGENTS-shape.md, AGENTS.md and validate-family.py: the holder's own docs say its parent folder is "named after the family" and omit "EXACTLY, case included", and "never hand-edit themembers:block" is contradicted by the validator's repair remedies #763 are the same heads as family.py add: the move back of a member's gitlink points its section of.gitmodulesback, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closingmvis guarded and leaves a checkout to delete by hand, and--sparsefollows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828, validate-family.py: the wrong-case move back mends.gitmodules, adds--sparseand moves an entry in the way aside, a row's staged removal and a url-less section are findings, and two refusals name a cure that works (#762) #820 and family-root README.md and AGENTS-shape.md, and AGENTS.md: the holder's docs say its parent folder is named EXACTLY after its repository, case included, on every disk, and the never to hand-edit themembers:block names the row repairvalidate-family.pyasks for, whose commit is the person's #822. The body said shape-doctor.py: a.gitmodulessection name keeps git's own bytes and is handed back to git as them, soconfigured_urlandkept_gitdirfind a member namedcaf\xe9, its detached commit gets its keep line under any locale, and a non-ASCII name no longer ends--rootin a UnicodeEncodeError traceback underLC_ALL=C PYTHONUTF8=0(#732) #774 and shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 conflict; they do not, since the refresh.--add-topicafter all three repositories are seeded, and the direct tools pass a name over 95 #838's pushed head9166f84f: clean.GIT_CONFIG_COUNTentries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) #818's head shows too (20 heads, in files this PR does not touch:tests/test_adopt_git_reads.py,tests/test_non_utf8_git_reads.py,tests/test_dry_run_parity.py,shape-doctor.py,scripts/shape_materialize.pyand others). tests: four tests,directory_namedand the file twin ask the one helper whether a name that is not UTF-8 can be made and skip on its two answers only, and three tests skip where an ASCII file name encoding cannot carry their text (#757) #806 (76ae6419) and tests: three tests ask the helper whether a name can be made,make_fileskips on EILSEQ or aUnicodeErroronly and the U+202E directory has no guard, so a full disk or a read-onlytmp_pathfails the test, not SKIPPED (#834) #845 (b18b243e, branchfix/834-oserror-skips) conflict intests/test_adopt_git_reads.py, which this PR does not touch.members/renamed withgit mvis renamed back in two steps through a free name, after what the disk lacks under it is put back, and a printed--sparsesays the git it needs, so no remedy fails as typed (#685) #767'sbd6d2afe(landed as98f14976, intests/test_family_add_spellings_remedies.py) and family.py init: the creator is asked whether a file can hold it whichever of--created-by, GIT_AUTHOR_NAME orgit config user.namegave it, git is asked about the first commit's author and committer before the remote exists, and a--familythat is a Windows device name is refused,--dry-runalike (#688) #809's98561405(landed as42aed788, intests/test_family_init_asks_every_value.py).GIT_CONFIG_COUNTappends go throughappended_to_command_scope, the scan names a count worked out by hand, andpaste()gives a pastedsubmodule updatetheinsteadOfrewrite (#835) #848, setup-project.py: a refused value names the check it failed, a project name over 95 characters is refused before step (4), and the prompt takes the spaces off a typed name, soAtlasno longer hides in step (4) (#777) #811 and scaffold-project.py, adopt-project.py and setup-project.py: nothing asks GitHub's topic and organisation limits, so an id over 39 characters fails--add-topicafter all three repositories are seeded, and the direct tools pass a name over 95 #838 lands later converts that count toappended_to_command_scope. setup-project.py: a refused value names the check it failed, a project name over 95 characters is refused before step (4), and the prompt takes the spaces off a typed name, soAtlasno longer hides in step (4) (#777) #811 (fix/777-typed-name-edges,050abea6) adds"GIT_CONFIG_COUNT": str(held + 1),attests/test_setup_project_name_edges.py:190: the scan finds[190]whereOWN_COUNTSallows none, sotest_no_test_sets_a_git_config_count_of_its_ownfails once tests: three more hand-workedGIT_CONFIG_COUNTappends go throughappended_to_command_scope, the scan names a count worked out by hand, andpaste()gives a pastedsubmodule updatetheinsteadOfrewrite (#835) #848 and setup-project.py: a refused value names the check it failed, a project name over 95 characters is refused before step (4), and the prompt takes the spaces off a typed name, soAtlasno longer hides in step (4) (#777) #811 are both on main. rv848 measured the same for scaffold-project.py, adopt-project.py and setup-project.py: nothing asks GitHub's topic and organisation limits, so an id over 39 characters fails--add-topicafter all three repositories are seeded, and the direct tools pass a name over 95 #838's local3845fd54attests/test_github_name_limits.py:503; the head scaffold-project.py, adopt-project.py and setup-project.py: nothing asks GitHub's topic and organisation limits, so an id over 39 characters fails--add-topicafter all three repositories are seeded, and the direct tools pass a name over 95 #838 has pushed,9166f84f, holds noGIT_CONFIG_COUNTin that file. It is the goal of tests: three moreGIT_CONFIG_COUNTappends worked out by hand escape #818's scan, andpaste()'sgit submodule update --initstill clones a local path with noinsteadOfrewrite #835, so it takes noOWN_COUNTSentry. No file of theirs is changed here.Known limits (add to the body)
Shapes the scan does not read, so a test that sets a count this way passes it:
format(),repr()oros.fsdecode()(str(...), an f-string, a.format(...)method and arithmetic are read);"2" if job else "1";setenv(name=..., value=...), with keywords;env["GIT_CONFIG_COUNT"]: str = ...;new = str(n + 1), then{"GIT_CONFIG_COUNT": new});GIT_CONFIG_KEY_{n}orGIT_CONFIG_VALUE_{n}name, which a hand-worked append writes beside its count;This replaces a separate issue for them.
Tests, through
run9.sh79b2eac4: 222 passed (test_command_scopes_built_at_the_test.py2,test_family_config_appends.py8,test_doctor_paste_through_git.py21,test_no_auto_maintenance.py60,test_adopt_reencoded_files.py53,test_family_init_asks_every_value.py78).test_doctor_legs_members_next.py127,test_clone_just_pushed.py19) and 253 passed (test_doctor_next_is_a_command.py,test_doctor_validator_rows.py,test_repo_hygiene.py,test_agents_md_codes.py,test_cli_reference.py).test_doctor_paste_through_git.py10,test_family_config_appends.py6,test_no_auto_maintenance.py51.fafba9ec: the command-scope test above,2 failed. The walk,on_disk-falseand the scan cases pass on the code offafba9ec, which is right; what they add shows in the mutants, where four survive the files offafba9ecand none survives these.Mutants, 29 in a throwaway clone at the head, each caught
on_diskignoring its value. Caught byon_disk-false(2 failures); survives thetest_family_config_appends.pyoffafba9ec.None(M01),remotes_of(tmp_path)orremotes_of(project): each caught by the walk of the real file, and each survives the walk offafba9ec. Eight mutants of the walk itself (theremotes=keyword, the parameter check, the remote-fixture check, the argument count, theremotes_ofname, the nearest-function rule, the call check, andpastes_without_remotesalways empty): each caught by its own case.test_command_scopes_built_at_the_test.py.count_of(env)case.GIT_CONFIG_PARAMETERS(1 each), the job's second entry another value (6).OWN_COUNTS:conftest.pyallowed one (2 failures), this file allowed two (1), no allowance fortest_family_config_appends.py(1).What changed
on_disk,with_configandtyped_with_local_remoteseach worked out theirGIT_CONFIG_COUNTfrom the environment's own count. Each is now one call ofappended_to_command_scope(env, [(key, value)]);typed_with_local_remotessets each key the helper returns, inside itsmonkeypatch.context(). The scan found a fourth:test_clone_just_pushed.py'senvforclone.defaultRemoteNamesummed its own count too, and goes through the helper.counts_set_to_a_numberfinds a count worked out in the same four places it finds one written out (a dict, a keyword, asetenv-style call, a subscript): astr(...)call, an f-string, a.format(...)call or arithmetic.OWN_COUNTSletsconftest.pyhold the one sum that remains, inappended_to_command_scope.paste(row, cwd, remotes=None)andpaste_env(remotes=None). Given the fixture's remotes, they appendinsteadof_entries(remotes)afterprotocol.file.allow, in the same count. Everypasteintest_doctor_legs_members_next.pyhandsremotes_of(<fixture>), and a walk of that file holds each call to it.Line 1: the three helpers
The issue says each site is right today, and it is: the held-two case below passes on the base. What the helper adds is an empty
GIT_CONFIG_COUNT, which git reads as no entries andappended_to_command_scopecounts as none (or 0). The sums written out by hand died onint("").tests/test_family_config_appends.py(6 tests) starts each helper underGIT_CONFIG_COUNT=2, the Windows job's two entries, and asksgit config --show-origin --list: both held entries and the new key, in order. It starts each under an empty count and checks the entry is the first.typed_with_local_remotesis asked from insidetyped_as_printed, and the environment is checked to be back as it was.Line 2: should the scan flag a count worked out by hand?
Yes. Measured by running the extended scan over every
tests/*.pyat the base: it finds exactly the three sites,test_clone_just_pushed.py:99, andconftest.py:290, the helper itself. No other file, so no false positive.str(...), an f-string,.format(...)and arithmetic are found. A value held in a name, read by subscript (added["GIT_CONFIG_COUNT"]) or returned by any other call (env.get(...)) is still not read, and so isint(env.get("GIT_CONFIG_COUNT", "0")), which sets nothing.is_a_written_countstill reads constants only; the newis_a_worked_out_countis the one that reads shapes, and both docstrings say so.test_the_scan_leaves_a_count_that_is_worked_outmoved itsworked-outanda-call(str(held)) cases to the found list, and is nowtest_the_scan_leaves_what_it_does_not_read, with the cases that are still not read.OWN_COUNTSgainsconftest.py: 1and the two new files (test_family_config_appends.py: 2,test_doctor_paste_through_git.py: 1), each a test that builds an environment on purpose. A second sum inconftest.py, or none, is named, as it is for every other file.test_the_scan_names_each_helper_that_worked_its_count_outholds the three helpers' old source, so the scan names the line of each.Line 3: does
pastestill want the rewrite?Yes. Measured on a pasted
git -C <root> submodule update --init --recursive --checkout -- codeof an empty mount, withGIT_TRACE:upload-packin the tracepack-objectsandindex-packin the traceorigin, and thesubmodule.code.urlthe root recordsAcross the whole of
test_doctor_legs_members_next.py(trace2 events of all 56sh -clines it runs), no pasted line ranpack-objects: every pasted clone was a local clone. A pasted fetch needs no rewrite (it asksupload-packwhatever the URL is), so the rewrite matters where a mount is cloned: 14 of the 31 tests that paste.The issue's Verify names
upload-packandpack-objectsfor theGIT_TRACErun.upload-packis in both columns, so the tests ask forpack-objectsandindex-pack, and for the storeassert_store_read_through_gitasks of every other clone.#645 keeps maintenance from starting, so nothing breaks today. #662 put every other read of these remotes through git as a second guard, and
pastewas the one left. The rewrite is for the directory resolved, which is how.gitmodulesrecords each url; no pasted line names a remote itself, sotyped=Trueis not needed.tests/test_doctor_paste_through_git.py(10 tests):pack-objectsandindex-packrun,assert_read_through_git,originequal to the url.gitmodulesrecords, noinsteadOfstored in the root or the clone;pack-objects;protocol.file.allow, then eachinsteadOfentry;pasteof the doctor file is given three arguments, and the walk's own five cases.Tests
Base
eaa72d9e, before: the three family files 117 passed;test_no_auto_maintenance.py,test_clone_just_pushed.pyandtest_repo_hygiene.py132 passed;test_doctor_legs_members_next.py127 passed.On the head
b9ad1187, throughrun9.sh(gh stub first,GH_TOKEN=bogus, emptyGH_CONFIG_DIR):test_family_head_lost_gitmodules.py,test_family_bootstrap_gate_order.py,test_family_add_spellings_remedies.py,test_family_config_appends.py,test_no_auto_maintenance.py,test_clone_just_pushed.py,test_repo_hygiene.py,test_agents_md_codes.py,test_cli_reference.py.test_doctor_paste_through_git.py,test_doctor_legs_members_next.py,test_validate_family_remedies_typed.py,test_doctor_next_is_a_command.py,test_doctor_validator_rows.py.New or changed tests: 31 new (6 + 10 + 15;
test_no_auto_maintenance.pygoes from 36 to 51 tests).Fail on the base, in a tree detached at
eaa72d9ewith the new test files copied in:on_disk,with_configand the typing helper (ValueError: invalid literal for int()), andtest_no_test_sets_a_git_config_count_of_its_ownnamestest_clone_just_pushed.py [99, 233],test_family_add_spellings_remedies.py [138],test_family_bootstrap_gate_order.py [136]andtest_family_head_lost_gitmodules.py [98];test_doctor_paste_through_git.py: ImportError (remotes_of), 10 not run; withpasteignoring the remotes (mutant 3a), the three pasted-clone tests fail.Mutants, 22 in a throwaway clone at the head, each caught:
test_no_test_sets_a_git_config_count_of_its_own, and the empty-count case; 1d the clone'senv: the same scan test; 1eon_diskkeeping a count of 1 held in a name, which the scan does not read: the held-two case;str,.format, arithmetic and f-string dropped fromis_a_worked_out_count; 2e-2h each of the four readers back to written counts; 2i-2k anOWN_COUNTSentry removed; 2z the scan as it was on the base;pastenot handingremoteson; 3bpaste_envdropping the rewrite; 3c the rewrite put beforeprotocol.file.allow; 3d one caller not handing the remotes (the walk); 3eremotes_ofnaming the wrong directory.Merge checks at
b9ad1187(git merge-tree --write-tree HEAD <ref>; superseded by the fix round's merge checks above)fc867207: one conflict,tests/test_non_utf8_git_reads.py, the one tests/test_doctor_legs_members_next.py and 24 other files: tests append what they tell git to theGIT_CONFIG_COUNTentries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) #818 has with main at its old head. It is the same with the baseeaa72d9e, and none of the files here conflicts.GIT_CONFIG_COUNTentries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) #818's head now,4a553e17(main merged in): clean.fix/*andl3-*branch, and the pull heads from shape-doctor.py:submodule_configandgitmodules_entryread a holder's.gitmodulesthrough onegit config -z --list, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor the mount url git andmake siblingsuse, under any locale (#668) #700): the conflicts they show are the same with the base, in files this commit does not touch (tests/test_non_utf8_git_reads.py,tests/test_adopt_git_reads.py,tests/test_dry_run_parity.pyand others). This commit adds one: family.py add: amembers/renamed withgit mvis renamed back in two steps through a free name, after what the disk lacks under it is put back, and a printed--sparsesays the git it needs, so no remedy fails as typed (#685) #767's stale PR headbd6d2afe, which landed as98f14976..gitmodules, adds--sparseand moves an entry in the way aside, a row's staged removal and a url-less section are findings, and two refusals name a cure that works (#762) #820, family-root README.md and AGENTS-shape.md, and AGENTS.md: the holder's docs say its parent folder is named EXACTLY after its repository, case included, on every disk, and the never to hand-edit themembers:block names the row repairvalidate-family.pyasks for, whose commit is the person's #822, family.py add: the move back of a member's gitlink points its section of.gitmodulesback, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closingmvis guarded and leaves a checkout to delete by hand, and--sparsefollows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828, tests/conftest.py: one helper builds theurl.<file URI>.insteadOfrewrite thatclone_just_pushed,read_through_gitand the adopt tests'pastedeach built, and the empty-mountsubmodule update --initsites use it (#662) #748, validate-family.py and validate-pins.py: a staged gitlink removal says to commit it,member-gitlink-absentputs the gitlink back by hand, the variantmvis guarded, and a pin row with nopath:isshape-pin-row(#681) #747, family.pyadd: after #646's move back of a member's gitlink, a.gitmodulesthat agit mvmade name the variant is not mended, a file or link in the way has no remedy that keeps the member, and the plainmvit prints orphans a live submodule checkout #761, validate-family.py: the case-variant move back leaves.gitmodulesnaming the variant and has no--sparse, a staged un-registration passes the gate, and three findings print no remedy, one that dies, or a claim a recursive clone does not keep #762 and templates/family-root README.md and AGENTS-shape.md, AGENTS.md and validate-family.py: the holder's own docs say its parent folder is "named after the family" and omit "EXACTLY, case included", and "never hand-edit themembers:block" is contradicted by the validator's repair remedies #763. shape-doctor.py: a.gitmodulessection name keeps git's own bytes and is handed back to git as them, soconfigured_urlandkept_gitdirfind a member namedcaf\xe9, its detached commit gets its keep line under any locale, and a non-ASCII name no longer ends--rootin a UnicodeEncodeError traceback underLC_ALL=C PYTHONUTF8=0(#732) #774 and shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 conflict only intests/test_non_utf8_git_reads.py, as they do with the base.Left out, and checklist candidates
GIT_CONFIG_KEY_{n}andGIT_CONFIG_VALUE_{n}names built from an index (an f-string) are not read, nor is a count held in a name and set later (new = str(n + 1), then{"GIT_CONFIG_COUNT": new}). A test that did that would pass the scan. Reading the indexed key names in the same four places would close it.typed=Trueis not used forpaste; the entries are for the resolved directory.Closes #835
🤖 Generated with Claude Code
Summary by Sourcery
Harden the test suite’s Git command-scope handling and pasted submodule coverage while detecting hand-computed configuration counts.
Enhancements:
insteadOfrewrites so submodule clones are read through Git.Tests: