Repository navigation
adopt-project.py: a refused command's REFUSED block spells each character a terminal acts on in the command, its directory and what it printed, as _first_said spells gh's, so --local-remote-dir no longer reaches the terminal raw there; a NOTE before NEXT: says when the source's path in its clone command holds a character a terminal does not show as itself; and rows pin six sites #1177's 148 left to chance (#1195) - #1225
brettheap wants to merge 1 commit into
Conversation
…cter a terminal acts on in the command, its directory and what it printed, as `_first_said` spells gh's, so `--local-remote-dir` no longer reaches the terminal raw there; a NOTE before `NEXT:` says when the source's path in its clone command holds a character a terminal does not show as itself; and rows pin six sites #1177's 148 left to chance (#1195) Follow-up of #1177 (for #1152), from RV1177's review: its N3, N4 and the surviving mutants K1, K2, K4, K5, K6 and K7. Stacked on #1177's branch, and lands after it. - N3: `_loudly` builds `CommandFailed.loudly`'s block from the command's arguments, its directory and its output, each `_unacted`: a character `str.isprintable` refuses is spelled as `_spelled_character` spells it, with no backticks, and the output keeps its line breaks. The leg push, `_failed_loudly` and `main` say a refused command through it. - N4: a source path that `_refuse_an_unwritable_clone_url` lets through (#255) but `str.isprintable` refuses, as U+200B and U+200F, gets a NOTE just before `NEXT:` naming it `_spelled`. The clone command stays as `sh` reads it. Refusing such a source would undo #255's ruling that a `Cf` character other than the nine bidi controls is text a file holds. The NOTE is printed before the block, so it touches none of the lines that #1121 and #1207 add after it. - Rows: a `--plan` ending in a space, a relative one between backticks, a relative one and `./plan.yaml` (K1, K2); a PLACEMENT plan's `plan-wrong-kind` and `adopt-org-too-long` with the plan's path spelled (K4, K5); a printable `--local-remote-dir` outside ASCII (K6); the GitHub-mode `split` line matched in full (K7); and N3's and N4's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR hardens adopt-project diagnostics against terminal-confusing characters by spelling unsafe content in REFUSED blocks, warns before pasteable clone commands containing visually hidden characters, and adds broad regression and mutant-killing coverage for these behaviors and related path-rendering cases. Sequence diagram for sanitized adopt-project command refusalssequenceDiagram
participant AdoptProject
participant GitOrGh
participant Terminal
AdoptProject->>GitOrGh: run command
GitOrGh-->>AdoptProject: CommandFailed(args_list, cwd, output)
AdoptProject->>AdoptProject: _loudly(exc, step)
AdoptProject->>AdoptProject: _unacted(args_list, cwd, output)
AdoptProject->>AdoptProject: _spelled_character(unsafe_characters)
AdoptProject->>Terminal: print REFUSED block with sanitized command, directory, and output
Sequence diagram for hidden-character clone command warningsequenceDiagram
participant AdoptProject
participant Terminal
AdoptProject->>AdoptProject: _next_steps(...)
alt clone URL is not printable
AdoptProject->>AdoptProject: _spelled(url)
AdoptProject->>Terminal: print NOTE before NEXT:
else clone URL is printable
AdoptProject->>Terminal: omit NOTE
end
AdoptProject->>Terminal: print pasteable NEXT: clone command
Flow diagram for safe rendering of adopt-project outputflowchart TD
A[Command or clone path contains characters] --> B{str.isprintable}
B -->|Printable| C[Render character as typed]
B -->|Not printable in refused output| D[_unacted]
D --> E[_spelled_character]
E --> F[Sanitized REFUSED block]
B -->|Not printable in NEXT clone path| G[Print NOTE with _spelled path]
G --> H[Keep quoted clone command unchanged]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
RV1225: independent review of #1225 at Independent adversarial review for lane 4. No MUST and no SHOULD. There are four NITs, below. Non-ASCII characters are written as escapes here. Merge with current main
N3: every REFUSED block a refused command prints
Probes, a real
N4: the NOTE before
|
| Parent holds | NOTE | Raw character in stdout |
|---|---|---|
| U+200B | one line, just before NEXT:, naming `<D>/so\u200burce/Thing` |
once, inside the quoted clone command |
| U+200F | the same, `<D>/so\u200furce/Thing` |
once, inside the quoted clone command |
| U+00E9 (printable) | none | as typed, everywhere |
| U+3164 (printable, default-ignorable) | none | in split and NEXT: (the body's #1165 note) |
| nothing | none | - |
- Clear, and the right path. The NOTE names
urls['assembly'], the url the clone command quotes, spelled as thesplitline spells it, and says "the clone command below", which matches where it is. BeforeNEXT:it is read before the command a person pastes, and it stays out of the bootstrap NOTEs that main, adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 and adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #1207 print after the block. I would keep it there. - Where the raw character still reaches the terminal. Only inside the single-quoted clone command, which must paste as it is (
_branch_deletion's rule). U+200B and U+200F are not terminal controls; the nine bidi controls and everyCcare refused before any leg (adopt-unwritable-value). - Placement is pinned. My mutant R11 (the NOTE after the block) fails rows, below.
Mutants
Each on adopt-project.py in its own worktree at 73c9f57e, run against the whole new file (188 rows; with tests/test_adopt_github_exits.py for K7, 211), restored with git checkout, sha256 163c692a... after every restore. K1 to K7 are RV1177's exact strings (rv1177/mutants.py). "Old rows" are the 148 ids collected at 5208e874e; a K mutant may fail only rows outside them.
| Mutant | What it does | Result |
|---|---|---|
| K1 | check's last count line names --plan by isprintable() alone |
killed: 2 of 188, both new (ends-in-a-space, relative-between-backticks) |
| K2 | that line names plan.path.absolute() |
killed: 3 of 188, all new (the three relative forms) |
| K4 | the PLACEMENT plan's plan-wrong-kind names the plan raw |
killed: 8 of 188, all new (plan-wrong-kind-a-placement-plan, check and execute) |
| K5 | adopt-org-too-long names the plan raw |
killed: 8 of 188, all new (org-too-long, check and execute) |
| K6 | the extracted leg line spells every url outside ASCII | killed: 1 of 188, new (printable-U+00E9) |
| K7 | split names source.path, not urls['assembly'] |
killed: 2 of 211, gh-pr-create-refused and the-topic-will-not-set of tests/test_adopt_github_exits.py's row whose split assertion this PR tightens |
| R2 | _loudly spells the directory only when it is a str (a Path stays raw) |
killed: 2 (the real refused push, U+202E and ESC) |
| R4 | _loudly leaves the output raw |
killed: 8 |
| R5 | _loudly keeps a line feed raw in the arguments |
killed: 2 (the two in-process line-feed rows) |
| R6 | the leg push prints exc.loudly again |
killed: 2 |
| R7 | _failed_loudly returns exc.loudly again |
killed: 4 |
| R8 | main prints exc.loudly again |
killed: 4 |
| R9 | _loudly drops the (in <cwd>) line |
killed: 14 |
| R11 | the NOTE prints after the NEXT: block (the writer's M14, re-made) |
killed: 2 (zero-width-space, right-to-left-mark) |
| R12 | the NOTE goes to stderr | killed: 2 |
| R14 | no NOTE | killed: 2 |
| R13 | the NOTE wherever _shown spells the url (the writer's M11) |
killed: 1 (name-ends-in-a-space) |
| R1 | _unacted spells every character outside ASCII, printable or not |
survives: 188 passed (N2) |
| R3 | _unacted keeps a tab raw in what the command printed |
survives: 188 passed (N2) |
| R10 | the NOTE fires for any url outside ASCII, so a caf\u00e9 source gets one |
survives: 188 passed (N2) |
- The old 148 stay green under every K mutant. Each K mutant failed only ids that are not among the 148 collected at
5208e874e; K7's two are intests/test_adopt_github_exits.py. - K4's and K5's expected lines, from real runs on the merge (
--plan <D>/pl\u202e<name>.yaml, no raw U+202E in either stream):REFUSED plan-wrong-kind: `<D>/pl\u202eplacement.yaml`: kind is 'placement-plan', expected 'adoption-plan'fromcheckandexecute(exit 2);FINDING adopt-org-too-long: `<D>/pl\u202elong-org.yaml`: `org:` is oooooooooooo...oooooooooooo, 40 characters, and ...fromcheck(exit 1) and the same withREFUSEDfromexecute(exit 2). Both match what tests/test_adopt_plan_and_remote_paths_spelled.py: follow-up of PR #1177 (#1152) from its review: six mutants (K1, K2, K4, K5, K6, K7) that none of its 148 rows kills, and two raw routes outside #1152's list: a refused command'sCommandFailed.loudlystill names--local-remote-dirraw, andNEXT:prints U+200B or U+200F raw #1195 read from the code.
Gate, on 43af5516c (targeted files only, sharded, -p no:cacheprovider, retention=failed)
| Shards | Ids | Result |
|---|---|---|
g0 to g5: 59 files, the 54 tests/test_adopt*.py files that print a REFUSED block, NEXT: or loudly (both changed files among them), test_repo_hygiene.py, test_path_guard_hygiene.py, test_stdout_encoding.py, test_non_utf8_name_callers.py and test_doctor_next_is_a_command.py |
4553 | 4545 passed, 8 skipped; g0 758+1, g1 758+1, g2 758+1, g3 757+2, g4 758+1, g5 756+2 |
d0 to d7: tests/test_shape_doctor.py, from a bash script with mapfile, 25 ids holding spaces, no timeout under 600 s |
449 | 446 passed, 3 skipped; 57, 56, 56, 55+1, 55+1, 55+1, 56, 56 |
- Every shard exited 0, and each set of shard lists held every collected id exactly once.
- The 11 skips are environment skips: three reftable rows (this bench has git 2.43), two
test_adopt_eol_block_through_links.pyrows,SHAPE_MEDXEHR_CLONEunset, a case-insensitive-disk row,test_stdout_encoding.py's Windows-default row, and the three Windows-only doctor rows.
Merge-trees
git merge-tree --write-tree of 73c9f57e against #1177's head 5208e874e: exit 0. Against origin/main 50ce8ab15: exit 0. Against all 24 other open PRs that touch adopt-project.py, run from this head, from the scratch merge and from origin/main alone (heads as fetched at 20:32Z):
- From this head: exit 0 for all 24. They are adopt-project.py:
checkandexecuteread one list of a plan's problems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), socheck's first finding isexecute's refusal for each pair (#1113) #121090eca4144, adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #12078b499842f, adopt-project.py:_pin_row_problemsays a pin field whose value is not text as_read_assays what the plan's reader read, "recordscommit:as the number 7", where it spelled it as Python does,commit: 7andTrue, socheckandexecutename it in the words the plan's other refusals use under the sameplan-pin-malformed(#1114) #1198d38e4e36d, adopt-project.py:_github_namereads no owner over 39 characters or repository over 100, soplan --source <50 b's>/Thingnever asksghfor a name GitHub cannot hold, no remedy offers--org <50 b's>, andcheckandexecutenever clone such a record again (#1129) #1194900f77af9, adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #11907f3e12802, adopt-project.py:checknames the case twins the plan keeps in one repository asplan-case-twinson every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, andexecuterefuses the finding before any leg (#1161) #118958aa9d2ba, adopt-project.py: a--source, or a plan'ssource.local_path, with a part of its repository git reads at mode 000 (the.git, itsHEAD,objects,refsorconfig, the git directory agitdir:line names, or a directory above it) is refusedsource-unreadablenaming that part, nevergit-failedand never cloned again (#1151) #118607486a498, adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180b18d2e8dc, adopt-project.py:plan's table pads each name by the columns a terminal gives it, not the code points it holds, so a wide or combining name'slegcolumn lines up underlegwith every other row's (#1034) #1178c4beba193, adopt-project.py and repo_shape.py: a name that holds a default-ignorable character, which a terminal draws as nothing, is spelled by its code point inplan's lines and in theunsafe-valuerefusal, so a branchma<U+034F>inno longer prints asmain(#1128) #11718162b874e, adopt-project.py:checknames each case twin at the shape's copy, above it and at the manifest, before a link there or after it, and at a pin's file whose exact spelling holds nothing, as_KeptTreekeeps every row, not the first (#1138) #117024ee133aa, adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #116976c9b7675, adopt-project.py:plan written toandcloning <repository> (read only) intoname the caller's--outand--work-dirwith_shown, as the other lines of the report name a path, so a bidi control, a line feed or a tab in either no longer reaches the terminal as it was typed (#1115) #11548a7828332, adopt-project.py: under a one-byte locale such as Latin-1, a clonecheckmakes again for a plan's clone record that neither spelling names is compared with both, so it is kept and the next run reads it rather than cloning again (#1112) #1153ff33ba12a, adopt-project.py: a--work-dirlink into a loop that Python 3.12 reads past a link that does not loop names the loop, Windows' winerror 1921 counts as one, and a path read as the loop's link says so, with tests for r1065's mutants (#1074) #11437af5fabc3, adopt-project.py: a recorded source whose.gitthis user cannot read is refusedsource-unreadable, naming that.git, socheckandexecuteno longer call a cloneplanmade with its.gitat mode 000 no longer whole and clone it again (#1099) #11417a2711d03, adopt-project.py:checkandexecuteask a plan'sorg:andid:right after its three repository names, so a bad one beside an unanswered entry, a moved source, a refused name or another repository is that one refusal in both, and apins:entry that is no pin is said as_read_assays it (#1020) #11259549006fe, README.md, adopt-project.py and the handbook: a kept directory entrymakefile/is tested, README saysmakeat the root reads a keptGNUmakefileormakefileinstead, and a docstring says the shape's Makefile alone holdsCONTRACTS_DIR(#1061) #11235c3908f8d, adopt-project.py: anorg/repo--sourcetoo long for the disk is refused as a path is, naming no GitHub repository and offering no url, so--source <300 a's>/Thingis no longer offeredhttps://github.com/<300 a's>/Thing, which can never clone (#1003) #1122c01c4d73d, adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #11211c5173758, adopt-project.py:plan'ssourceline names the source's branch as_shownnames a path, so a bidi control, a line boundary or a byte that is not UTF-8 in it is spelled and no longer reaches the terminal raw (#1035) #1120c55e55e33, adopt-project.py, shape_materialize.py and tests: a named bad link left out of the copy and pin findings takes no other path's finding with it, and the docstrings and the materializer's refusal name the #979 exception and what helps it (#1089) #11182307036ab, adopt-project.py: under a one-byte locale such as Latin-1,checkreads a plan written in UTF-8 whose source is there, asking the plan's UTF-8 bytes where the spelling as written names nothing, rather than refusingplan-source-gone(#1078) #1106504a1dd65and shape_materialize.py and the pinned AGENTS-shape.md: where the root keeps aGNUmakefileor amakefile, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file saysmakereads the first of the three and the block reaches it only once that file holds it (#1048) #1105b28f40de0. - From the scratch merge: 22 clean. adopt-project.py:
check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169 and adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #1190 conflict, and they conflict withorigin/mainalone in the same files with the same number of hunks: adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169 6 inadopt-project.pyand 13 intests/test_adopt_check_throwaway_repos.py, adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #1190 8 in that test file. This PR adds no conflict. adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180 now merges cleanly with main. - adopt-project.py:
execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 and adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #1207 aroundNEXT:. Both add lines right after theNEXT:print and in the bootstrap NOTE branches; this PR's NOTE comes before the print, in a separate hunk, and both merge cleanly.
Findings
- N1 (NIT), a line feed in a path git echoes still splits the output block.
- What I measured.
--local-remote-dir <D>/re\nmotewith the refusing hook: the command line saysre\nmote/Thing-spec.git(and with the line feed in--work-dirtoo, the(in ...)line sayswo\nrk), but under--- output ---git's own lines readTo <D>/re, thenmote/Thing-spec.git, anderror: failed to push some refs to '<D>/re, thenmote/Thing-spec.git'. So a path can still put lines of its own into the block.kept="\n"is what tests/test_adopt_plan_and_remote_paths_spelled.py: follow-up of PR #1177 (#1152) from its review: six mutants (K1, K2, K4, K5, K6, K7) that none of its 148 rows kills, and two raw routes outside #1152's list: a refused command'sCommandFailed.loudlystill names--local-remote-dirraw, andNEXT:prints U+200B or U+200F raw #1195 asked for, and the new e2e row leaves the line feed out, so nothing pins either choice. - Fix: in
_loudly, before_unacted(exc.output, kept="\n"), replace in the output each argument and the directory that holds a line break with its_unactedspelling, and addline-feedback toREFUSED_CHARACTERS. Or keep the behaviour, and add a row for it and a sentence to_loudly's docstring saying that a line break in a path git echoes reads as one in the block.
- What I measured.
- N2 (NIT), three new survivors of the K6 class. Each leaves behaviour this head gets right, measured above, but no row holds it:
- R1.
_unactedspelling every character outside ASCII passes all 188 rows: no row puts a printable non-ASCII character in a refused command, so the body's "a printable character prints as it is" is unpinned. Fix: addpytest.param("\u00e9", "\u00e9", id="printable-U+00E9")to the parameters oftest_a_late_refused_command_is_said_spelledandtest_a_refused_command_no_step_said_is_said_spelled(the_block_ofthen expects it as typed). - R3.
_unactedkeeping a tab raw in the output passes all 188;_first_saidspells a tab. Fix: givea_refused_push's output a tab (for example" ! [remote rejected]\t(pre-receive hook declined)"), whichthe_block_ofexpects as\t. - R10. The NOTE keyed on
url.isascii()passes all 188, so acaf\u00e9source would be told its path "holds a character a terminal does not show as itself". Fix: add aprintable-U+00E9source toSPLIT_SOURCES(notedFalse), withnamedas it is for it, aswhere_a_leg_isdoes forspelled=None.
- R1.
- N3 (NIT), the spelling is not reversible.
_unactedleaves a backslash as it is, so a path that holds the six characters\u202eand one that holds U+202E print the same line, under a header that saysTHE EXACT COMMAND WAS:. A Windows path such asC:\x1b\...reads like a spelled ESC._first_saidshares this, and nothing is dropped. Fix: one sentence in_loudly's docstring saying a spelled character and a typed backslash sequence read alike; no code change. - N4 (NIT), one raw route the body's "Not in this PR" leaves out.
adopt-mount-unaddableprintsgit will not add the leg mount path {path}: {exc.output}, git's output as it is, beside the_init_refused,adopt-remote-unreadableandsource-unclonableroutes the body lists. I read this and did not probe whether a value that reaches it can hold a control character. Fix: add it to that list, and to the follow-up with validate-repository-naming.py, validate-manifest.py, validate-pins.py, repo_shape.py, shape_materialize.py and adopt-project.py: the helpers that spell a name for a line or for the plan teststr.isprintable()or userepr, so a combining mark, a variation selector or a Hangul filler in a name prints raw, andmaU+034Finreads asmain#1165.
No MUST and no SHOULD. The NOTE's place before NEXT: (the issue's N4) is the better one, for the reasons above; I would not move it.
Lane: openreposhape-4 (openRepoShape-4)
…` before `git filter-repo`, so under a Latin-1 locale a source under a directory named with the byte 0xE9 is extracted, where filter-repo died printing that url as U+FFFD, and every branch of the source is still rewritten as before (#1247) (#1251) **Head `abeac66d` is a fix round for RV1251's review of `825923c7`** (#1251 (comment)): one SHOULD and two NITs, set out under "Review round 1" below. The sections after that describe this head. `_extract_leg` (`adopt-project.py`) now unsets the leg clone's `remote.origin.url`, in the clone's own config file (`--file <work>/.git/config`), right after its `checkout -B` and before `git filter-repo` runs. filter-repo then prints no url, so a Latin-1 stdout has no U+FFFD to choke on, and it still turns every remote-tracking branch into a branch first, so the legs are the ones main makes. ## The defect, reproduced on main - **Where:** main `b1f45fa7`, git 2.43.0, Python 3.12.3, git-filter-repo 2.47.0 (`--version` prints `a40bce548d2c`). - **Launcher:** pip's entry point, `#!.../frvenv/bin/python3`, WITHOUT `-E`. So any `PYTHON*` variable would reach filter-repo, and the reproduction sets none: no `PYTHONIOENCODING`, no `PYTHONUTF8`. - **Locale:** Latin-1, built as #1214 and #1106 build it (`localedef -c`, `LOCPATH`, `LC_ALL=en_US.ISO-8859-1`). - **Source:** `make_source_repo` under a directory `caf` plus U+00E9, made as the byte 0xE9. The probe runs `plan`, `check` and `execute --local-remote-dir` as the existing row does, but without its `PYTHONIOENCODING=utf-8:strict`. - **Result:** `plan` exit 0, `check` exit 0 (`plan ok`), and `execute` exit 2: ```text creating the leg repositories bare .../remotes/Northwind-spec.git bare .../remotes/Northwind-code.git REFUSED extracting the spec leg. THE EXACT COMMAND WAS: git filter-repo --paths-from-file .../work/spec-paths.txt --force (in .../work/Northwind-spec) exit 1 --- output --- Traceback (most recent call last): File ".../frvenv/bin/git-filter-repo", line 8, in <module> sys.exit(main()) File ".../git_filter_repo.py", line 4973, in main filter.run() File ".../git_filter_repo.py", line 4878, in run self._migrate_origin_to_heads() File ".../git_filter_repo.py", line 4462, in _migrate_origin_to_heads print(m) UnicodeEncodeError: 'latin-1' codec can't encode character '\ufffd' in position 237: ordinal not in range(256) --- end output --- ``` Both bare leg repositories were left behind. So the defect does not need a `-E` launcher: a person in a Latin-1 locale sets no `PYTHONIOENCODING`, and pip's launcher fails as well. **The existing row, `test_adopt_template_values.py::test_a_utf8_source_directory_still_adopts_with_local_remotes`, under Latin-1 on main:** - With pip's launcher it PASSES (1 passed). Its `PYTHONIOENCODING=utf-8:strict` reaches filter-repo through `execute`'s environment. - With a copy of that launcher whose only change is `-E` on the shebang (a model of the pipx launcher #1247 quotes), it FAILS with the traceback above. That matches W1196's log. - A check that `-E` drops the variable: with `PYTHONIOENCODING=bogus-codec`, pip's launcher dies at start-up, while the `-E` copy prints its version. - On this head the row passes under Latin-1 with both launchers. ## The fix, and the two candidates measured first **Candidate 1 in #1247, `git remote remove origin` before filter-repo, changes what the legs hold.** It is rejected. - `git remote remove` deletes `refs/remotes/origin/*` with the remote. filter-repo then no longer makes the source's other branches into branches, and never rewrites them. - **Measured** with `execute` on main and on candidate 1, over two ASCII sources with fixed dates. For each leg's bare repository the probe compared: every ref, every commit with its tree, parents and full message, and every reachable object. - **The standard source** (`make_source_repo`, one branch): identical. - **A source with a `release` branch** whose commit R changes `specs/001-feature/tasks.md`, no tag reaching it, and on `main` a `git cherry-pick -x R`: the spec leg's tip differs, `0316169e` on main against `82176493` on candidate 1, with the same tree `9ef5b676`. On main its message reads `(cherry picked from commit 407d03a0...)`, R as rewritten. Under candidate 1 it reads `(cherry picked from commit 30e9dd30...)`, R's id in the source. - **It also changes behaviour that `check` describes.** As a mutant, it fails the existing `test_adopt_split_branch.py::test_the_extraction_stops_at_a_source_branch_in_its_way` (both rows, `DID NOT RAISE CommandFailed`). With it, a source branch in git's way no longer stops the extraction. **Candidate 2, `LC_ALL=C.UTF-8` for the child, depends on the host.** - **Measured here:** under `python3 -E`, a pipe's encoding is `utf-8` under `LC_ALL=C.UTF-8`. It is also `utf-8` under `C`, `POSIX` and a locale name that does not exist, because CPython falls back to C and turns on its UTF-8 mode. - **But** it hands the child a locale by name and leans on that interpreter behaviour. It changes the locale of every git command filter-repo starts. And on Windows, `LC_ALL` does not set a pipe's encoding (read, not run). - No `PYTHON*` variable is a cure, because a `-E` launcher drops it. **What this PR does: unset the url only (`git config --file <work>/.git/config --unset remote.origin.url`).** It depends on no locale. It names the clone's own config file, so `GIT_CONFIG` in the environment cannot redirect it to a file of the person's (RV1251's SHOULD-1, below). It goes on from a clone that has no url to unset, and from no other failure (NIT-1, below). - filter-repo's `_migrate_origin_to_heads` (2.47.0) turns `refs/remotes/origin/*` into `refs/heads/*` first. Then it returns when `remote.origin.url` is not in the config, before the NOTICE that printed the url and before its own `git remote rm origin`. - **The legs are byte-identical to main's** in both probes above, the cherry-pick source included (`diff` of the dumps: empty). - **What stays in each work clone** (measured on this head, a plain `execute`): the url-less `[remote "origin"]` with its fetch refspec `+refs/heads/*:refs/remotes/origin/*`, and `branch.main.remote = origin` and `branch.main.merge = refs/heads/main`. Nothing reads any of it. `_push_the_leg` pushes to the leg's url, and `_mount_the_legs` mounts the clone by its path. RV1251 measured that the mounted submodules hash the same as main's. - The change is one `run` in `_extract_leg` (now with a `try`), with a comment. No pinned or template copy of `adopt-project.py` exists, and no document describes this step. ## Review round 1 (RV1251 at `825923c7`) **SHOULD-1, fixed: the unset named no config file.** `GIT_CONFIG` in the environment (a legacy variable git still reads) names the file a bare `git config` writes. - **Reproduced** with `GIT_CONFIG` naming a file that holds `[remote "origin"] url = ...` and the standard source, in `execute`: - main: exit 0, `adoption verified`, the file unchanged. - `825923c7`: exit 2, `REFUSED extracting the spec leg`, both bare legs made and nothing pushed, and the file empty. The first leg's unset took the only key out of it, and filter-repo, reading that empty file as the config, died with a `ValueError` in `get_config_settings`. (RV1251's file ended in an unset exit 5 instead.) - this head: exit 0, `adoption verified`, the file byte for byte as it was. - **Fix:** `run(["git", GIT_CONFIG, "--file", str(work / ".git" / "config"), "--unset", "remote.origin.url"], cwd=work)`. `--local` is no way round it: with `GIT_CONFIG` set, git 2.43 exits 129 (only one config file at a time). - **`work / ".git"` is a directory wherever `execute` reaches the clone:** - the clone is made by one `git clone -q <source> <work>`, and `work` is `work_root / <name>`, where `work_root` is `os.path.realpath(--work-dir)` or a `tempfile.mkdtemp` folder (`_work_root`): an absolute path; - an exported `GIT_DIR` does not move a clone (measured: the clone has its own `.git`); - an exported `GIT_WORK_TREE` does make the clone another shape (the git dir is `work` itself), but `execute` stops on it first, on main and on this head alike, at the legs' `git init --bare` (`REFUSED a git or gh command failed`); - a path whose directory is not there gives exit 255 (`could not lock config file ... No such file or directory`), which is refused. - **Not changed:** filter-repo's own `git config --list` also reads the file `GIT_CONFIG` names (the `ValueError` above came from it), so under `GIT_CONFIG` it sees that file's settings, as on main. On this head the run ends `adoption verified` with the legs main makes. **NIT-1, handled: a clone with no `origin` url.** A global `clone.defaultRemoteName = upstream` gives the clone a remote that is not `origin`, so `git config --unset remote.origin.url` has no key and exits 5 (measured, git 2.43). It can happen. - **Before:** main fails at `mounting the legs` (`No such remote 'origin'`), after both legs are pushed. `825923c7` failed earlier, at the unset (exit 5, an empty output block), with nothing pushed. - **Now:** `_extract_leg` goes on from exit 5 and from no other exit (`GIT_CONFIG_KEY_ABSENT = 5`). The run fails where main fails, not earlier and not worse. Any other exit of the unset (255 for a file git cannot lock, 129 for a usage it refuses) is refused as any failed command is. - **Not mended here:** `clone.defaultRemoteName` itself. `execute` fails on it on main too. It is a follow-up for its own issue. - Exit 5 also covers a config with more than one `remote.origin.url` (measured); that too goes on, as main does. **NIT-2, corrected in this body:** the claim that the fix "depends on no locale and no variable" (it now names its config file, so `GIT_CONFIG` cannot redirect it); the work clone's leftovers, now listed under "What stays in each work clone" (the url-less `[remote "origin"]`, `branch.main.remote` and `branch.main.merge`); and the note that #1141 conflicts, dropped: it merges clean at `0b8fb320`. ## Rows `tests/test_adopt_filter_repo_origin_url.py`, new, five rows: - **`test_filter_repo_starts_with_no_origin_url_and_every_branch`** (any locale). `_extract_leg` itself runs over an ASCII source with a second branch, with `run` spied at the `git filter-repo` call. When filter-repo starts, the clone has no `remote.origin.url` and still has `refs/remotes/origin/main` and `refs/remotes/origin/beside`. filter-repo's output holds neither `Removing 'origin' remote` nor the source path. Afterwards the clone has `refs/heads/beside`, and the leg holds the clone's HEAD. - **`test_execute_adopts_a_source_named_in_latin1_under_latin1`** (Latin-1). It runs `plan`, `check` and `execute --local-remote-dir` for #1247's source (a directory `caf` plus the byte 0xE9) in a Latin-1 locale the test builds as #1106 builds one. No `PYTHON*` variable is set except `PYTHONUTF8=0`. It asserts no traceback, `check` exit 0, `execute` exit 0 and `adoption verified`. - It skips, by name and with `localedef`'s words, where no Latin-1 locale can be built, and on Windows. It also skips where a name that is not UTF-8 cannot be made (`skip_where_a_name_cannot_be_not_utf_8`). - It builds its locale whatever the gate's locale is, so it fails on main under a UTF-8 gate too, which is what CI runs. - **`test_execute_leaves_the_file_git_config_names_alone`** (new, SHOULD-1). `execute` with `GIT_CONFIG` naming a file that holds a `[remote "origin"]` url: `adoption verified`, and the file is byte for byte as it was. The child gets none of the gate's locale variables, so the row runs alike under a UTF-8 and a Latin-1 gate. - **`test_a_clone_with_no_origin_url_goes_on_to_filter_repo`** (new, NIT-1). `_extract_leg` itself under a global `clone.defaultRemoteName = upstream`: the unset exits 5 (recorded as it is raised), the clone's `remote.upstream.url` is the source, and the leg is pushed. - **`test_an_unset_that_fails_for_any_other_reason_stops_the_leg`** (new, NIT-1). A `config.lock` in the clone makes the unset exit 255: `CommandFailed`, before `git filter-repo` and with nothing pushed. It needs no git-filter-repo. - The earlier rows' setup is now two helpers, `extract_the_code_leg` and `adopt_each_step`. The file is ASCII with no backslash escape; each non-ASCII character is built with `chr` or `bytes`. **Counts** (5 rows; the same file in each tree): | code | UTF-8 gate, pip and `-E` | Latin-1 gate, pip and `-E` | |---|---|---| | main (`_extract_leg` as on `dffc4226`; unchanged in `5926ccca`) | 4 failed, 1 passed (the GIT_CONFIG row passes: main leaves the file alone) | 4 failed, 1 passed | | `825923c7` (the previous head) | 2 failed, 3 passed (the GIT_CONFIG row and the exit-5 row fail) | 2 failed, 3 passed | | this head | 5 passed | 5 passed | The two earlier rows fail on main and pass here. The GIT_CONFIG row fails on `825923c7` and passes here. The exit-5 row fails on `825923c7` and on main (no unset to record) and passes here. The lock row passes on `825923c7`, which refuses every exit; it guards the new `except`. ## Mutants Each mutant was applied in a worktree at this head, the five rows plus the two `test_the_extraction_stops_at_a_source_branch_in_its_way` rows were run under UTF-8 and under Latin-1, and `adopt-project.py` was restored with its sha256 checked after each one (`RESTORED sha256 ok`, tree as before). | mutant | UTF-8 | Latin-1 | killed by | |---|---|---|---| | M1: drop the unset | 4 failed, 3 passed | same | the two earlier rows, the exit-5 row and the lock row (no unset to see) | | M2: the unset moved after `git filter-repo` | 2 failed, 5 passed | same | the two earlier rows | | M3: `git remote remove origin` instead | 5 failed, 2 passed | same | the any-locale row, the exit-5 row, the lock row and both split-branch rows | | M4: drop `--file <path>` | 1 failed, 6 passed | same | the GIT_CONFIG row | | M5: `--local` instead of `--file <path>` (exit 129 under `GIT_CONFIG`) | 1 failed, 6 passed | same | the GIT_CONFIG row | | M6: accept every exit of the unset | 1 failed, 6 passed | same | the lock row | | M7: accept none (no `try`) | 1 failed, 6 passed | same | the exit-5 row | | M8: `--file <work>/config`, a path with no file (exit 5, carried on) | 3 failed, 4 passed | same | the any-locale row, the Latin-1 row and the lock row | ## Gates All runs used `/usr/bin/python3 -m pytest -p no:cacheprovider` with one reused basetemp per run kind and `-o tmp_path_retention_policy=failed`, at most 2 pytest runs at a time. The environment had a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL` an empty file and `GIT_CONFIG_NOSYSTEM=1`, and no `GIT_ALLOW_PROTOCOL`, `GIT_SSH_COMMAND`, `GIT_CONFIG`, `LANES_*` or `PYTHON*` variable. filter-repo was pip's launcher (`a40bce548d2c`) unless a row says otherwise. **The tree gated is a scratch merge** of this head and `origin/main` `5926ccca` (built with `merge-tree` and `commit-tree`; its diff against main is this PR's two files). One hygiene row caught a path in the first attempt (`/home/someone/...` in the new test, `test_no_committed_file_names_a_host_absolute_path`); the row now names `https://example.invalid/keep-me.git`, and everything below ran on the final head. **Gate A, UTF-8 (`LC_ALL=C.UTF-8`), one pytest, 23 files:** **1518 passed, 3 skipped** on the scratch merge. - the new file, `test_adopt_template_values.py`, `test_adopt_split_branch.py` and `test_adopt_e2e.py`; - the other four files that name `_extract_leg` or `_push_the_leg`: `test_adopt_filter_repo_failure.py`, `test_adopt_history_gitlinks.py`, `test_adopt_late_failure.py` and `test_adopt_plan_escapes.py`; - `test_adopt_check_throwaway_repos.py`, `test_adopt_partial_clone_source.py`, `test_adopt_pins.py`, `test_adopt_git_rm_bytes.py` and `test_adopt_plan_partial_clone.py` (new on main); - `test_repo_hygiene.py`, `test_path_guard_hygiene.py`, `test_cli_reference.py` and `test_scaffold_leg_path_collisions.py`; - the files that scan every test file: `test_no_auto_maintenance.py`, `test_non_utf8_git_failures.py`, `test_git_output_decoding.py`, `test_setup_project_repo_misdrawn.py`, `test_shape_advisory.py` and `test_openreposhape_package.py`. - **Main `5926ccca`, the same 22 files without the new one:** 1513 passed, 3 skipped. The head's extra 5 are the five rows. - **Skips (`-rs`):** the same 3 on both, none for lack of git-filter-repo: `test_adopt_check_throwaway_repos.py:482` (a disk that keeps case) and `test_no_auto_maintenance.py:222` x2 (`GIT_CONFIG_GLOBAL` set). **`tests/test_shape_doctor.py`, UTF-8, 4 shards from a bash script** (449 ids): **Head (scratch merge):** 118, 110, 118 and 100 passed, with 3 skipped in the last shard (the PowerShell line, Windows only): 446 passed, 3 skipped. RV1251 and W1247 measured 446 passed and 3 skipped on main at `11560eb9` and `b1f45fa7`; it was not re-run on `5926ccca`, because this PR touches nothing the doctor reads. **Latin-1** (the locale built as above, `PYTHONUTF8=0`), the new file and `test_adopt_template_values.py`: | run | main `5926ccca` | this head (scratch merge) | |---|---|---| | new file + `test_adopt_template_values.py`, pip launcher | 4 failed, 69 passed, 5 skipped (template values only) | 4 failed, 74 passed, 5 skipped | | the same, `-E` launcher | 5 failed, 68 passed, 5 skipped (template values only) | 4 failed, 74 passed, 5 skipped | - **The 4 failures are the same rows on main and on this head, under both launchers.** All four already fail on main and are #1214's to judge, not this PR's: `test_plan_refuses_a_flag_no_file_can_hold` at `--elected-by` and `--reference` with U+DCE9, `test_plan_and_check_say_the_same_sentence`, and `test_execute_refuses_a_local_source_directory_that_is_not_utf8` (under Latin-1 the byte 0xE9 is valid text). - The fifth failure on main under `-E` is `test_a_utf8_source_directory_still_adopts_with_local_remotes`, which this head fixes. ## Merge-trees `git merge-tree --write-tree` of this head against: - **main:** clean against `5926ccca`, and the merged `adopt-project.py` keeps the `--file` call. - **#1177 (`5208e874`) and #1225 (`73c9f57e`)**, the only open PRs that touch `_extract_leg`: both clean, each keeps the `--file` call. - **#1141 (`0b8fb320`):** clean. The earlier note that it conflicts is stale and is dropped. - RV1251 merge-tree'd the 27 other open PRs that touch `adopt-project.py` against the previous head: 26 clean, and #1237 conflicts in `tests/test_adopt_check_throwaway_repos.py` with main alone. This round re-ran main, #1177 and #1225 (the only PRs that touch `_extract_leg`) and #1141 (the one the earlier body got wrong). ## Not done, and limits - Windows, another one-byte locale (cp1252 included) and a real pipx install were not run. The `-E` launcher here is a one-line copy of pip's. - **Read from the code, not run:** filter-repo prints nothing else that holds the url. Its other prints of `origin` are under `--sensitive-data-removal`, which `execute` never passes. Closes #1247 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Closes #1195
This PR is stacked on #1177: its base is
fix/1152-plan-and-remote-paths-spelled, at5208e874e. It lands after #1177 is onmain, and it stays a draft until then.What
#1195 follows RV1177's review of #1177 (for #1152). It names two raw routes, N3 and N4, and six mutants, K1, K2, K4, K5, K6 and K7, that all 148 rows of #1177's
tests/test_adopt_plan_and_remote_paths_spelled.pylet through.Three files change:
adopt-project.py, that test file, andtests/test_adopt_github_exits.py.N3: a refused command no longer names
--local-remote-dirrawCommandFailed.loudlybelongs toscripts/shape_materialize.py, and every tool shares it. It printed the command, its(in <cwd>)line and its output as they were. So when apre-receivehook refused a leg push under--local-remote-dir <D>/re<U+202E>mote, the raw U+202E was printed three times: in the command, and in git's ownToanderror:lines.The change is in
adopt-project.pyonly.shape_materialize.pyis unchanged._unacted(text, kept="")writes each character thatstr.isprintablerefuses, other than those inkept, as_spelled_characterspells it, with no backticks. That is what_first_saiddoes to the line gh said._loudly(exc, step)builds the same REFUSED block from the command's arguments, its directory and its output, each passed through_unacted.kept="\n"), so it still reads one line per line.loudlyprints the whole output, while_first_saidkeeps one line._push_the_leg),_failed_loudlyandmain's catch-all now print a refused command through_loudly, notexc.loudly.I measured it with a real
execute --allow-empty-leg specand a globalpre-receivehook that refuses every push:5208e874e, U+202E in--local-remote-dirreaches stderr raw 3 times. On this head it reaches stderr 0 times. The command, the(in ...)line and git'sToline each name it as\u202e, six ASCII characters.?in itserror:line.\udcffbefore this change, because stderr isbackslashreplace. git's own lines hold U+FFFD for it, the characterrundecodes that byte to.N4: a NOTE says when
NEXT:'s clone command holds a character a terminal does not show#1195 asks for a ruling: refuse such a source, as the README's clone line does, or add a note under
NEXT:. The code settles it in favour of the note:_refuse_an_unwritable_clone_urlasksunwritable_reason. Since adopt-project.py: since #247 a byte that is not UTF-8 in--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255, that lets through aCfcharacter other than the nine bidi controls. Its docstring inscripts/shape_materialize.pysays such a character "is invisible, not unwritable", and that a Persian name joins its parts with U+200C.tests/test_adopt_template_values.pylistsAli<U+200C>Reza,a<U+200E>b,a<U+200F>banda<U+061C>basWRITABLE. Refusing the source would reverse adopt-project.py: since #247 a byte that is not UTF-8 in--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255._branch_deletion's rule that a printed command pastes as it is.shreads a quoted path, not a spelled one.I measured the old behaviour on
5208e874e, with a fullexecute --local-remote-dirof a source under<D>/so<c>urce/Thing. U+200B, U+200F, U+200C and U+00A0 each pass, and thesplitline spells them.NEXT:prints each one raw, once, insideshlex.quote's single quotes.On this head, the clone command is unchanged. When the url is not
str.isprintable, this line prints just before theNEXT:block:A url whose every character is printable gets no NOTE. A source name that ends in a space shows between the quotes, so it gets none either.
Where the NOTE goes differs from the issue's wording; please weigh it.
NEXT:.execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 and adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #1207 each add lines right after theprintof theNEXT:block. With the NOTE printed under the block, this branch conflicted with adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 (git merge-treeexit 1).printand flipping the row's order check. It would then need a rebase over whichever of adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 and adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #1207 lands first.The new rows
The new file goes from 148 to 188 rows, 40 of them new.
tests/test_adopt_github_exits.pykeeps its 23 rows, with one assertion tightened.K1, K2.
test_a_printable_plan_is_named_as_typed_unless_900_spells_itrunscheckon the uncovered plan, in the plan's own directory, four ways.run_adoptnow takescwd=andover=.--plangiven`<D>/plan.yaml ``plan.yaml`, relative, between backticks`\x60plan.yaml\x60`plan.yaml, relativeplan.yaml./plan.yamlplan.yamlK4, K5. Two new
ROWSentries. Each runs undercheckandexecute, with the plan at each of the five path forms.plan-wrong-kind-a-placement-planexpectskind is 'placement-plan', expected 'adoption-plan'.org-too-long, with anorg:of 40o, expects`org:` is oooooooooooo...oooooooooooo, 40 characters.5208e874e, which confirms them.K6.
printable-U+00E9is added to the remote-dir test.re<U+00E9>moteprints as typed, with no backticks.K7. In
tests/test_adopt_github_exits.py, the GitHub-modesplitline must now match in full:split <12 hex> on adopt/three-repo-shape -> https://github.com/testorg/Northwind.git.N4.
SPLIT_SOURCESgainsright-to-left-mark(U+200F). Every split row checks either that the NOTE comes beforeNEXT:, or that there is no NOTE.N3. Three tests:
test_a_refused_command_names_the_remote_and_work_dirs_spelledis the probe above as a test. The character goes in both--local-remote-dirand--work-dir, for U+202E, ESC, byte 0xff and a plain control. The line feed is left out, because git's own output names the path and the output's line breaks are kept.test_a_late_refused_command_is_said_spelledchecks_failed_loudly, in process, for all fiveCHARACTERS.test_a_refused_command_no_step_said_is_said_spelledchecksmain's own block, in process, withcmd_checkreplaced, for all five.Proof
Before and after
tests/test_adopt_github_exits.py(23 rows)5208e874e73c9f57e85208e874e. The 12 failures are all N3 and N4 rows:_failed_loudlyrows;mainrows.5208e874e, because its code is right there. Each one fails on its mutant (below).\udcff, and the plain control.Mutants
All 20 were killed. Each was applied to
adopt-project.pyon this head and restored withgit checkout. After every restore, the file's sha256 was163c692aff5b076410bde27740439070f2b9a4c62b4a7dc9510199cb0a7f890a.tests/test_adopt_github_exits.pywas run too (211 rows).check's last count line names--planbyisprintable()aloneplan.path.absolute()plan-wrong-kindnames the plan rawadopt-org-too-longnames the plan rawsplitline names the source's own path_loudlyleaves the arguments raw_loudlyleaves the working directory raw_loudlyleaves the output rawexc.loudly_failed_loudlygoes back toexc.loudlymaingoes back toexc.loudly_unactedkeeps every whitespace charactersplitline spells the pathGate
The gate ran on the scratch merge
37c65a609oforigin/main961640c68and this head73c9f57e8, in the landing gate's environment:tests/test_shape_doctor.pytests/test_adopt_root_symlinks.pyrows need git 2.45 for the reftable backend. Onetests/test_stdout_encoding.pyrow and the three doctor rows run only on Windows.The 52 files in g0 to g5
tests/test_adopt_attributes_eol_block.py,tests/test_adopt_c_locale_bytes.py,tests/test_adopt_clone_template.py,tests/test_adopt_contracts_dir.py,tests/test_adopt_e2e.py,tests/test_adopt_entry_paths.py,tests/test_adopt_execute_refusals.py,tests/test_adopt_filter_repo_failure.py,tests/test_adopt_git_reads.py,tests/test_adopt_git_rm_bytes.py,tests/test_adopt_github_exits.py,tests/test_adopt_late_failure.py,tests/test_adopt_leg_keys.py,tests/test_adopt_mount_ignored.py,tests/test_adopt_pins.py,tests/test_adopt_plan_and_remote_paths_spelled.py,tests/test_adopt_plan_escapes.py,tests/test_adopt_reencoded_files.py,tests/test_adopt_relative_submodule_url.py,tests/test_adopt_root_manifest.py,tests/test_adopt_root_shape_paths.py,tests/test_adopt_root_symlinks.py,tests/test_adopt_spec_only.py,tests/test_adopt_spelled_names.py,tests/test_adopt_split_branch.py,tests/test_adopt_submodule_url_answers.py,tests/test_adopt_submodules.py,tests/test_adopt_temp_work_roots.py,tests/test_adopt_template_values.py,tests/test_adopt_tracking_branch.py,tests/test_adopt_unsearchable_source.py,tests/test_adopt_values_as_typed.py,tests/test_adopt_verify.py,tests/test_doctor_next_is_a_command.py,tests/test_dry_run_asks_remotes.py,tests/test_family_add_refusals.py,tests/test_git_output_decoding.py,tests/test_github_limits_init_check_org.py,tests/test_github_name_limits.py,tests/test_non_utf8_name_callers.py,tests/test_path_guard_hygiene.py,tests/test_refusal_default_remediation.py,tests/test_repo_hygiene.py,tests/test_ruleset_hint_tracking_branch.py,tests/test_scaffold_leg_path_collisions.py,tests/test_scaffold_manifest_values.py,tests/test_scaffold_reuse_seeded_legs.py,tests/test_stdout_encoding.py,tests/test_template_values_single_pass.py,tests/test_update_shape.py,tests/test_update_shape_add.py,tests/test_update_shape_merged_away.pymainhas moved since the gate's base, and it touchedadopt-project.py.origin/mainis now50ce8ab15. Of the 15 commits since961640c68, one touchesadopt-project.py:b7edd8816(adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119, for adopt-project.py check: follow-ups of PR #1031 (#910): on git 2.43 an init.defaultBranch git will not take passes check and execute stops after both legs exist; _resolved and _answers still use TMPDIR, hooks and a plain git init #1083), with 279 lines added and 78 removed. None touches either test file.git merge-treeof this head with50ce8ab15is clean. In the mergedadopt-project.py:_unactedand_loudlyare each defined once;.loudly(is called only inside_loudly, so adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119 adds no newexc.loudly(call.50ce8ab15. The landing gate re-tests this head on its own merge.Merge-trees (2026-10-23T20:14Z)
git merge-tree --write-tree <X> 73c9f57e8exits 0 (clean) for every<X>below:origin/mainat50ce8ab15;--planpath incheckandexecute, and the--local-remote-dirpath inexecute'sbare, leg andsplitlines, are named with_shown, as #1154 names--outand--work-dir, so a bidi control, an escape, a line feed or a byte that is not UTF-8 in either no longer reaches the terminal as it was typed (#1152) #1177's head,5208e874e;adopt-project.py: adopt-project.py:checkandexecuteread one list of a plan's problems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), socheck's first finding isexecute's refusal for each pair (#1113) #1210, adopt-project.py, shape_materialize.py and the assembly-root template: a keptGNUmakefile/is a directory GNU make stops at,python3 scripts/bootstrap.pynamespythonon Windows, and the template says what to run wheremakereads another file #1207, adopt-project.py:_pin_row_problemsays a pin field whose value is not text as_read_assays what the plan's reader read, "recordscommit:as the number 7", where it spelled it as Python does,commit: 7andTrue, socheckandexecutename it in the words the plan's other refusals use under the sameplan-pin-malformed(#1114) #1198, adopt-project.py:_github_namereads no owner over 39 characters or repository over 100, soplan --source <50 b's>/Thingnever asksghfor a name GitHub cannot hold, no remedy offers--org <50 b's>, andcheckandexecutenever clone such a record again (#1129) #1194, adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #1190, adopt-project.py:checknames the case twins the plan keeps in one repository asplan-case-twinson every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, andexecuterefuses the finding before any leg (#1161) #1189, adopt-project.py: a--source, or a plan'ssource.local_path, with a part of its repository git reads at mode 000 (the.git, itsHEAD,objects,refsorconfig, the git directory agitdir:line names, or a directory above it) is refusedsource-unreadablenaming that part, nevergit-failedand never cloned again (#1151) #1186, adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180, adopt-project.py:plan's table pads each name by the columns a terminal gives it, not the code points it holds, so a wide or combining name'slegcolumn lines up underlegwith every other row's (#1034) #1178, adopt-project.py and repo_shape.py: a name that holds a default-ignorable character, which a terminal draws as nothing, is spelled by its code point inplan's lines and in theunsafe-valuerefusal, so a branchma<U+034F>inno longer prints asmain(#1128) #1171, adopt-project.py:checknames each case twin at the shape's copy, above it and at the manifest, before a link there or after it, and at a pin's file whose exact spelling holds nothing, as_KeptTreekeeps every row, not the first (#1138) #1170, adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169, adopt-project.py:plan written toandcloning <repository> (read only) intoname the caller's--outand--work-dirwith_shown, as the other lines of the report name a path, so a bidi control, a line feed or a tab in either no longer reaches the terminal as it was typed (#1115) #1154, adopt-project.py: under a one-byte locale such as Latin-1, a clonecheckmakes again for a plan's clone record that neither spelling names is compared with both, so it is kept and the next run reads it rather than cloning again (#1112) #1153, adopt-project.py: a--work-dirlink into a loop that Python 3.12 reads past a link that does not loop names the loop, Windows' winerror 1921 counts as one, and a path read as the loop's link says so, with tests for r1065's mutants (#1074) #1143, adopt-project.py: a recorded source whose.gitthis user cannot read is refusedsource-unreadable, naming that.git, socheckandexecuteno longer call a cloneplanmade with its.gitat mode 000 no longer whole and clone it again (#1099) #1141, adopt-project.py:checkandexecuteask a plan'sorg:andid:right after its three repository names, so a bad one beside an unanswered entry, a moved source, a refused name or another repository is that one refusal in both, and apins:entry that is no pin is said as_read_assays it (#1020) #1125, README.md, adopt-project.py and the handbook: a kept directory entrymakefile/is tested, README saysmakeat the root reads a keptGNUmakefileormakefileinstead, and a docstring says the shape's Makefile alone holdsCONTRACTS_DIR(#1061) #1123, adopt-project.py: anorg/repo--sourcetoo long for the disk is refused as a path is, naming no GitHub repository and offering no url, so--source <300 a's>/Thingis no longer offeredhttps://github.com/<300 a's>/Thing, which can never clone (#1003) #1122, adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121, adopt-project.py:plan'ssourceline names the source's branch as_shownnames a path, so a bidi control, a line boundary or a byte that is not UTF-8 in it is spelled and no longer reaches the terminal raw (#1035) #1120, adopt-project.py, shape_materialize.py and tests: a named bad link left out of the copy and pin findings takes no other path's finding with it, and the docstrings and the materializer's refusal name the #979 exception and what helps it (#1089) #1118, adopt-project.py: under a one-byte locale such as Latin-1,checkreads a plan written in UTF-8 whose source is there, asking the plan's UTF-8 bytes where the spelling as written names nothing, rather than refusingplan-source-gone(#1078) #1106 and shape_materialize.py and the pinned AGENTS-shape.md: where the root keeps aGNUmakefileor amakefile, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file saysmakereads the first of the three and the block reaches it only once that file holds it (#1048) #1105.No other open PR touches either test file. #1121, which adds lines after
NEXT:, has not landed, and it merges cleanly.Each of the 24 was also merged against a scratch merge of
origin/mainand this head:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169, adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180 and adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #1190 conflict intests/test_adopt_check_throwaway_repos.py, and adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169 also conflicts inadopt-project.py.origin/mainalone, in the same hunks byte for byte: 6 inadopt-project.pyfor adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169, and 13, 8 and 8 in the test file. So this head adds no conflict.Not in this PR
These are outside #1195, which covers
adopt-project.py's--local-remote-dir:scaffold-project.py,update-shape.py,scripts/family.pyandscripts/bump-leg.pystill printCommandFailed.loudlyas it is._init_refusedprintsgit init's argument list raw, and those arguments hold the--work-dirpath.adopt-remote-unreadableandsource-unclonablerefusals print git's or gh's output raw._unactedand the NOTE teststr.isprintable, as_first_saiddoes. So the default-ignorable characters that adopt-project.py and repo_shape.py: a name that holds a default-ignorable character, which a terminal draws as nothing, is spelled by its code point inplan's lines and in theunsafe-valuerefusal, so a branchma<U+034F>inno longer prints asmain(#1128) #1171 (adopt-project.py: follow-ups from RV1120's review of #1120 (#1035):_shownlets 267 invisible characters through, and two cases the tests do not pin #1128) adds to_shownget no NOTE, and are not spelled in a REFUSED block. They belong with_first_saidunder validate-repository-naming.py, validate-manifest.py, validate-pins.py, repo_shape.py, shape_materialize.py and adopt-project.py: the helpers that spell a name for a line or for the plan teststr.isprintable()or userepr, so a combining mark, a variation selector or a Hangul filler in a name prints raw, andmaU+034Finreads asmain#1165.🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)
Summary by Sourcery
Harden adoption diagnostics and next-step guidance against invisible and terminal-sensitive path characters while extending coverage for previously untested output routes.
New Features:
Bug Fixes:
Enhancements:
Tests: