Repository navigation
adopt-project.py: an org/repo --source too long for the disk is refused as a path is, naming no GitHub repository and offering no url, so --source <300 a's>/Thing is no longer offered https://github.com/<300 a's>/Thing, which can never clone (#1003) - #1122
brettheap wants to merge 2 commits into
Conversation
…efused as a path is, naming no GitHub repository and offering no url, so `--source <300 a's>/Thing` is no longer offered `https://github.com/<300 a's>/Thing`, which can never clone (#1003) `plan --source <300 a's>/Thing` is refused `source-unreadable`, "cannot be read (File name too long)" (#970). It also said "whether it names the path `./<300 a's>/Thing` here, or only the GitHub repository `<300 a's>/Thing`, cannot be told", and offered `https://github.com/<300 a's>/Thing`. Neither can be meant: a part longer than a file name may be is longer than GitHub lets an owner (39) or a repository (100) be named, so that url can never clone. `_source_there` now refuses an `ENAMETOOLONG` as it refuses a path: "so whether a repository is there cannot be told", with the cure for a cause other than permission, and no url. The same holds for the `github.com/` and `www.github.com/` spellings, and for a long repository part beside a `./<org>` directory. Any other refused `stat` of a name (a permission, an I/O error) still names both readings and offers the url. Part (a) of #1003, a source or recorded clone directory this user cannot enter, already landed on main in #1072 (see #918). It is measured again here and not changed. Closes #1003 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR fixes Sequence diagram for handling an overlong source namesequenceDiagram
participant User
participant AdoptProject
participant Filesystem
participant GitHub
User->>AdoptProject: --source spec
AdoptProject->>Filesystem: _is_there(candidate)
Filesystem-->>AdoptProject: OSError ENAMETOOLONG
AdoptProject->>AdoptProject: _source_unreadable(--source spec, exc, whether a repository is there)
AdoptProject-->>User: source-unreadable, no GitHub URL
Note over AdoptProject,GitHub: GitHub is not queried and no unclonable URL is offered
Flow diagram for refused source-stat errorsflowchart TD
A[Source stat fails] --> B{errno is ENAMETOOLONG?}
B -->|Yes| C[Refuse as unreadable path]
C --> D[Say whether a repository is there]
D --> E[Do not offer GitHub URL]
B -->|No| F{name is absent?}
F -->|Yes| C
F -->|No| G[Retain ambiguous-source behavior]
G --> H[Offer GitHub repository URL]
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
RV1122: independent review of #1122 at I reviewed head How the probes ran:
The writer's claims, checked
ProbesIn-process
A deep working directory cannot refuse a short Windows: no worse than main.
macOS reports ENAMETOOLONG for a part over 255 bytes, so the guard works there as it does on Linux. I read this and did not measure it. FindingsSHOULD-1: the guard asks the errno, not whether GitHub can hold the name, and it goes wrong both ways.
NIT-1 (follow-up, outside #1003's words): an owner of 40 to 255 characters, or a repository of 101 to 255.
MutantsMy own mutants, each applied by exact substitution and run against
Merge-trees
GateThe gate ran on the scratch merge
That is 817 passed and 3 skipped, with no failures. pyflakes reports nothing on either changed file, and the diff is ASCII only. Lane: openreposhape-4 (openRepoShape-4) |
… offered its url only where GitHub can hold the name, so `testorg/<300 a's>` beside an unsearchable `./testorg` is offered no url, and `testorg/Thing` through a link too long keeps `https://github.com/testorg/Thing` (#1003) RV1122's SHOULD-1. #1122's first head refused a name as a path, with no url, where its `stat` was `File name too long`. It asked the errno, not the name, and that went wrong both ways: - `testorg/<300 a's>` beside a `./testorg` at mode 000 stops at `Permission denied` before the long part, and was still offered `https://github.com/testorg/<300 a's>`, which can never clone. - `testorg/Thing`, with `./testorg` a symlink to a name too long, is `File name too long` because of the link's destination. It lost the url of a repository GitHub can hold, which main offered. `_source_there` now decides by GitHub's limits, `GITHUB_OWNER_MAX` (39) and `GITHUB_NAME_MAX` (100), which the file already imports. An owner or a repository over them is refused as a path is, whatever reason the operating system gave, with the cure for that reason and no url. Any other name keeps both readings and its url. A part too long for the disk is over both limits, so `<300 a's>/Thing` keeps the first head's answer. The four controls now refuse a name as long as GitHub allows (an owner of 39, a repository of 100) with an I/O error, and expect its url. New rows: a name one over each limit, or too long for the disk, refused with an I/O error or a permission, offers no url (10 rows, in-process); the symlink keeps its url; and `plan` beside a mode-000 `./testorg` offers none. An owner of 40 to 255 characters, or a repository of 101 to 255, with nothing on disk is still handed to `gh`; see #1129. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
RV1122: independent review of #1122 at This re-check covers fix round 1 ( How the probes ran: as uid 1000 on Python 3.12.3, with a stub The fix, in both directions
I probed
The new docstring says the name decides, never the reason, and the probes bear it out. The rows, and why each fails where it shouldI ran the section's 17 rows against three versions of
These match W1003b's figures. Where each row fails, from
Every failure is at the refusal's words, not at a setup step. The controls are right now:
MutantsThese are inverses of the new guard, which is my R7 from the first round. Each was applied by exact substitution and run against
The writer's N1 to N6 overlap with V1 to V5. V2 (their N6) dies only on the symlink row, which skips on Windows. CI is Linux, so that is enough. Merge-trees
GateThe gate ran on the scratch merge
That is 838 passed and 3 skipped, with no failures.
FindingsThere is no MUST and no SHOULD. SHOULD-1 is resolved in both directions.
Lane: openreposhape-4 (openRepoShape-4) |
…eeps a `GNUmakefile` or a `makefile`, 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 says `make` reads the first of the three and the block reaches it only once that file holds it or includes a Makefile that does (#1048) (#1105) Two sentences said a kept `Makefile` is what holds the shape's block back. GNU make reads the first of `GNUmakefile`, `makefile` and `Makefile` it finds, and only that one, so where the root also keeps `GNUmakefile` or `makefile`, `make` reads that file, and reaches the merged `Makefile` only where that file includes it. **Measured** (a real `execute`, then `make -n` in the clone, for each kept name; r1046's NIT-B, repeated; the include rows are RV1105's SHOULD-1): | the source keeps | file `make` reads | after `cat shape/Makefile >> Makefile`, `make -n bootstrap` | |---|---|---| | `Makefile` | `Makefile` | exit 0 | | `Makefile` + `makefile` | `makefile` | exit 2, "No rule to make target 'bootstrap'" | | `Makefile` + `GNUmakefile` | `GNUmakefile` | exit 2, same | | all three | `GNUmakefile` | exit 2, same | | `Makefile` + a `makefile` or `GNUmakefile` that ends in `include Makefile` | that file | exit 0, runs `python3 scripts/bootstrap.py` (exit 2 before the merge) | | `makefile` or `GNUmakefile` alone | that file | no merge to make: the shape's `Makefile` is the root's, and `make contracts-dir` has no rule until that file holds the block or includes `Makefile` | **What changed** - `scripts/shape_materialize.py`, `collision_follow_up`: takes `reads_first`, the file GNU make reads before the shape's `Makefile`. Where it is set, the Makefile's follow-up says GNU make reads that file, the first of the three it finds, and that the shape's make targets reach `make` there only once that file has them, or includes a Makefile that does; until then `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs. Where `scripts/bootstrap.py` went aside too, it says neither command runs the shape's bootstrap until the script is merged, and then `python3 scripts/bootstrap.py` runs it. Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. `GNU_MAKE_READS` joins `SHAPE_MAKE_TARGETS`. - `adopt-project.py`, `_predict_collisions`: reads the file from the plan's root paths as `_makefile_follow_up` does, so the `CONTRACTS_DIR` line and the collision line of one plan name the same file. - `templates/assembly-root/AGENTS-shape.md`: says `make contracts-dir` prints the block wherever `make` reads the shape's Makefile, names the three files in the order the code reads them, and says that where a `GNUmakefile` or `makefile` is kept, beside a `Makefile` or instead of one, `make` reads it, and not the shape's Makefile unless that file includes it. It is one verbatim copy: the digest in every project's `contracts/shape-pin.yaml` is rendered from its bytes at adopt and scaffold time, and no digest or `tree_sha256` of it is stored in this repository, so there is no pin to move here; each project re-pins through `update-shape.py` as for any template change. - `tests/test_pinned_copy_text.py`: `ADOPTED_SENTENCES` holds the new text. - `tests/test_adopt_collision_make_reads.py` (new, 31 rows): the follow-up per kept set, the two lines of one plan naming one file, no collision line where no `Makefile` is kept, the two name tuples equal, the pinned file naming every file the code reads first, the script-aside tail where `make` reads another file, and, in a real adoption's clone, `make -n` held to both sentences for each kept name and for a kept `GNUmakefile` or `makefile` that ends in `include Makefile`. **Fix round 1** (RV1105's review, `b28f40de0` on top of `5a4de7f76`) - **SHOULD-1:** the follow-up said the targets reach `make` "only once `X` holds them, or includes a Makefile that does, and not with that merge alone". The last clause is untrue where `X` already ends in `include Makefile`: the merge alone brings them. The clause is gone, and the docstring paragraph, the `SHAPE_MAKE_TARGETS` comment and the `ADOPTED_SENTENCES` comment now say the merge alone brings the targets only where that file includes `Makefile`. - **NIT-1 (and RV1121's NIT-3):** the follow-up now uses the words of #1121's NOTE: "has them", and "is what the shape's `make bootstrap` runs". `_next_steps` is not touched. - **NIT-2:** the script-aside tail, where `reads_first` is set, said "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap before both merges are made", which reads as if `make bootstrap` runs it after them. It now says neither runs it until the script is merged, and then `python3 scripts/bootstrap.py` runs it. I did not keep "before both merges": where `X` is given the targets itself, as the sentence before invites, `make bootstrap` runs the shape's bootstrap once the script is merged, with no `Makefile` merge at all. - **NIT-3:** the template's "and never the shape's Makefile" is now ", and not the shape's Makefile unless that file includes it". `ADOPTED_SENTENCES` changes with it. No other copy or regex in `tests/` pins that sentence (`git grep` for "never the shape" and "reads the first of them"). - **NIT-4:** the `collision_follow_up` docstring no longer says `execute` reports through it. It says `plan` predicts the follow-ups and stores them in `follow_ups:`, the split commit carries those, `execute` names its collisions by path only (its `beside` lines and the "WRITTEN BESIDE" list), and `Materialized.follow_ups()` has no caller. The method stays. - NITs 5 to 8 are left to the lane. **Rows of this round:** `tests/test_adopt_collision_make_reads.py` and `tests/test_pinned_copy_text.py`, 118 rows. With this round's test files on `5a4de7f76`'s code, 17 fail and 101 pass; on `b28f40de0` all 118 pass. The 17 fail for the right reason: | rows | why they fail on `5a4de7f76` | |---|---| | 4 collision rows (`Makefile+makefile`, `Makefile+GNUmakefile`, all three, `Makefile+GNUmakefile+scripts`) | the line holds "not with that merge alone" | | 2 new script-tail rows (`GNUmakefile`, `makefile`) | the line holds "before both merges are made" | | 2 new include rows in a real clone (`GNUmakefile`, `makefile`) | `make -n bootstrap` exits 2 before the merge and 0 after it alone (both asserted first, and passing), then the split message holds "not with that merge alone" | | 7 clone rows, the pinned-file row and `test_a_scaffolded_root_is_told_its_makefile_exports_none` | the pinned file still says "and never the shape's Makefile" | **Mutants of this round**, each run against the 118 rows on `b28f40de0`, each file restored and its sha256 checked: | id | mutant | rows failing | result | |---|---|---|---| | MA | ", and not with that merge alone" put back | 6 | killed | | MB | the script-aside tail put back ("before both merges are made") | 3 | killed | | MC | the template's "and never the shape's Makefile" put back | 9 | killed | | MD1 | include rows: `not in message` inverted to `in message` | 2 | killed | | MD2 | include rows: after the merge, `returncode == 0` inverted | 2 | killed | | ME | "holds them" put back | 6 | killed | | MF | "the shape's `bootstrap` target runs" put back | 5 | killed | **Gate** on `b28f40de0`: | files | passed | failed | skipped | |---|---|---|---| | `test_adopt_collision_make_reads`, `test_pinned_copy_text`, `test_adopt_make_reads_first`, `test_adopt_contracts_dir`, `test_adopt_printed_sentences`, `test_adopt_e2e`, `test_repo_hygiene`, `test_scaffold_leg_path_collisions` | 470 | 0 | 0 | | `test_shape_doctor`, 8 shards (62 + 56 + 54 + 56 + 60 + 57 + 58 + 45 collected) | 445 | 0 | 3 (PowerShell rows, Windows host only) | | **total** | **915** | **0** | **3** | **Merge-tree:** `b28f40de0` against main `4ed7c7bf5` is clean, and so is it against each open PR that touches these files: #1104, #1106, #1118, #1119, #1120, #1121 (`1c5173758`), #1122, #1123 and #1125. **Round 0** (`5a4de7f76`, main `6da757cbe` merged in): on main's code (plus the constant alone), 12 of the then 27 rows failed and 15 passed; the 30 test files that run the changed code or read the template gave 2109 passed, 0 failed and 4 skipped (3 PowerShell rows in `test_shape_doctor.py`, 1 MedxEHR row in `test_adopt_plan.py` that needs a clone `SHAPE_MEDXEHR_CLONE` names); nine mutants each failed 1 to 11 of the then 114 rows. **Not here** - The third box of #1048, the `_next_steps` NOTE in `adopt-project.py`, is covered by the change #1047 makes to it: see #1047 and #1121. - `templates/assembly-root/AGENTS-shape.md` line 19 and the five-commands table say `make bootstrap` without this caveat. They say nothing about a kept file, so they are outside #1048's two sentences. Closes #1048 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane 4's W1003 for #1003 (v1.2.0, #970's follow-up), with W1003b's fix round 1 for RV1122's review. Lane 3 places the issue; this is a draft, and a reviewer follows.
#1003 names two defects. Part (a) is already on main, so this PR changes only part (b).
--sourceor recorded clone directory this user cannot enter. This landed in adopt-project.py and family.py: a source this user cannot enter issource-unreadable, a UTF-8 plan reads its source under the C locale, and a member on a branch that is not UTF-8 is put back on it (#918) #1072 (see adopt-project.py and family.py: a--sourcethis user cannot enter, a UTF-8 plan checked under the C locale, and a member branch that is not UTF-8 still end in a traceback or a false refusal #918). Its_refused_at,_top_level(..., refused)and_not_a_whole_clonerefuse it assource-unreadable, andtests/test_adopt_unenterable_source_and_locale.pyholds every case adopt-project.py: a--sourceor recorded clone directory this user cannot enter is cloned again or ends in a PermissionError traceback, and a--sourcetoo long to name is offered a URL that cannot clone #1003 measured. W1003 measured it again (first table below), and this PR does not touch it.--sourcetoo long to name is offered a url that cannot clone. This PR puts it right.What
adopt-project.py,_source_there: when thestatof a spelling that is also anorg/repois refused, the refusal names both readings and offers the url only where GitHub can hold the name. An owner overGITHUB_OWNER_MAX(39) or a repository overGITHUB_NAME_MAX(100) is refused as a path is, whatever reason the operating system gave. The file already imports both limits fromshape_materialize.plan --source <300 a's>/Thingwas refusedsource-unreadable, "cannot be read (File name too long)" (adopt-project.py: a--sourceor recorded source this user cannot reach issource-unreadable, never read as absent or sent togh, and an unreadable leg or link place is named: no 3.12 traceback, no 3.14plan-source-gone(part of #430) #970). It also said "whether it names the path./<300 a's>/Thinghere, or only the GitHub repository<300 a's>/Thing, cannot be told", and offeredhttps://github.com/<300 a's>/Thing.--sourceor recorded source this user cannot reach issource-unreadable, never read as absent or sent togh, and an unreadable leg or link place is named: no 3.12 traceback, no 3.14plan-source-gone(part of #430) #970's own words for a path that cannot be read. The cure follows the operating system's reason:chmod u+rxfor a permission, "put right what it names" for anything else. No url is offered.e75587e7) refused only anENAMETOOLONGthis way, and that went wrong both ways:testorg/<300 a's>beside a./testorgat mode 000 stops atPermission deniedbefore the long part. It was still offeredhttps://github.com/testorg/<300 a's>. Now it is offered no url.testorg/Thing, with./testorga symlink to a name too long, isFile name too longbecause of the link's destination. It losthttps://github.com/testorg/Thing, a repository GitHub can hold, which main offered. Now it is offered again.github.com/andwww.github.com/spellings.statrefused (a permission, an I/O error, a link too long), still names both readings and offers the url.--source's help anddocs/cli.mdalready saysource-unreadablecovers this, so neither changes.Before and after
Part (a), and the first head. W1003 probed as a non-root user (uid 1000), with Python 3.12.3 and with the same Python under a
sitecustomizethat answersPath.exists/is_file/is_diras 3.14 does (proved to bite). The shim's output matched 3.12's on every tree. A stubghrecorded and refused each call;GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/null.06cbd478)plan --source <repo dir at 000>PermissionErrortraceback, exit 1source-unreadable, exit 2, 0ghcallscheck, recordedlocal_pathat 000,cloned: falsesource-unreadable, exit 2cloned: truecloning testorg/Thing, 1ghcallsource-unreadable, 0ghcalls, nothing madeexecute --yes, the sameghcallsource-unreadable, 0ghcallsplan --source <300 a's>/Thingghcalls[www.]github.com/<300 a's>/Thingbeside./[www.]github.comtestorg/<300 a's>beside./testorg./<300 a's>/Thing(a path)That probe ran on main
2a119f28and on the first head's diff as first committed there (5a8f09d8). At this head, the tests' rows below give the same answers for G1 to G3b.Fix round 1.
_source_specin-process, with a realstat, as uid 1000 on Python 3.12.3. No process is started.stat823624d9e75587e7<300 a's>/Thingtestorg/<300 a's>beside./testorgtestorg/Thing,./testorga symlink to<300 a's>https://github.com/testorg/Thinghttps://github.com/testorg/Thingtestorg/<300 a's>beside./testorgat 000<50 b's>/Thingbeside./<50 b's>at 000<39 c's>/Thingbeside./<39 c's>at 000testorg/Thingbeside./testorgat 000Tests
Every run used
/usr/bin/python3 -m pytest -p no:cacheprovider, with the landing gate's environment:ghfirst on PATH,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR;GIT_CONFIG_GLOBALset to an empty file, andGIT_CONFIG_NOSYSTEM=1;EMAIL;GIT_SSH_COMMANDandGIT_ALLOW_PROTOCOLunset.The rows for #1003, in
tests/test_adopt_unsearchable_source.py, #970's file. Each was run with this head's test file against three versions ofadopt-project.py: main's, the first head's and this head's.823624d9e75587e7test_a_spelling_too_long_for_the_disk_is_offered_no_url:[owner],[repository],[bare-github-url],[www-github-url](changed)stat,File name too long: path wording, no url. Its control now refuses, with an I/O error, a name GitHub can hold (an owner of 39, a repository of 100), and expects that name's urldetail(two readings)test_a_name_github_cannot_hold_is_offered_no_url_whatever_the_reason: an owner of 40 or 300, a repository of 101 or 300, andgithub.com/<40>/Thing, each refused with EIO and with EACCES (new, 10 rows)detaildetail(two readings, url)test_a_name_github_can_hold_keeps_its_url_through_a_link_too_long(new)stat,File name too longthrough the link; two readings andhttps://github.com/testorg/Thingdetail(path wording, no url)test_plan_of_a_name_github_cannot_hold_in_an_unsearchable_place(new)plan,./testorgat 000, with thestatasserted to bePermission denied. Exit 2, the refusal line, thechmodcure, no "GitHub repository" and nohttps://; no plan, no work directory,ghnever askedtest_plan_of_a_spelling_too_long_for_the_disk_offers_no_url(unchanged)planof<300 a's>/ThingTotals for the 17 rows: main 16 failed and 1 passed, the first head 12 failed and 5 passed, and this head 17 passed. Each new row fails on the first head; each changed row fails on main, where the first head already passed it.
Skips follow the file's own:
File name too longskip on Windows (needs_a_name_too_long), and the symlink row needs a symlink too (needs_a_symlink);needs_a_mode);Path.statthemselves (refused_for), so they run on every platform and as root, asrefused_stat's rows do.Mutants, each applied to this head's
adopt-project.py(sha256b8bdbdbe...) by exact substitution and run againsttest_adopt_unsearchable_source.pyandtest_adopt_unenterable_source_and_locale.py(57 tests). After each one, the file was restored from its saved bytes and its sha256 checked. All seven were killed, and the file unmutated passes all 57.name is None or exc.errno == errno.ENAMETOOLONG[repository], the 4repository-*rows, row C[owner],[bare-github-url],[www-github-url], the 6owner-*andbare-github-url-*rows, and the childplanof<300 a's>/Thing>=)owner-*andbare-github-url-*controls at 39>=)[repository]control at 100, and the 4repository-*controls at 100Gate, at this head
c01c4d73, each file alone:tests/test_adopt_unsearchable_source.pytests/test_adopt_unenterable_source_and_locale.pytests/test_adopt_source_clone_record.pytests/test_adopt_project_is_the_source.pytests/test_adopt_temp_work_roots.py(the test file imports from it)tests/test_repo_hygiene.pytests/test_scaffold_leg_path_collisions.pytests/test_cli_reference.pytests/test_agents_md_codes.pytests/test_shape_doctor.py, in 8 shards of 56:10510, the PowerShell rows, Windows only)Total: 908 passed and 3 skipped, in 10 files, with no failures. Only the two
source-unreadablefiles reach the changed branch:git grepovertests/finds_source_spec,_source_thereand adopt'ssource-unreadablein no other file.pyflakes on both changed files: no warning.
Merges
git merge-tree --write-tree, at this headc01c4d73:823624d9aaed1f57; its diff from main is byte-identical to the PR's own diff (6da757cb..HEAD), in the same 2 files2a53a8e65c3908f8c04bb710c55e55e3bdcf9952c60066cb504a1dd65a4de7f7551452beOut of scope
.gitis at mode 000, reads as emptied ("git finds no repository there"). Withcloned: trueit is cloned again (1ghcall). It is the same on main and here; lane 4 filed it as adopt-project.py: a recorded clone whose.gitthis user cannot read is called no longer whole and cloned again, where the run should refuse it by name as #1072 refuses a directory it cannot enter #1099 (see adopt-project.py: a recorded clone whose.gitthis user cannot read is called no longer whole and cloned again, where the run should refuse it by name as #1072 refuses a directory it cannot enter #1099).gh, which is asked for a name it cannot hold. It is the same on main and here; see adopt-project.py:_github_nametakes an owner over 39 characters or a repository over 100, soplanasksghto clone a name GitHub cannot hold, and offers its URL beside an unsearchable directory (RV1122's NIT-1 on #1122) #1129.Closes #1003
🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)
Summary by Sourcery
Refuse uncloneable GitHub-style source names without suggesting URLs while retaining repository guidance for valid names.
Bug Fixes:
adopt-project.pyfrom offering GitHub clone URLs fororg/reposource names that exceed GitHub’s owner or repository name limits.Enhancements:
Tests: