Repository navigation
shape-doctor.py: a path that is not ASCII is printed as that path in every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming Projets/Équipe or a Latin-1 name runs as printed (#785) - #792
Conversation
…every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming `Projets/Équipe` or a Latin-1 name runs as printed and `--json` carries the same command (#785) `ascii_text` ended in `encode("ascii", "replace")`, and `quote_arg` and `Row` both ran it, so every character of a path that is not ASCII printed as `?` on every console, in the table and in `--json`: a keep line under `Projets/Équipe` named `Projets/?quipe`, which is not there. - `quote_arg` keeps an ASCII value's spelling byte for byte. A value that is not ASCII is quoted as itself where each such character is printable, is not typography `ascii_text` flattens, and is written by the report's encoding (stdout's, or `encoding=`) as the bytes the path holds; otherwise it is escaped whole: `$'...'` with three-digit octal escapes for `sh` (a `surrogateescape` byte as that byte), and a `('...' + [char]0x00C9 + '...')` expression for PowerShell. - `ascii_text` still flattens the typography, and replaces with `?` only a character the report cannot show (and U+FFFD, `?` since #385), so a UTF-8 terminal reads `café` in a sentence and an ASCII one still reads ASCII. - Docstrings of `shape_materialize.escape_what_stdout_cannot_encode` and `shape_advisory.printable`, which described the doctor's old flattening. - tests/test_doctor_non_ascii_paths.py: the spellings on every OS, and the members row's keep lines and NEXT, from the table and from `--json`, pasted into bash under a UTF-8 locale, `LC_ALL=C` and `LC_ALL=C PYTHONUTF8=0`, for a holder under a directory named in UTF-8 and in Latin-1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis PR makes shape-doctor preserve the identity of non-ASCII paths in generated commands: values are shown literally when the report can faithfully represent them and escaped as shell- or PowerShell-safe whole expressions otherwise, while ordinary report prose remains encoding-safe. Extensive tests validate quoting, JSON/table consistency, locale and legacy-byte paths, and pasted command round trips. Sequence diagram for encoding-safe command reportingsequenceDiagram
participant V as Validator
participant Q as quote_arg
participant E as Encoding checks
participant R as Row
participant O as Report
V->>Q: quote_arg(value, platform, encoding)
Q->>E: prints_as_itself(char, encoding)
E-->>Q: Show literally or escape
Q-->>V: Shell or PowerShell-safe argument
V->>R: Build next_command or WHY
R->>R: ascii_text(report text)
R-->>O: Table and JSON command
O-->>V: Command preserves path identity
Flow diagram for encoding-aware path quotingflowchart TD
A[quote_arg receives value] --> B{Value is ASCII?}
B -->|Yes| C[Existing ASCII quoting]
B -->|No| D[quoted_beyond_ascii]
D --> E{Every character prints as itself?}
E -->|Yes| F[Quote path as itself]
E -->|No| G{Windows?}
G -->|No| H[Emit sh $'...' byte escapes]
G -->|Yes| I[Emit PowerShell expression]
F --> J[Row ascii_text preserves quoted value]
H --> J
I --> J
C --> J
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-17T10:33:54Z) Independent review of #792 by RV-a (opus), on head 1. dash is not a regression. The members NEXT was run through each shell for 3 locales × UTF-8/Latin-1 parent × table/json:
2. Windows consoles.
3. ASCII paths are byte-identical to main. 24 doctor runs across three locales, with a plain parent and 4. Merges: clean with main and with all 17 open lane-3 PRs. On committed merges, #792's tests plus each PR's own pass:
SHOULDs:
NITs:
Mutants:
Runs:
Next: lane 3 takes S1-S3 with N1 and N3 in one round after 13:50Z. S1 makes #792 a README PR, so it lands in lane 3's README turn. Lane: openreposhape-3 (openRepoShape-3) |
…t's encoding is, and a name read in byte by byte under `LC_ALL=C` is read as the UTF-8 it spells (#785, review round 2) Review S2 and S3 on PR #792 (RV-a). What `--json` held followed stdout's encoding: `PYTHONIOENCODING=ascii` or `latin-1` gave `$'...\303\211...'` where `utf-8` gave `É`, though `json.dumps` writes ASCII whatever stdout is. - `REPORT_ENCODING`, which `main` sets to UTF-8 for `--json` before any row is built, and `printed_encoding` reads first: the table still follows stdout's encoding. - `quoted_beyond_ascii` reads a POSIX name again as UTF-8 through `surrogateescape`, so the two bytes `LC_ALL=C PYTHONUTF8=0` reads `é` in as are `é` again; a surrogate `surrogateescape` never makes is left, and escaped. A Latin-1 name is still `$'...'`. - tests: the members NEXT's `--root` with stdout in cp1252 and in ASCII (S3), `--json` holding the name as itself under all five (S2), dash running the lines that show a UTF-8 name as itself (N1), and the two new spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rint as itself is written `$'…'` off Windows, which bash, zsh and ksh read and dash (`/bin/sh` on Debian and Ubuntu) does not (#785, review round 2) Review S1 on PR #792 (RV-a): the sentence that printed commands are quoted for a POSIX shell did not say that the form #785 writes is not read by dash, and `sh -c` is dash on Debian and Ubuntu. - README.md, "Windows, natively": two lines after the pinned sentence, which `tests/test_readme_windows_quoting_sentence.py` still finds once; `tests/readme-cap.d/785.txt` raises the cap by those two. - docs/handbook.html: the same sentence in the Windows panel. The seven markers reading README § Starting a project in a new organisation move to its new blob. Of the seven sections that read it, only `start` (the Windows panel) says how printed commands are quoted; `masthead`, `partway`, `ci`, `legs`, `doublepin` and `park` say nothing the new sentence changes, so for them the marker alone moves. - AGENTS.md point 7 of "Checking": a `next` with `$'…'` needs bash, in the two lines the point had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-17T12:43:26Z) Re-verification of #792 round 2 by RV-a (opus), on head M1: under a legacy filesystem encoding, the re-decode names another path, in the table and in
Checked and holding:
NITs:
Runs, on the committed merge
Lane: openreposhape-3 (openRepoShape-3) |
…TF-8 it spells only where the joined name has the same bytes, so a UTF-8 directory under an EUC-JP, Big5 or EUC-KR filesystem encoding is never written as its twin (#785, review round 3) Review M1 on PR #792 (RV-a, round 2). Under a legacy multibyte filesystem encoding a directory named `日` in UTF-8 (`\xe6\x97\xa5`) is read in as three escaped bytes; round 2 joined them into `日`, which `path_bytes` writes as EUC-JP `\xc6\xfc`, so both the table and `--json` named another directory. - `quoted_beyond_ascii` keeps the joined text only where its `path_bytes` are the value's own (RV-a's measured fix), and its docstring no longer says the bytes are the same either way. - tests: the escaped UTF-8 bytes of `日` (EUC-JP, Big5) and `€` (EUC-KR), with `sys.getfilesystemencoding` patched, stay `$'...'` in the table and in `--json`; dash runs `--json` under all five stdouts (N1). - AGENTS.md point 7: a `next` with `$'…'` needs bash or zsh, not `sh` (N2), in the same two lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-17T12:55:11Z) Re-verification of #792 round 3 by RV-a (opus), on head 1. No name changes its bytes.
2. Nothing else changed. The 20-cell shells matrix is unchanged line for line from 3. The new test really exercises the check. 4. Only the expected lines moved:
Merge-tree is clean with main Runs:
Lane: openreposhape-3 (openRepoShape-3) |
…side #785's `$'…'` sentence (#785) README.md conflicted in one hunk, the "Windows, natively" paragraph: main's #630 rewording (the two-liner is the Windows way in that installs nothing, pipx's `openRepoShape` runs the same flow) is kept whole, and #785's two lines on `$'…'` follow it, as they followed the old text. docs/handbook.html conflicted in seven hunks, each one § Starting marker (masthead, start, partway, ci, legs, doublepin, park). The prose merged cleanly: #630's pipx text and #785's `$'…'` sentence stand together in `start`. The seven markers were recomputed from the merged README.md with tests/test_repo_hygiene.py's own readme_segments and readme_fingerprint (3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3), and handbook_marker_problems reports none. Every other marker is main's and still current. README.md is 1476 lines, at its cap: main's 1474 + 2 from tests/readme-cap.d/785.txt. AGENTS.md is 494 lines, at its cap of 494. Co-Authored-By: Claude Opus 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) (#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>
|
(lane openRepoShape-3, 2026-10-17T23:46:16Z) RV792m's re-check of #792's merge round ( Main has moved since, to RV792m re-check of #792 (#785) at head
|
… from paste_env(), the function #818 (#758) made of PASTE_ENV, so the file collects on main again #818 replaced the module-level dict `PASTE_ENV` in tests/test_doctor_legs_members_next.py with `paste_env()`, built at the call. This file still imported `PASTE_ENV`, so over any main since aec3b38 it failed to collect (ImportError) and pytest exited 2; #792's CI was green only because its run merged a main from before #818. `environ()` now builds from `paste_env().items()`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-18T00:24:44Z) RV792p's re-check of #792's fix round ( #792 asks for its README turn once RV792p re-check of #792 (#785) at head
|
| Files | Result line |
|---|---|
tests/test_doctor_non_ascii_paths.py |
126 passed in 76.02s (0:01:16) |
tests/test_no_auto_maintenance.py |
36 passed in 5.90s |
tests/test_stdout_encoding.py |
47 passed, 1 skipped in 11.85s |
| the 8 docs files | 331 passed in 42.22s |
- The one skip in
tests/test_stdout_encoding.pyistests/test_stdout_encoding.py:286: the default this is about is Windows' own. - The 8 docs files were
test_agents_md_codes.py,test_cli_reference.py,test_readme_sections.py,test_readme_windows_quoting_sentence.py,test_release_workflow.py,test_repo_hygiene.py,test_setup_by_hand_commands.pyandtest_update_add_codes_documented.py. - I did not run
tests/test_shape_doctor.py, as you said.
Marker check, using the predicted tree's tests/test_repo_hygiene.py helpers:
handbook_marker_problems(readme, page, README_HEADING_TO_HANDBOOK_ID, HANDBOOK_READS)returned[].- The ledger problems from
readme_cap_raiseswere[]. README.md is 1476 lines; the cap is 1422 + 2 (460.txt) + 5 (550.txt) + 12 (563.txt) + 22 (564.txt) + 10 (606.txt) + 1 (683.txt) + 2 (785.txt) + 1 (786.txt) = 1477. Headroom is 1 line.- Main alone is 1474 lines against a cap of 1475, also 1 line of headroom. shape-doctor.py: a path that is not ASCII is printed as that path in every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming
Projets/Équipeor a Latin-1 name runs as printed (#785) #792 adds 2 lines and its own785.txtraise of 2. - The 786.txt entry is the README-turn PR already on main.
Check 6: CI and the tree
- The predicted squash tree
4a4f1bf00e2e19108053c1c242b9bd1ca924e6c4is over main7b2105937567da30231db6a42749f258af48d9f9. gh pr checks 792 -R opensoft/openRepoShapeshowed this, read at the time:
| Check | State |
|---|---|
| SonarCloud Code Analysis | pass (1m11s) |
| package | pass (29s) |
| tests-git-bash | pass (2m16s) |
| tests | pending |
- The pending
testsjob is the Linux job, run39082233093, which I did not wait for.gh pr view 792showedheadRefOid4948c7a4, state OPEN,mergeableandmergeStateStatusUNKNOWN. origin/mainis 2 commits pastbee84d55:5fc049b7and7b210593. They merge into the branch cleanly in the prediction.
Scratch paths
- Clone:
/workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792p - Scratch root:
/workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792p-scratch - Predicted squash worktree:
.../RV792p-scratch/RV792p-wt - Worktree of
86d4c708:.../RV792p-scratch/RV792p-wt-merge - Probe worktree, holding the untracked probe test:
.../RV792p-scratch/RV792p-wt-probe - Probe scripts:
.../RV792p-scratch/RV792p-probe.pyand.../RV792p-scratch/RV792p-probe2.py - Stub
gh:.../RV792p-scratch/RV792p-stub/gh - Run scripts:
.../RV792p-scratch/RV792p-runA.shand.../RV792p-scratch/RV792p-runB.sh - Logs, all in
.../RV792p-scratch/:RV792p-nonascii.log,RV792p-nam.log,RV792p-stdout.log,RV792p-docs.log - Basetemps:
.../RV792p-scratch/bt-RV792p-* - I made no GitHub write and ran no
rm -rf. No test processes of mine are still running.
Lane: openreposhape-3 (openRepoShape-3)
|
|
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 3 days and 17 hours by commenting |
…book: under Git Bash (`MSYSTEM` set) the lines that follow the host are quoted for `sh`, through one question, `host_shell_dialect`, so the doctor's lines, setup's family lines and `family.py`'s `NEXT`, `switch -c`, `add` and `cd` lines paste whole in the shell that printed them, and README's Windows sentence says so (#711) (#821) The tools that quote a printed line for the host chose PowerShell's dialect wherever `os.name` was `nt`, and it is `nt` under Git Bash too, which runs the Windows Python. So Git Bash was handed `'C:\Users\Jane O''Neill'`, which it reads as `C:\Users\Jane ONeill`, a bare `C:\work\Atlas`, which it reads as `C:workAtlas`, and `Copy-Item` and `$env:PYTHONPATH = ...;`, which it has no command for. Brett Heap chose option (a) of #532's Git Bash question at 2026-10-16T13:07:34Z. - scripts/repo_shape.py: `host_shell_dialect(name=None, environ=None)` answers "nt" (PowerShell) on a Windows host whose environment has no `MSYSTEM`, or an empty one, and "posix" (`sh`) everywhere else, Git Bash and every MSYS2 shell included. `shell_quoted` and `shell_line` ask it when they are given no `platform`; an explicit one keeps its meaning. Its docstring names the limit: a PowerShell started from inside Git Bash inherits `MSYSTEM` and is answered `sh`'s. - setup-project.py carries a copy, as it does of the quoting, because it runs before any checkout of the standard exists. `SHELL_BARE`, decided at import from `os.name`, is gone: `shell_word` asks the question at each call and takes the alphabet and the escape from one answer, which the old code read twice. Setup's family lines follow it (L2-29). - shape-doctor.py: `quote_arg`, `restore_from_head`, `validator_command`, `bump_leg_command`, `checkout_notes`, `submodule_update_next` and `copy_command` ask it, so Git Bash gets `sh`'s quoting, `cp` and `env PYTHONPATH=...`. `scaffold_command` keeps its entry point and interpreter on the machine (`python setup-project.py` on Windows, from either shell) and takes only its quoting from the question. The interpreter is `PYTHON`, the machine's, on every line. - scripts/family.py is unchanged: its host-dialect lines go through `shell_quoted` and `shell_line`, `_print_next_steps`'s `cd` included. Tests: tests/test_git_bash_dialect.py emulates PowerShell, Git Bash, an empty `MSYSTEM` and POSIX on any host (each module under test gets an `os` whose `name` is the host's; the real `os.name` is never touched) and reads each line back with `shlex.split` or `powershell_words`: the question's answers, setup's copy held to them, `shell_quoted` and `shell_word` held to one answer on every host, and the lines of setup, family.py and the doctor. #724's inventory in tests/test_readme_windows_quoting_sentence.py now fails a function that quotes for the host and reads `os.name` itself. tests/test_family_printed_quoting.py reads the host through the question, and tests/test_setup_project_py.py unsets `MSYSTEM` where it patched the gone `SHELL_BARE`. Fix round 1 (rv711, no MUST): no pinned script may import `host_shell_dialect` (siblings.py's older-library test strips it too, and the skew test reads all seven pinned scripts that import from repo_shape); the guard counts the doctor's nine line builders, the functions that hand a `platform` on to a quoter, as quoters, so `copy_command(s, d, os.name)` in a new row fails it, and names a quoter's body by `Class.method`; the Git Bash rows read back a kept repository's line and a lane trailer spelled by the line's own quoter. A lane name cannot hold an apostrophe (`LANE_NAME_RE`), so that trailer test hands the quoter one through a stand-in. README and the handbook: README's Windows sentence keeps "Printed commands are quoted for a POSIX shell, so paste them into Git Bash", names `init`'s `cd` line among `family.py`'s, and says the exceptions are "quoted for the shell they are printed in, PowerShell or Git Bash, on Windows." #785's sentence after it says a path `shape-doctor.py` cannot print as itself is written `$'...'` "Off Windows and in Git Bash": its `quote_arg` now asks `host_shell_dialect` too, so Git Bash is handed `sh`'s spelling of such a path. The handbook's Windows panel says the same, and adds that a shell started from inside Git Bash inherits `MSYSTEM`, so a PowerShell started there is handed Git Bash's quoting, and a cmd.exe started anywhere else is handed PowerShell's. The seven markers of README's § Starting a project in a new organisation move to its new blob, e0d2b672, and tests/readme-cap.d/711.txt files the two lines this adds (README 1476 -> 1478, the cap 1477 -> 1479). NIT 6 of lane 2's review of #808, which landed first: the one-question guard's `calls_to` counts a quoter handed on as a value (`host_values`) as well as a call, so `map(shell_quoted, w)` beside a branch on `os.name` is seen. Stacked on #724, whose final head 949fd4d is merged here and which has landed as 2000488, merged here with main. #630, which rewrote the README and handbook around this sentence, has landed too (190133d), and so has #792 (#785, 5cbec22), which wrote into the same paragraph and into `quote_arg`; main d7befb9 is merged here with both. The copied `scripts/repo_shape.py` changes, so it rides in v1.1.1's shape bump. Closes #711 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #785 (comment)
Closes #785.
Lands after the v1.1.0 tag.
Where the
?came from, and why it was thereascii_textended intext.encode("ascii", "replace"). It is the original doctor's PURE ASCII rule (3d46ab5): "a verdict a cp1252 console cannot render is a verdict nobody reads".quote_argran it over every value before quoting it (32450eb, shape-doctor.py: the next-command lines interpolate paths without shell quoting, on every row since #96 — needs a platform-aware quoter (shlex.quote is POSIX-only; the file runs on Windows by contract) #101/shape-doctor.py: one platform-aware quoter, on every next command, so a root or a name with a space in it is a line that runs (#101) #102), so thatRow's second pass could not knock a curly apostrophe bare inside the quoting.Row.__init__ran it over every reason, NEXT and WHY.?, on every console and in--jsontoo. The flattening was there so that printing never raised. Since adopt-project.pyplanraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214,main()installsescape_what_stdout_cannot_encodefirst, so a print no longer raises. For a name, the replacement only named another path.What changed
quote_arg(shape-doctor.py:773, :897) andquoted_beyond_ascii(:917):prints_as_itself: it is printable, it is not typographyascii_textflattens, and the report's encoding writes it. That encoding is stdout's, or the newencoding=keyword. On POSIX the bytes written must also be the bytes the path holds (path_bytes, :731).sh, as$'...', every byte of such a character a three-digit octal escape. Asurrogateescapebyte (a name that is not UTF-8) is that byte. The apostrophe and backslash become\047and\134.('...' + [char]0x00C9 + '...')(powershell_expression, :948). The first operand is always a string, because[char] + [char]adds two numbers. Its verbatim strings double all five of PowerShell's single quotes (powershell_verbatim, :940, withrepo_shape.POWERSHELL_SINGLE_QUOTESimported at :428).ascii_text(:704), withFLATTENED(:675),printed_encoding(:679) andprints_as_itself(:685):?only where the report cannot show it: one the encoding cannot write, a control or bidi mark, or U+FFFD, which has printed as?since scripts/shape_advisory.pyrepository_namesand shape-doctor.pyorigin_urlandorigin_namereadgit remote get-url originthroughgit_outand catchRefusalandOSErroronly, so an origin URL that is not UTF-8 makesreadanswernot-answered(a single repository loses the Triad advisory, a leg clone its pointer to the assembly root) and endsshape-doctor.py --rootin a UnicodeDecodeError traceback, exit 1, where an ASCII origin gives NOT A SHAPE ROOT, exit 2 #385.caféin a sentence, and an ASCII one still reads ASCII. A valuequote_argspelled holds no character this pass changes.quote_arg's docstring (:831) and theRowcomment (:1002);scripts/shape_materialize.py:162(escape_what_stdout_cannot_encode) andscripts/shape_advisory.py:172(printable), which described the doctor's old flattening.--helpis unchanged.What a reader sees (measured on this branch)
These lines are for a holder under
Équipe, with its member detached and a commit made on it. Main printed?quipe(and??quipeunderLC_ALL=C PYTHONUTF8=0) in every one of them.LC_ALL=C(UTF-8 mode):python3 '/…/Équipe/InkRouter/scripts/bootstrap.py' --root '/…/Équipe/InkRouter'git -C '/…/Équipe/InkRouter/members/IRRS' branch NAMELC_ALL=C PYTHONUTF8=0:python3 $'/…/\303\211quipe/InkRouter/scripts/bootstrap.py' --root $'/…/\303\211quipe/InkRouter'\xc9quipe), under every locale:git $'--git-dir=/…/\311quipe/InkRouter/.git/modules/members/IRRS' branch NAME <commit>In round 1,
--jsoncarried the same command as the table. Since round 2 it is written for UTF-8 whatever stdout's encoding is (see Round 2): a character shown as itself there is written as a\uescape that decodes to it.Shells, and Windows
$'...'runs in bash, zsh, ksh and a POSIX.1-2024sh. dash 0.5.12, Ubuntu's/bin/sh, reads the$as itself. Measured on this host:!inside$'...';sh -cover ASCII paths and are unaffected, and the new ones paste with bash.'C:\Users\Équipe'). Only a stdout redirected to a file or a pipe is in the ANSI code page. There, a character outside that code page, a typography character or a control gets the[char]expression. No PowerShell is installed here, so that form is asserted as text and read back by the test's own reader. It was not run in PowerShell.$'O\342\200\231Neill'or('O' + [char]0x2019 + 'Neill'). It used to be flattened to', which named another path.test_a_curly_apostrophe_is_normalized_before_it_is_quotedbecametest_a_curly_apostrophe_is_escaped_where_the_row_cannot_reach_it(tests/test_shape_doctor.py:10101).Tests
New file, tests/test_doctor_non_ascii_paths.py (92 cases):
shand for PowerShell, asserted on every OS (:119, :124).Row's second pass leaves each spelling whole (:130).shspelling back into the path's bytes (:185).LC_ALL=Cand underLC_ALL=C PYTHONUTF8=0, for a holder under a directory named in UTF-8 and one named in Latin-1:git -C) and an emptied one (--git-dir), taken from the table's bytes and from--json, is pasted into bash and keeps the commit on a branch (:279);--rootis the root's own bytes, from the table and from--json(:308);78 failed, 14 passed(corrected in round 2). The 14 are the bash and zsh cases, which test the spellings alone.Changed: tests/test_shape_doctor.py:10101 (the curly-apostrophe test, as above) and the
_doctordocstring at tests/test_stdout_encoding.py:314.Runs. Each was made with a stub
ghfirst on PATH exiting 97,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR, on git 2.43.0 and Python 3.12.3:/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_doctor_gitmodules_not_utf8.py tests/test_doctor_legs_members_next.py tests/test_doctor_members_spelled.py tests/test_doctor_gitlink_repairs.py→
274 passed in 204.38s (0:03:24)/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_next_is_a_command.py tests/test_family_printed_quoting.py tests/test_stdout_encoding.py tests/test_update_shape_next_quoting.py tests/test_readme_windows_quoting_sentence.py tests/test_doctor_validator_verdicts.py tests/test_doctor_recorded_answers.py tests/test_commit_trailers.py tests/test_shape_advisory.py tests/test_repo_hygiene.py tests/test_cli_reference.py→
602 passed, 1 skipped in 157.06s (0:02:37)/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt -o tmp_path_retention_policy=failed tests/test_shape_doctor.py→
437 passed, 3 skipped, 1 xfailed in 319.47s (0:05:19)A member NAMED
café, and #774The issue's own example is a member whose
.gitmodulessection is namedcaféorcaf\xe9; its keep line names.git/modules/members/café. On main, finding that name is #732's (#774's) job: a Latin-1 name gets no keep line at all, andLC_ALL=C PYTHONUTF8=0ends in a traceback. So this PR tests the spelling through the holder's path, which every keep line, mount and kept repository sits under.On a scratch merge of this branch with #774's head
93b9bac3, I ran a 12-case check. It uses #774'smember_named,BYTE_NAMESandNAME_LOCALES, with each line taken from the table and from--json. All 12 passed, and each keep line pasted into bash kept the commit. On #774's head alone, all 12 failed. The check is not in this PR because its helpers are #774's. It can follow once #774 lands.Overlaps
See Round 2 for the overlaps at the head.
Pinned copies
None touched.
scripts/repo_shape.py, a pinned copy, still says inshell_quoted's docstring that the doctor flattens a value to ASCII. I left it, because any change to that file puts anupstream-changedrow in front of every project. It can ride the next change to that file.Left as it was
?where it cannot, as before. It is not escaped: a sentence is not pasted.quote_arg, such as a branch name, is printed as itself where the output can show it, and as?elsewhere, as before.rootandstandardheader lines and--json'srootare unchanged: adopt-project.pyplanraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214's handler prints them.Round 2 (RV-a's review, issuecomment-6232864874)
Two commits on
a1e2c9a8, pushed as a plain fast-forward to4338345c. Main is not merged in.S2:
--jsonholds a UTF-8 name as itself, whatever stdout's encoding is (174efe80). This is RV-a's sketch, with its two caveats taken.REPORT_ENCODING(shape-doctor.py:685):mainsets it to"utf-8"for--json(:10146), before any row is built, and to None for the table.printed_encoding(:688) reads it before stdout's encoding.json.dumpswrites ASCII whatever stdout is, and every character as an escape its reader decodes.quoted_beyond_ascii(:931, docstring :937) reads a POSIX name again as UTF-8 throughsurrogateescape(:949). UnderLC_ALL=C PYTHONUTF8=0,éis read in as two escaped bytes; they becomeéagain, andpath_bytesgives the same bytes either way.surrogateescapenever makes (outside U+DC80..U+DCFF) cannot be re-read like that. It is left as it is, and escaped, because it is not printable.--jsonand the table carry the same command.$'…'in both.S3 and N1: tests (tests/test_doctor_non_ascii_paths.py).
test_785_the_members_next_names_the_real_root(:343) runs underSTDOUTS(:225): the three locales, plus a UTF-8 locale withPYTHONIOENCODING=cp1252and withascii. That is 20 cells.test_785_json_holds_a_utf_8_name_as_itself_whatever_stdout_holds(:373) runs under the same five (S2).test_785_dash_runs_a_next_that_shows_the_name_as_itself(:358) runs dash on the UTF-8 name, under the UTF-8 locale andC, from the table and from--json(N1). It is skipped where there is no dash.utf-8-read-byte-by-byte(:87) andstray-surrogate(:91).next_root(:296), which the dash test shares.S1: the dash limit, documented (
4338345c).shape-doctor.pycannot print as itself is written$'…': bash, zsh and ksh read that; dash (/bin/shon Debian and Ubuntu) does not."tests/readme-cap.d/785.txtraises the cap by 2 (README is now 1461 lines).b0ab5750…. Onlystart(the Windows panel) says how printed commands are quoted. The other six (masthead,partway,ci,legs,doublepin,park) say nothing the sentence changes.nextwith$'…'needs bash." It stays inside the point's two lines, and AGENTS.md stays at 492.tests/test_readme_windows_quoting_sentence.pystill finds the pinned sentence once.Projets/Équipeor a Latin-1 name runs as printed (#785) #792 a README PR. It conflicts with README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 (l3-563-docs) in README.md (the same paragraph, which README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 rewraps) and in the handbook markers. It lands in lane 3's README turn, after README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 and validate.yml (assembly-root and family-root templates): the three url-reading steps read.gitmodulesby the rules the doctor andsiblings.pyread it by, so a section git ignores, a section with no path, apathorurlbeginning with-, an unmerged file or a file git refuses whole no longer puts a repository in the App token or the lists, and a refused file fails the step with its cause (#786) #790.N3. Round 1's count against main's shape-doctor.py is corrected above to
78 failed, 14 passed, as RV-a measured. Round 2's file gives99 failed, 18 passed; the 18 are the bash and zsh spelling cases. The #791 note is gone: #791 is clean.Mutants. Each was run on tests/test_doctor_non_ascii_paths.py and tests/test_stdout_encoding.py:
path_bytescheck droppedcp1252-escapedspelling twice, and the cp1252-stdout table--rootcell--jsonunder all five stdoutsprinted_encodingignores stdoutC-ascii-fstable--rootcells (S3), 2 keep lines and the cure underC-ascii-fsmainleavesREPORT_ENCODINGunset (--jsonfollows stdout)--jsonunderC-ascii-fs, cp1252 and ascii (S2)surrogateescapere-decodeutf-8-read-byte-by-bytetwice,--jsonunderC-ascii-fsstray-surrogate(aUnicodeEncodeError)Runs on
4338345c. Each was made with a stubghfirst on PATH exiting 97,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py tests/test_doctor_next_is_a_command.py tests/test_doctor_validator_rows.py tests/test_doctor_validator_verdicts.py tests/test_doctor_validator_site.py tests/test_doctor_members_spelled.py tests/test_doctor_legs_members_next.py tests/test_doctor_gitlink_repairs.py tests/test_doctor_recorded_answers.py tests/test_family_printed_quoting.py tests/test_readme_windows_quoting_sentence.py tests/test_update_shape_next_quoting.py tests/test_shape_advisory.py tests/test_doctor_gitmodules_not_utf8.py tests/test_cli_reference.py tests/test_repo_hygiene.py tests/test_validator_child_encoding.py→
862 passed, 1 skipped in 763.71s (0:12:43)… tests/test_repo_hygiene.py tests/test_readme_windows_quoting_sentence.py tests/test_readme_sections.py tests/test_agents_md_codes.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py→
214 passed in 4.56s… tests/test_shape_doctor.py(once) →437 passed, 3 skipped, 1 xfailed in 540.84s (0:09:00)Overlaps at
4338345c.git merge-treeis clean with main (20004887) and with every open lane-3 PR (#802, #794, #791, #790, #789, #788, #782, #781, #780, #779, #778, #775, #774, #773, #772, #771, #770, #769) but #630, which conflicts in README.md and docs/handbook.html as above.Left as RV-a said:
scripts/repo_shape.pydocstring.Round 3 (RV-a's re-verification, issuecomment-6234017777)
One commit,
26d61255, pushed as a plain fast-forward on4338345c. Main is not merged in.M1: under a legacy multibyte filesystem encoding, the re-decode named another directory.
日in UTF-8 (\xe6\x97\xa5) is read in as three escaped bytes. Round 2 joined them into日, andpath_byteswrote that as EUC-JP\xc6\xfc, the twin directory, in the table and in--json.quoted_beyond_ascii(shape-doctor.py:957-963) keeps the joined text only where itspath_bytesequal the value's own. This is RV-a's measured fix,5bac3ea1.test_785_a_utf_8_name_under_a_legacy_filesystem_keeps_its_bytes(tests/test_doctor_non_ascii_paths.py:144, casesLEGACYat :137) patchessys.getfilesystemencoding. It checks that the escaped UTF-8 bytes of日under EUC-JP and Big5, and of€under EUC-KR, stay$'\346\227\245'(or$'\342\202\254'). It checks the table, written in that encoding, and--json, in UTF-8. That is 6 cases.text = joinedalways, which is round 2's code):6 failed, 167 passed, 1 skipped. All 6 are the new cases. The head passes all 6.N1.
test_785_dash_runs_a_next_that_shows_the_name_as_itself(:392) now runs overDASH_CELLS(:384): the table under the UTF-8 locale andC, and--jsonunder all five stdouts. That is 7 cells, and all pass in dash.N2. AGENTS.md:268-269: "each row with its
id, to act on a row. Anextwith$'…'needs bash or zsh, notsh." This is in the same two lines, and AGENTS.md stays at 492.Runs on
26d61255. Each was made with a stubghfirst on PATH exiting 97,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py→
173 passed, 1 skipped in 69.67s (0:01:09)… tests/test_repo_hygiene.py tests/test_readme_windows_quoting_sentence.py tests/test_readme_sections.py tests/test_agents_md_codes.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py→
214 passed in 4.09sMerge round (main 2e476a7)
One merge commit,
1ccb3dd7, on26d61255: parents26d61255(this branch) and2e476a77(main, after #804). It was pushed as a plain fast-forward. It brings in #630's v1.1.0 docs (190133d5) and everything else main landed sincebf1f099b. Lane 2's #821 is not on main and none of its changes are taken; lane 2 resolves on #821's side (#473 issuecomment-6234162871).Conflicts. Two files, as the planner predicted: one README hunk and seven handbook marker hunks.
pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 rewrote it (the two-liner is the Windows way in that installs nothing, pipx'sopenRepoShaperuns the same flow, Git Bash runs step 3's bash command). Main's paragraph is kept whole, and this branch's two lines on$'…'follow it, as they followed the old text (README.md:162-163).masthead(:475),start(:615),partway(:1092),ci(:1129),legs(:1191),doublepin(:1436) andpark(:1735). Each took main's line and was then recomputed (below). The prose merged without a conflict:start's Windows panel carries README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630's pipx text and this branch's$'…'sentence together (:741-743).Markers. The seven § Starting a project in a new organisation markers were recomputed from the merged README.md with
tests/test_repo_hygiene.py's ownreadme_segmentsandreadme_fingerprint, to blob3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3.handbook_marker_problemsreports none, and 23 of 23 markers are current. Every other marker is main's, unchanged.Cap. README.md is 1476 lines, at its cap: 1422 + the ledger (460: +2, 550: +5, 563: +12, 564: +22, 606: +10, 683: +1, 785: +2) = 1476. That is main's 1474 plus this branch's 2, and
tests/readme-cap.d/785.txtstays at +2. AGENTS.md is 494 lines, at main's cap of 494; #630 raised it from 492, so round 3's "stays at 492" was measured before that.The delta is the reviewed one. Outside README.md, the handbook and
tests/readme-cap.d/, the-U0changed lines ofgit diff origin/main HEADequal those of the reviewedgit diff bf1f099b 26d61255: 772 lines each,cmpidentical. The cap file is byte-identical too. In README.md and the handbook, the prose lines are the same; only the seven marker hashes differ.Runs on
1ccb3dd7. Each was made with a stubghfirst on PATH exiting 97,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt-docs -o tmp_path_retention_policy=failed tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_readme_sections.py tests/test_readme_windows_quoting_sentence.py tests/test_release_workflow.py tests/test_repo_hygiene.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py→
300 passed in 41.67s… --basetemp=<W>-scratch/bt-gate …on shape-doctor.py: a path that is not ASCII is printed as that path in every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line namingProjets/Équipeor a Latin-1 name runs as printed (#785) #792's gate set, 22 files (tests/test_shape_doctor.pyonce, inside it):test_doctor_non_ascii_paths,test_shape_doctor,test_readme_sections,test_setup_by_hand_commands,test_readme_windows_quoting_sentence,test_update_add_codes_documented,test_doctor_legs_members_next,test_doctor_next_is_a_command,test_doctor_validator_rows,test_doctor_validator_verdicts,test_doctor_recorded_answers,test_doctor_gitmodules_not_utf8,test_doctor_gitlink_repairs,test_doctor_plan_overrides,test_doctor_pin_restore_merges,test_doctor_partial_clone,test_doctor_validator_site,test_doctor_unfinished_merge,test_doctor_members_spelled,test_doctor_machine_row,test_shape_advisory,test_validator_child_encoding→
1206 passed, 3 skipped, 1 xfailed in 2071.76s (0:34:31)scripts/render-cli-reference.py --check→[ok] docs/cli.md is what the generator producesgit diff --check origin/main...HEAD→ no output, exit 0Main moved during the gate run, to
c100843b: #752's conflicted-pin NEXT inshape-doctor.py,677f9bfa's quoting-sentence inventory, and2c6543e8'srelease/v<major>.<minor>push runs. None touches README.md, the handbook, AGENTS.md or the cap ledger, andgit merge-treeof this head withc100843bis clean. A predicted merge, built locally withmerge-treeandcommit-tree(82c944ab, never pushed), passes the files main changed plus this branch's own and the docs checks:tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_doctor_pin_restore_merges.py tests/test_validator_child_encoding.py tests/test_release_workflow.py tests/test_workflow_concurrency.py tests/test_readme_sections.py tests/test_repo_hygiene.py→572 passed, 1 skipped, 1 xfailed in 334.47s (0:05:34).🤖 Generated with Claude Code
Summary by Sourcery
Make shape-doctor print non-ASCII paths faithfully or escape them safely so generated commands continue to target the correct files across output encodings and shells.
New Features:
Bug Fixes:
Enhancements:
Documentation:
$'…'quoting used for non-ASCII paths and its bash, zsh, and ksh compatibility limitations with dash.Tests: