Repository navigation
adopt-project.py: a --work-dir link 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
Conversation
…hat does not loop and takes Windows' winerror 1921 as a loop, and a `--work-dir` read as the link in the loop is named as read, with the test file's helpers readied for #1074's rows, which are not written yet (#1074) Checkpoint at the coordinator's word (usage at 77%). Not for review. Done: - adopt-project.py: `_the_loop_in` no longer ends its walk at a link whose `stat` succeeds (N1, r1065's R12), and accepts `CANNOT_RESOLVE_WINERROR` (1921) beside ELOOP (N6); docstring relaxed. `_unmade_work_dir` says "read as `<A>/self`, it is a symbolic link that leads round a loop" where the path as read IS the loop's link (N5). Probed on Python 3.12.3: N1's `w -> loop/../d/self2` now names `<A>/d/self2`; N5's `<A>/nothere/../self` the new words; every other #1065 row unchanged. - tests/test_adopt_work_dir_refusal.py: module docstring for #1074, `import errno`, `cannot_write_in` imported, `run_in` takes `cwd=`. Left: the rows themselves (S1, S2, N1, N2, N3, N4, N5, N6), the `refused_for_a_link` `cwd=`/`named=` keywords, `cannot_make` asking the real path, the base-vs-head run, the mutants and the gate. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s not loop passed over to the one that does, a `--work-dir` read as the loop's link named as read and Windows' winerror 1921 taken for a loop, and controls that hold r1065's surviving mutants R3, R5, R9, R11, R19 and R20 (#1074) The rows the checkpoint 672822e left unwritten, on the code it pushed. Pin a change (fail on main at 6da757c, pass on this head): - N1: `w -> loop/../d/self2`, through `plan`, `check` and `execute` (3), and `_the_loop_in` asked directly of `<A>/to-dir/self2` (1), which holds the walk on every Python. - N5: `<A>/nothere/../self`, through the three commands (3). - N6: an `OSError` with `winerror` 1921 from `os.stat` of the link (1). Controls (pass on both): S1, a link to `a-file/sub` and one into a 555 folder (6); S2, `viadir/self` (3); N3, a relative `self` and `<A>/x/../self` (6); N4, `tofile/sub` (3); N6's EINVAL and winerror 123 (2); N2's dangling link, in the existing direct test. Helpers: `run_in` and `refused_for_a_link` take `cwd=`, and `refused_for_a_link` `named=`; `cannot_make` asks `mkdir` of the real path, which is the one `_work_root` makes; `locked_folder` and `refused_in_the_systems_words` are new; `check` is no longer imported. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationUpdate work-directory refusal handling to identify loops beyond non-looping symlinks, recognize Windows error 1921, and describe a path read as the loop link accurately.
TestsAdd command-level and direct regression tests for loop detection and Windows error handling, with controls for existing refusal behavior and helper adjustments to support the new cases.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-21T21:58:55Z) r1143, lane 1's independent review of #1143 at
The full reviewr1143: independent review of PR #1143 (
|
| Claim | Measured |
|---|---|
N1 row Before (plan, main 038dafba, Python 3.12.3) |
true, word for word |
| N1 row After (head) | true, and check/execute match |
| N5 row Before / After | true / true |
| Same way out before and after, exit 2 in every case | true |
| A dangling link still ends the walk | true (direct row; X1 is equivalent) |
| N6: winerror 1921 is a loop; EINVAL alone and winerror 123 are not | true by direct test (X2, X3, X9, X12 killed); on Windows unverified, reasoned reachable for EEXIST |
3.12 realpath(w) is <A>/d/self2 |
true (3.12.3) |
3.13+ gives <A>/realdir/self2; there the N1 command rows pass on main too |
unverified. My reading of 3.13's algorithm agrees. CI is CPython 3.14.8, per main's last green tests run 39655123686. |
| 28 new tests, 53 to 81; head 123 = 81 + 42, no skips | true (collect-only: 53 and 123; head run: 123 passed) |
Base 8 failed, 115 passed on 038dafba, exactly the 8 rows that pin a change |
true |
The same on 6da757cb |
unverified (not re-run) |
| All 20 controls and the N2 row pass on both | true |
| The mutant table: R3 3, R5 6, R9 6, R11 9, R19 3, R20 6, W1 4, W2 1, W3 3, with their "killed by" columns | true, re-measured on the same 87-test selection |
| R4 survives | true; it is equivalent on a real file system |
sha256 ae1fc9f1… at 6d85d59b |
true |
| Functions at 17031 / 17088 / 17159 / 17190; main's last hunk at 11191 | true |
| The merges do not touch those functions or the two test files | true |
Gate: 122 files, 9157 passed, 34 skipped on 6d85d59b |
not re-run. The writer's logs add up: 8,743 + 448 node ids; skips 2 + 8 + 16 + 3 + 2 + 3. The file list matches git grep plus test_agents_md_codes.py. |
| Merged-head runs: 49; 123; 445 passed + 3 skipped; 104 | true (my reruns) |
| Both merges are clean | true (merge-tree tree equals commit tree: fe49b7ed, 08134580) |
Main at 038dafba holds #1091, #1101, #1102 and #1111 |
true |
Of the PR's two files only adopt-project.py moved on main |
true |
"Not in this PR": tofile/sub wording |
true (`<A>/a-file/sub` … (Not a directory)) |
| "Not in this PR": the errno gate was not widened | true (:17124-17125) |
| R14 and R23 were not run | true of the writers; I ran both, and both survive |
Hygiene
Closes #1074is the only closing keyword in the body, the title or the four commit messages.closingIssuesReferencesis[1074].- All four commits are authored by
service@opensoft.dev, and neither PR commit names the operator's address. - The docs need no change:
docs/cli.md:612/668/728stay true, and README, AGENTS.md and templates/ do not mention the refusal's wording.
Base vs head
| Tree | The two PR test files |
|---|---|
Base (038dafba's adopt-project.py in a scratch copy of head) |
8 failed, 115 passed. The 8: N1 through plan, check and execute; N5 through the same three; test_a_link_that_does_not_loop_does_not_end_the_walk; winerror-1921. |
| Head | 123 passed |
My mutants: 10 of 15 killed
I also re-ran the body's nine mutants (R3, R5, R9, R11, R19, R20, W1-W3); all nine are killed, as noted in the table above. Killed:
| Mutant | Change | Failed |
|---|---|---|
| X2 | every _ABSENT_WINERRORS winerror taken for a loop |
1 |
| X3 | a missing winerror defaults to 1921 |
2 |
| X4 | ELOOP and 1921 needed | 36 |
| X5 | root itself never asked |
27 |
| X6 | the N5 branch also taken where the --work-dir is itself a link |
21 |
| X7 | "read as" names the typed path | 3 |
| X8 | the N5 way out names the typed path | 3 |
| X9 | the constant is 1920 | 1 |
| X10 | the walk returns root, not the link |
11 |
| X12 | EINVAL also taken for a loop | 2 |
The five survivors are all equivalent on a real Linux file system:
- R4, the walk from the leaf up. For a deeper link to be
lstat-able, every link above it must resolve to a directory, so only the deepest link can failstat. - R14, the loop question asked for every errno. Any loop in the path makes
mkdirfail with EEXIST or ELOOP; even in a folder at mode 555 it gives EEXIST. This is the change that would matter on Windows. - R23,
_same_entryanswering True on an OSError. Both paths are known to belstat-able by then. - X1, a failing
statthat does not end the walk. Nothing below that link can belstat-ed. - X11, every part asked, link or not. A link above any failing part is reached first.
Only a mocked stat could kill these, and only R14 matters, on Windows.
Main now
fetch origin mainat 21:56:32Z: still038dafba. The head already holds it, so there was no merge-tree to build, and I ran on head.- Results:
test_path_guard_hygiene.py: 49 passed;test_shape_doctor.pyin 4 shards: 111 + 112 + 111 + 111 = 445 passed, 3 skipped, all three the PowerShell line;test_scaffold_leg_path_collisions.py: 104 passed;- the two PR files: 123 passed;
- the other files that name
adopt-work-dir-unwritable:test_adopt_plan_flag_refusals_first.py89,test_adopt_root_symlinks.py205 passed and 3 skipped (reasons not captured),test_adopt_shape_pin_and_work_dir.py20,test_adopt_check_throwaway_repos.py29.
- No failures.
Everything is under /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1143/:
- scripts:
probe.sh,mutants.py,walktime.py,run2.sh,runids.sh,env.sh; - outputs:
probes/,mutants-m*.out,mutants-body-m*.out,head-2files.out,base-2files.out,now-*.out.
Lane: openreposhape-1 (openRepoShape-1)
…re than 40 links on the way, so the walk can name a link that does not itself loop where a loop folded out of the path leaves it more than 40 links (#1074) r1143's NIT-1, a docstring sentence and no change of behaviour. Its probe `contrived-toolong`: `w -> loop/../l0/l1/.../l45/x`, with `loop -> loop`, `l0 -> r0` and each `r(i-1)/li -> ../ri`. On main at 038dafb `plan` said "--work-dir `<A>/l0/.../l45/x` cannot be made: making it a directory failed (Too many levels of symbolic links)"; on 06cc799 it says "--work-dir `<A>/w` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/l0/.../l40`". `l40` is no loop: `r39/l40 -> ../r40` and `r40` is a directory, and `stat` of `l0/.../l39` (40 links) is ok while that of `l0/.../l40` (41 links on the way) gives ELOOP, Linux's limit. The way out still names `<A>/w`, which is right. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ly of a link to itself with EEXIST and a winerror-1921 `os.stat`, and the N5 rows skip on Windows, which folds a `..` out of the path first (#1074) r1143's NIT-4 and NIT-5. NIT-4. `test_a_link_windows_cannot_resolve_is_named_as_a_loop_by_the_refusal` calls `_unmade_work_dir(work, root, FileExistsError(EEXIST))` for a `--work-dir` that is a link to itself, with `os.stat` of it patched to raise an `OSError` with errno EINVAL and `winerror` 1921: the one path N6 is live on, since Windows' `mkdir` of a name that exists is winerror 183, EEXIST, which passes the errno gate. It asserts the reason is "it is a symbolic link that leads round a loop" and the way out names the link. On main at 038dafb it fails ("leads to `<A>/self`, which is not a directory": 9 failed, 115 passed in the two files); with winerror 1921 not taken for a loop (r1143's W2) or the constant 1920 (X9) it fails too, together with the existing winerror-1921 row (2 failed, 122 passed each); on this head, 124 passed. `stat_fails_at` is the one `os.stat` patch, shared with the existing winerror row. NIT-5. `test_a_work_dir_read_as_the_link_in_the_loop_is_named_as_read`, the three N5 rows, takes a new `POSIX_ONLY` skip: Win32 folds `<A>\nothere\..\self` to `<A>\self` before the file system is asked, so there the path IS the link and the reason is not "read as". The file had no Windows skip of its own, and `conftest.WINDOWS_SKIP`'s reason is about `setup.sh`, so the file gets its own, as `test_adopt_entry_paths.py` has. Windows CI runs none of this file. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-22T02:45:54Z) r1143b, lane 1's independent re-check of the NIT round at
The full re-checkVerdict: READY on condition. The condition is that CI's Checks1. The diff: confirmed.
2. NIT-1: true. I re-ran
3. NIT-4: confirmed.
4. NIT-5: confirmed.
5. The body: true, except one list item (NIT-6 below).
6. Runs on head.
7. CI for
I did not wait for it. FindingsMUST: none. SHOULD: none. NIT-1 (docstring over-generalises).
NIT-2 (body, a wrong item in the N6 list).
Nothing else is wrong. The probe dirs are under Hygiene.
Lane: openreposhape-1 (openRepoShape-1) |
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 6 days and 19 hours by commenting |



What
#1065 made
plan,checkandexecutename a--work-dirthat is, orruns through, a symlink round a loop. r1065, the review of #1065, found
three things it got wrong and a set of rows no test held; #1074 lists them
as N1 to N7, S1 and S2. This puts the three right and holds the rest.
Outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z): no milestone, a draft.
The code stays
adopt-work-dir-unwritable, exit 2, nothing is made, and theway out is the one #1065 gave. Three changes in
adopt-project.py, in_unmade_work_dirand_the_loop_in:_the_loop_instopped at the first symlink left in the path: whenos.statof that link succeeded it answered "no loop" without readingthe links below it. It now asks each link left in the path, from
/down,and takes the first that loops. A link that fails otherwise, a dangling
one, still ends the walk, as it did.
CANNOT_RESOLVE_WINERROR = 1921, Windows' "cannotresolve".
os.statof a link round a loop raises it there, with an errnothat is not ELOOP, as
validate-pins.py's_what_standsreads it (notmeasured on Windows: below).
_the_loop_incounts it as a loop as itcounts ELOOP. Any other failure, an EINVAL alone or winerror 123, is no
loop. N6 is live only for a
--work-dirthatos.path.realpathreads asthe link itself (
self, oraforinto -> a): thereos.mkdirof a name that exists gives EEXIST on Windows too (winerror 183), which
passes the errno gate in
_unmade_work_dir, so_the_loop_inis asked. Aloop ABOVE the work dir on Windows (
loop\sub) still gets the system'swords:
CreateDirectoryWthere most likely fails with winerror 1921 andso errno EINVAL, and the gate keeps
_the_loop_infrom being asked. Thatchain is reasoned, not measured.
os.path.realpathreads it as the loop's own link (looped == root), thereason says so: "read as
<A>/self, it is a symbolic link that leadsround a loop", not "the way to it leads round a loop, at the symbolic
link
<A>/self", where the path read IS that link and not the way to it.The way out is unchanged: it names that link.
Measured with
planof a local source,<A>the directory that holds thelinks, before on main at
038dafbaand after on the merged head06cc799c(Python 3.12.3, exit 2 in every case). The first two rows measured earlier
on
6da757cband6d85d59bread the same. The NIT round after r1143(
b5b0e6ca,7af5fabc) changes a docstring and tests only; the first andthird rows were read again on its head
7af5fabc, the third withplan,checkandexecute, which agree, with038dafba'sadopt-project.pyin ascratch copy of this tree for the before.
--work-dirw, withw -> loop/../d/self2,loop -> loop,d -> realdirandrealdir/self2 -> self2--work-dir `<A>/w` cannot be made: it is a symbolic link that leads to `<A>/d/self2`, which is not a directory--work-dir `<A>/w` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/d/self2`<A>/nothere/../self, withself -> selfand nonothere--work-dir `<A>/nothere/../self` cannot be made: the way to it leads round a loop, at the symbolic link `<A>/self`--work-dir `<A>/nothere/../self` cannot be made: read as `<A>/self`, it is a symbolic link that leads round a loopw, withw -> loop/../d/self2/sub,loop -> loop,d -> realdirandrealdir/self2 -> self2--work-dir `<A>/d/self2/sub` cannot be made: making it a directory failed (Too many levels of symbolic links)--work-dir `<A>/w` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/d/self2`The way out is the same before and after in the first two rows:
"Remediation: Remove the symbolic link
<A>/w, or make it lead to adirectory, or pass another --work-dir, or none: without one, each run makes
a new one." in the first row, and the same with
<A>/selfin the second.The first row said only that
<A>/d/self2is not a directory, and not thatit is a link round a loop; the new words say so. The third row is a change
the review r1143 found that the table lacked (its NIT-3), and an
improvement: the first row's folded path with one name more, where
mkdirof
<A>/d/self2/subfails with ELOOP and not EEXIST. Before, the reason wasthe system's, of a path the person never typed, with #524's way out
("Remediation: Pass a --work-dir this user can write in, or none: without
one, each run makes a new one."); after, it names
wand the link in theloop, with the way out for
w, the first row's.Python. The first row's old words need Python 3.12, the one measured,
and I believe earlier ones. There
os.path.realpathstops atloop,leaves the rest of the path unread and folds its
.., which leaves<A>/d/self2, a link that does not loop (d) before one that does. Ibelieve 3.13 and later read on past a loop and give
<A>/realdir/self2,which holds no other link, so that the old words need 3.12. That is from
reading 3.13's
realpath, not from a run: no 3.13 interpreter was on thismachine. If CI's
3.xjob is on 3.13 or later, the command-level N1 rowspass on main there as well, and only the direct test of
_the_loop_incatches the old code.
The docstrings of
_unmade_work_dirand_the_loop_insay the same.Tests
tests/test_adopt_work_dir_refusal.py: 29 new tests, 53 to 82, and the N2row added inside the existing
test_only_a_symlink_that_loops_is_called_a_loop.w -> loop/../d/self2through thethree commands (3), and
_the_loop_inasked directly of<A>/to-dir/self2(1), which holds the walk on every Python; N5nothere/../selfthrough the three commands (3); N6 winerror 1921, askedof
_the_loop_in(1) and, r1143's NIT-4, of_unmade_work_dirwith aFileExistsErrorfor a--work-dirthat is a link to itself andos.statpatched to raise an
OSErrorwithwinerror1921 and errno EINVAL (1),the one Windows path that is live.
and hold what r1065's surviving mutants break:
w -> a-file/subandw -> locked/xwithlockedat mode 555:the system's words and adopt-project.py
check: refuse a kept file at the shape's pin path, and an unwritable--work-dir, before any leg exists #524's way out (6);viadir/selfwithviadir -> realdir: a loop spelled through alinked folder is named once (3);
selfand<A>/x/../self: named as given, madeabsolute, not resolved (6);
tofile/subwithtofile -> a-file: named asrealpathreads it(3);
run_inandrefused_for_a_linktakecwd=, andrefused_for_a_linktakesnamed=;cannot_makeasksmkdirof thereal path; a new
locked_folderfixture (it skips wherechmodcannotkeep this user out, root or Windows) and
refused_in_the_systems_words;stat_fails_at, the oneos.statpatch, which the two N6 rows share.checkis no longer imported.POSIX_ONLYskip, a file-local
skipifonos.name == "nt"with its reason: Win32folds
<A>\nothere\..\selfto<A>\selfbefore the file system isasked, so there the path IS the link and the reason is not "read as". The
file had no Windows skip of its own, and
conftest.WINDOWS_SKIP's reasonis about
setup.sh, so it has its own, astest_adopt_entry_paths.pydoes. Windows CI runs none of this file.
Fail on base. With main's
adopt-project.pyin this head's tree, thetwo files
tests/test_adopt_work_dir_refusal.pyandtests/test_adopt_clone_claimed.py: 9 failed, 115 passed, on038dafba(with the NIT round's tests). Before NIT-4's row it was 8 failed, 115
passed, on
038dafbaand on6da757cb. The 9 are exactly the rows that pina change, each failing on main's words: the three N1 rows through
plan,checkandexecute, the three N5 rows, the direct N1 test, the N6winerror 1921 row, and the N6 row of
_unmade_work_dir, which fails with"it is a symbolic link that leads to
<A>/self, which is not a directory".All 20 controls and the N2 row pass on both. On this head
7af5fabc: 124passed (82 and 42), no skips.
Mutants. Each applied alone to a scratch copy of
adopt-project.pyat6d85d59b(sha256ae1fc9f1e3c2d143a7a0b945f122b8f10521cb2e92e569d67ae015f867f817d0),then the refusal file and
test_a_loop_or_a_file_above_the_work_dir_keeps_its_650_refusal(87 tests)run, then the copy restored and its sha256 read back equal. R3, R5, R9, R11,
R19 and R20 are r1065's surviving mutants: 6 of 6 killed. W1 to W3 each
undo one of this change's three edits: 3 of 3 killed. The merge of
038dafbachanges no line of the functions they mutate: main's last hunk in
adopt-project.pystarts at line 11191, and_work_root,_unmade_work_dir,_the_loop_inand_same_entryare at lines 17031,17088, 17159 and 17190 on the merged head.
stattaken for a loop, no ELOOP testtest_only_a_symlink_that_loops_is_called_a_loop; the two N6 controls (EINVAL, winerror 123)_same_entrycompares the spellings, notlstatidentityrealpathreads it--work-dirnot made absolute--work-dirnormalised withos.path.abspath, which folds..statsucceeds_unmade_work_dir_the_loop_inwalks from the leaf up, not from/downW2's 2 is from a re-run on the NIT round's head
7af5fabc, with the twofiles whole (124 tests): 2 failed, 122 passed. On the same selection r1143's
X9, the constant set to 1920 (
ERROR_CANT_ACCESS_FILE), is killed by thesame two rows: 2 failed, 122 passed. The other rows of this table were
measured on
6d85d59bwith the 87 tests above.The gate, lane 1's, on
6d85d59b(this branch merged with main at6da757cb, before the merge below), one basetemp,EMAILexported,LANES_LANEunset, a stubghfirst on PATH,GH_TOKEN=bogus-w1074, anempty
GH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false, in 63 chunks, each one pytest call under 9minutes: 9157 passed, 34 skipped, 0 failed. It is 122 test files, the
ones that name
work-dirorwork_diras grep reads them andtest_agents_md_codes.py: 121 of them in 59 chunks of up to 150 tests bynode id (8,743 tests), and
test_shape_doctor.py, all 448 tests, in 4shards. It is not the whole suite. The 34 skips are the tests' own, none a
row of this change: template rows in
test_family_init_values.py(19) andtest_leg_path_yaml_scalars.py(8), the PowerShell line intest_shape_doctor.py(3), aGIT_CONFIG_GLOBALset at the session's startin
test_no_auto_maintenance.py(2), and two intest_adopt_eol_block_through_links.py, each on its own reason.On the merged head
06cc799c, the same environment, one call at a time,one basetemp, each under 9 minutes (four runs):
tests/test_path_guard_hygiene.py, which 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 added: 49 passed;tests/test_adopt_work_dir_refusal.pyandtests/test_adopt_clone_claimed.py: 123 passed;tests/test_shape_doctor.py, in 4 shards by node id: 445 passed, 3skipped (112, 112, 112 and 109 passed; the 3 are the PowerShell line);
tests/test_scaffold_leg_path_collisions.py: 104 passed.On the NIT round's head
7af5fabc(main still038dafba, which the headholds; no merge), the same environment, at most three calls at once, one
basetemp each, each under 9 minutes,
test_shape_doctor.pyin 4 shards bynode id (112 each, lists checked non-empty):
tests/test_adopt_work_dir_refusal.pyandtests/test_adopt_clone_claimed.py: 124 passed;tests/test_path_guard_hygiene.py: 49 passed;tests/test_scaffold_leg_path_collisions.py: 104 passed;tests/test_shape_doctor.py: 445 passed, 3 skipped (111 + 1, 112, 111 + 1and 111 + 1; the 3 are the PowerShell line);
tests/test_adopt_plan_flag_refusals_first.py,tests/test_adopt_root_symlinks.py,tests/test_adopt_shape_pin_and_work_dir.pyandtests/test_adopt_check_throwaway_repos.py, which with the two above nameadopt-work-dir-unwritable: 343 passed, 3 skipped (the reftable backendneeds git 2.45 or later; the gate's skips are as listed above).
Not the full suite, and not the 63-chunk gate again: the NIT round changes a
docstring, one test file and this body, and CI runs the full suite.
r1143's NITs
r1143, the independent review of this PR on
06cc799c(#1143issuecomment-6327473568), found it ready on condition (
testson that head)and no MUST or SHOULD. That condition passed. Its six NITs are answered, in
b5b0e6caand7af5fabc, with no change of behaviour:_the_loop_in's docstring says ELOOP also means more than 40links on the way (Linux's limit), so the walk can name a link that does
not itself loop where over 40 links are left; the probe
contrived-toolongis in the commit message. Measured again withplan:w -> loop/../l0/l1/.../l45/x, withloop -> loop,l0 -> r0and eachr(i-1)/li -> ../ri: main says "making it a directory failed (Too manylevels of symbolic links)" of
<A>/l0/.../l45/x; this head names<A>/w,"at the symbolic link
<A>/l0/.../l40", a link that does not loop.head.
_unmade_work_dirin the Tests section: it failson main, and under W2 and X9; 124 passed here.
POSIX_ONLYon the three N5 rows, file-local because the filehad no Windows skip of its own.
this PR" say N6 is live only for a
--work-diros.path.realpathreads asthe link, and that a loop above the work dir on Windows keeps the system's
words.
Merge with main
Two plain merges, no rebase, amend, squash or force.
58ba91b6:origin/mainat6da757cb(shape_materialize.py, adopt-project.py and bump-leg.py: a tag a clone holds without its commit is one it cannot answer for,checksays a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) #1090), clean. It came beforethe tests were written.
06cc799c(head):origin/mainat038dafba, which holds adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091, scaffold-project.py: the--reuse-empty-repohelp, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101,templates/assembly-root/project.yaml and templates/family-root/family.yaml: the comment above
visibility:says it is the visibility the run asked for, and that a repository the run reuses keeps its own, where it said "this is all three repositories' visibility" (#913) #1102 and 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. Clean. Of this branch's two files onlyadopt-project.pymoved on main, in
_kept_pin_findings,_leg_findings,_PlannedRootand_SymlinkProbeamong others, none of them_work_root,_unmade_work_diror
_the_loop_in.tests/test_adopt_work_dir_refusal.pyandtests/test_adopt_clone_claimed.pydid not move on main. 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.pypasses with this branch'sadopt-project.py. The gate above ran on6d85d59b, before this merge;on the merged head only the four runs listed above were made, and CI's
run is the full one.
Not in this PR
--work-dir <A>/tofile/sub, withtofile -> a-file, still names the pathos.path.realpathreads,<A>/a-file/sub, in the system's words, andnames no link, as adopt-project.py: a
--work-dirthat is, or runs through, a symlink round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978) #1065 left it;w -> a-file/subreads the same way.The new N4 and S1 rows hold both as controls.
DANGLING link (r1143's NIT-2):
w -> loop/../dang/self2, withloop -> loopanddang -> missing. Followingwthe system meets<A>/loopfirst, but
os.path.realpath(3.12) folds the path to<A>/dang/self2,and the walk ends at
dang, a link that dangles, as it must. Main at038dafbaand this head say alike, withplan: "--work-dir<A>/wcannot be made: it is a symbolic link that leads to
<A>/dang/self2,which is not a directory". In N1's row
<A>/d/self2is named only becausethe folded reading reaches a second loop; here there is none.
OSErrorwithwinerrorset,built by hand with
os.statpatched; no Windows host ran it. N6 is liveonly for a
--work-dirthatos.path.realpathreads as the link itself(
mkdirgives EEXIST, which passes the errno gate), and a loop ABOVE thework dir on Windows (
loop\sub) still gets the system's words, becausethe gate keeps
_the_loop_infrom being asked. The errno gate in_unmade_work_dir,exc.errno in (errno.EEXIST, errno.ELOOP), was notwidened to winerror 1921, so a
mkdirthat fails there with anothererrno still never asks
_the_loop_in. Widening it is a follow-up ifwanted: r1143's R14, the loop question asked for every errno, changes
nothing on Linux (123 of 123 pass).
_the_loop_in's walk. r1065's R14and R23 were not run by this branch's writers; r1143 ran both, and both
survive, equivalent on a real file system.
Closes #1074
Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Fix work-directory symlink-loop detection and diagnostics while adding regression coverage for platform-specific and path-resolution edge cases.
Bug Fixes:
Enhancements:
Documentation:
Tests: