Repository navigation
adopt-project.py: plan written to and cloning <repository> (read only) into name the caller's --out and --work-dir with _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
brettheap wants to merge 1 commit into
Conversation
…nly) into` name the caller's `--out` and `--work-dir` with `_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) `cmd_plan` printed `plan written to {out}` and `Source.clone` printed `cloning {repository} (read only) into {path}` with the typed path as it was. A path holding U+202E or U+2066 turned the line round, a line feed split it and a tab padded it, where the `source` line, the table and the follow-ups spell such a path with `_shown` (#199, #900). #1115 named the `--out` line; RV1120's review of #1120 (its N4) found the `--work-dir` line, which is said for an `org/repo` source by `plan`, `check` and `execute` alike. A path whose every character is printable prints exactly as before. The `--out` line also spells a byte that is not UTF-8 now, `\udcff` between backticks where it was written back as 0xff, as `_shown` spells it in every other line; `test_stdout_encoding.py` pinned the raw byte for that one line, and its row now holds the spelled one. tests/test_adopt_plan_paths_spelled.py runs `plan` as a child with U+202E, U+2066, a line feed, a tab and all four in `--out` and in `--work-dir`, and with a plain path that must print unchanged. The `--work-dir` rows use a `gh` that refuses: the line is said before `gh` runs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR hardens Sequence diagram for safe plan path reportingsequenceDiagram
participant Caller
participant adopt_project as adopt-project.py
participant Filesystem
participant gh as gh
Caller->>adopt_project: cmd_plan(args)
adopt_project->>Filesystem: write plan to out
adopt_project->>adopt_project: _shown(str(out))
adopt_project-->>Caller: print plan written to shown out
adopt_project->>adopt_project: Source.clone(repository, work_root)
adopt_project->>adopt_project: _shown(str(path))
adopt_project-->>Caller: print cloning repository into shown path
adopt_project->>gh: repo clone repository path
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
RV1115: independent review of #1154 at Independent adversarial review for lane 4. No MUST, no SHOULD; two NITs for follow-ups, below. Merge with current main
Probes (real runs of
|
| path holds | main | PR |
|---|---|---|
| U+202E, U+2066, ESC, CR, TAB | raw on both lines | spelled in backticks (\u202e, \u2066, \x1b, \r, \t) on both lines |
| a line feed | the line split in two, both lines | `.../pl\nan.yaml` one line, both lines |
byte 0xff (stdout utf-8:surrogateescape) |
raw 0xff, both lines | `.../pl\udcffan.yaml`, both lines |
a space, a backtick inside, ", ', \, caf\u00e9, a leading - (--out=-plan.yaml, --work-dir=-wdir) |
as typed | as typed (unchanged) |
plain absolute, plain relative, ./plan.yaml, <dir>/./plan.yaml |
as typed (./ dropped by Path) |
identical to main |
--out ending in a space (plan.yaml ) |
plan written to <B>/plan.yaml |
plan written to `<B>/plan.yaml ` |
relative --out `plan.yaml` |
plan written to `plan.yaml` |
plan written to `\x60plan.yaml\x60` |
check --plan P --source testorg/Thing --work-dir <path>andexecute ... --work-dir <path>print the samecloningline: spelled there too (check: U+202E, ESC, 0xff and a line feed;execute: U+202E), and a plain path is unchanged in both (measured).- The non-UTF-8 behaviour change is consistent. One
planover a source directoryTh<0xff>ngholdingcaf<0xff>.md,--out <B>/b<0xff>d/plan.yaml: under bothutf-8:surrogateescapeandutf-8:stricttheplan written toline, thesourceline, the table row and the question all write\udcffbetween backticks, and no raw 0xff reaches stdout. On main theplan written toline alone wrote the raw byte (surrogateescape) or a bare\udcff(strict). - No reader relies on the raw form.
git grep 'plan written to'and'(read only) into': the readers aretest_adopt_plan_out_checked.py(lines 485, 497),test_adopt_plan_flag_refusals_first.py:585,test_adopt_single_repository_record.py:222andtest_stdout_encoding.py:164, all with plaintmp_pathpaths that_shownreturns unchanged;docs/cli.mdnames neither line;docs/release-notes/v1.1.1.md:226,266sayplan written to <out>as history. Nothing reads thecloningline but the new file. - The two changed tests keep their purpose.
test_non_utf8_name_callers.pyonly follows the rename; itsstdout-encodingrow still asks the helper's skip/raise question (36 passed). The renamedtest_stdout_encoding.pytest still proves a non-UTF-8--outleaves exit 0, no traceback and the whole report (with\u65e5\u672c.mdescaped on cp1252) on bothsurrogateescapestdouts; the half it gave up, a surrogate written back as its byte, is still pinned by the handler's own unit rows (test_a_surrogateescape_stdout_keeps_its_raw_bytes_and_escapes_the_rest,test_the_handler_draws_each_run_at_the_range_of_the_bytes_it_escaped).
Gate, on 7a51704a6
| Files | Result |
|---|---|
| the new file | 18 passed |
test_stdout_encoding.py |
47 passed, 1 skipped (Windows default stdout) |
test_non_utf8_name_callers.py |
36 passed |
test_adopt_plan_escapes.py |
59 passed |
test_path_guard_hygiene.py (#1111's rule) |
49 passed |
test_repo_hygiene.py |
100 passed |
test_scaffold_leg_path_collisions.py |
104 passed |
test_shape_doctor.py, 448 ids in 8 shards of 56 (bash, mapfile, every list non-empty, the lists together hold every collected id once, timeout 900 s) |
445 passed, 3 skipped (the PowerShell rows, Windows only); every shard passed |
Mutants (mine; each on adopt-project.py, run against the new file, the file restored with git checkout and its sha256 checked after each)
| Mutant | Result (new file, 18 rows) |
|---|---|
M0: main's adopt-project.py (both lines raw) |
15 failed, 3 passed, the counts the body gives |
R1: --out by isprintable() alone (no trailing-space or backtick rule) |
0 failed (survives) |
R2: --out spells every non-ASCII path, printable or not |
0 failed (survives) |
R3: --work-dir spells every non-ASCII path |
0 failed (survives) |
R4: --out printed as _shown(os.path.abspath(out)) |
0 failed (survives) |
R6: --out spells only Cf, LF and TAB (ESC, CR, a surrogate raw) |
0 failed (survives) |
R7: --work-dir spells only Cf, LF and TAB |
0 failed (survives) |
R12: cloning spelled only where the caller keeps a made list |
0 failed (survives) |
W1115's M1 to M7 (each line raw again, the wrong helper, the wrong value) are the ones that matter, and the 15 failures on main confirm the rows bite. Every survivor above leaves behaviour that the PR's code gets RIGHT unpinned (the probes measure it), so none is a defect: N1 and N2.
Merge-trees
git merge-tree --write-tree of the scratch merge against every open PR that touches adopt-project.py, test_stdout_encoding.py or test_non_utf8_name_callers.py: #1153, #1143, #1141, #1125, #1123, #1122, #1121, #1120, #1119, #1118, #1106, #1105. Exit 0 each time. (#1153 is newer than the body's list.)
Findings
- N1 (NIT), the body's sentence. "A path whose every character is printable prints exactly as before" is not exact: an
--outthat ends in a space, or a relative--outthat begins and ends with a backtick, is printable and is now spelled (the last two rows of the probe table). That is_shown's own adopt-project.py: a kept or dropped name reaches the split commit and the pull request body unspelled, and_spelled,_shownandplan's report print text that reads as another name, or raw #900 rule and the right output; correct the sentence, and add a row for each, since R1 (isprintable()alone) survives without them. - N2 (NIT), the rows. R2, R3, R4, R6, R7 and R12 survive the new file; the PR's code is right on each (probes above). Follow-up rows: a printable non-ASCII path (
caf\u00e9) for each flag, which must print unchanged (R2, R3); ESC and CR inCHARACTERS, and a non-UTF-8--work-dir(R6, R7; for--out, the non-UTF-8 row oftest_stdout_encoding.pyasserts the spelled byte, not run against R6 here); a relative--out(plan.yaml,./plan.yaml), which must print as typed less the./(R4); and onecheck --source testorg/Thing --work-dir <a U+202E path>row, since the body sayscheckandexecuteprint the line too and onlyplanis run (R12).
Lane: openreposhape-4 (openRepoShape-4)



What changes
adopt-project.pynamed two caller-typed paths in its output as they were typed. A path holding U+202E or U+2066 turned the line round, a line feed split it and a tab padded it. Both now go through the existing_shown, the helper thesourceline, the table and the follow-ups already use (#199, #900):cmd_planprintedplan written to {out}with the--outpath raw. This is the line adopt-project.py:plan written toprints the caller's--outpath raw, so a bidi control in it reaches the terminal, where the other plan lines spell paths with_shown#1115 names.Source.cloneprintedcloning {repository} (read only) into {path}with the--work-dirpath raw, for anorg/reposource only (plan,checkandexecuteall say it). RV1120's review of 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 found this one (its N4, see 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), and the comment on adopt-project.py:plan written toprints the caller's--outpath raw, so a bidi control in it reaches the terminal, where the other plan lines spell paths with_shown#1115 asked for it to be fixed with--out.A path whose every character is printable prints exactly as before, unless
_shownspells it by #900's rule: a path that ends in a space, or one that begins and ends with a backtick. The diff inadopt-project.pyis 13 lines.Behaviour change
A path that is not UTF-8 now prints as
\udcffbetween backticks on theplan written toline, as_shownspells it in every other line, where it was written back as the raw 0xff byte before.tests/test_stdout_encoding.pypinned the raw byte for that one line, so its twosurrogateescaperows now hold the spelled form, and one test is renamed totest_a_name_that_is_not_utf8_is_spelled_and_the_report_still_comes_out.tests/test_non_utf8_name_callers.pycalls that test by name and is changed to match.Rows
tests/test_adopt_plan_paths_spelled.pyis new: 18 rows, ASCII only (every non-ASCII character is an escape in the source), POSIX only. It runsplanas a child with U+202E, U+2066, a line feed, a tab and all four together in--out(5 rows) and in--work-dir(5 rows), a raw-bidi-control check for each flag (2 rows each), a plain path for each flag that must print unchanged (1 row each), a./dot-segment--outthat must print as it always did, and one run that clones and writes its plan with both flags at once. The--work-dirrows use aghthat refuses, because the line is said beforeghruns../row pass there, as they must.Mutants
7 of 7 are killed, each restored with its sha256 checked: the
--outpath raw again (M1), the--work-dirpath raw again (M2), either path spelled with_spelledso a plain path gets backticks (M3, M4),--outspelled with_line_shown, the wrong helper (M5), the spelling put on the repository with the path left raw (M6), and--outspelled fromargs.outwhere the printed value isout(M7).Gate
gh, empty global git config): 2098 passed, 4 skipped.038dafba9, which has tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111'stests/test_path_guard_hygiene.py:test_path_guard_hygiene.py, the new file,test_stdout_encoding.pyandtest_non_utf8_name_callers.py: 150 passed, 1 skipped.test_shape_doctor.py, 448 rows in 8 shards of 56: all 8 shards pass, 445 passed and 3 skipped (the PowerShell rows, which need a Windows host).git merge-tree --write-treeagainstorigin/mainexits 0, and the merged tree differs from main in exactly the four files of this diff. Against every open PR that changesadopt-project.py(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: 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, 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, 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) it exits 0 each time.Not in this PR
The
--planpath and--local-remote-dirare still printed raw. That is the follow-up #1152.Landing
This PR lands only after the v1.2.0 tag.
Closes #1115
🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)
Summary by Sourcery
Safely display caller-provided output and work-directory paths in adoption plans.
Bug Fixes:
--outand--work-dirpaths containing terminal-control characters or invalid bytes from being emitted raw in plan output.Enhancements:
Tests: