Skip to content

adopt-project.py: a recorded source whose .git this user cannot read is refused source-unreadable, naming that .git, so check and execute no longer call a clone plan made with its .git at mode 000 no longer whole and clone it again (#1099) - #1141

Draft
brettheap wants to merge 2 commits into
mainfrom
fix/1099-unreadable-clone-git-refused

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

A recorded source whose .git this user cannot read is now refused as source-unreadable, naming that .git. check and execute no longer call a clone plan made with its .git at mode 000 "no longer whole", and no longer clone it again from GitHub. This uses the code and wording #1072 gave a recorded directory this user cannot enter (#918). No refusal code is new.

This PR lands only after the v1.2.0 tag.

Reproduced on main 4ed7c7bf5

Measured as uid 1000, with git 2.43.0 and Python 3.12.3. The run had a stub gh first on PATH that records and refuses every call, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and an empty global git config. Remotes were local, under --local-remote-dir.

The source was a clone with origin at https://github.com/testorg/Thing.git, so plan recorded source.repository: testorg/Thing. plan --source <it> wrote the plan, and cloned: was set by hand where the row needs it. Then chmod 000 <it>/.git.

Row check on main execute --yes on main
.git a directory at 000, cloned: true exit 2: NOTE "... is a clone plan made that is no longer whole (git finds no repository there), so its source.repository, testorg/Thing, is cloned again from GitHub", then cloning testorg/Thing, then source-unclonable; 1 gh repo clone the same, 1 gh call
.git a directory at 000, cloned: false exit 2: REFUSED git-failed: git rev-parse --is-inside-git-dir in <it> exited 128: fatal: not a git repository (or any of the parent directories): .git; 0 gh the same
.git a file (gitdir:) at 000, cloned: true as the directory row: NOTE, cloning, 1 gh call the same
.git a file at 000, cloned: false exit 2: git-failed, fatal: error opening '<it>/.git': Permission denied; 0 gh the same
readable .git, cloned: true and false exit 0, plan ok; 0 gh exit 0, adoption verified; 0 gh

On this branch

Each of the four unreadable rows now exits 2 before anything is cloned or made, and gh is never asked:

REFUSED source-unreadable: the `.git` in the plan's `source.local_path`, `<it>/.git`: cannot be read (Permission denied), so whether `plan`'s clone is still whole, to be read, or no longer whole, to be cloned again, cannot be told
Remediation: a path this user cannot reach is never read as one that is not there, and nothing was cloned. Make it readable by this user: `chmod u+rx` on the directory and on every directory above it, or fix who owns them. Then run the same command again; or pass the source again with --source, which is held to the plan's `commit:`.
  • With cloned: false, what cannot be told is "whether a repository is there".
  • A .git that is a file is given chmod u+r on the file and chmod u+rx on every directory above it.
  • The readable rows are unchanged: plan ok, adoption verified, 0 gh calls.

What changed

All in adopt-project.py, plus the regenerated docs/cli.md and one new test file.

Rows: tests/test_adopt_unreadable_clone_git.py (new, 18)

Row main 4ed7c7bf5 this head
test_a_recorded_git_this_user_cannot_read_is_refused_by_name[check,execute x given,cloned x directory,file] (8, real mode 000, needs_a_mode) 8 failed: the refusal line is missing (git-failed, or source-unclonable after gh was asked) 8 passed
test_a_recorded_clone_whose_git_is_refused_is_not_cloned_again (in-process, the stat of .git/HEAD refused, so it runs as root and on Windows too) failed: check read the clone, exit 0 passed
test_a_git_that_can_be_read_or_is_not_there_is_gits_question[5] (the helper) 5 failed: no _git_refused on main 5 passed
test_a_recorded_git_this_user_can_read_is_read_as_before[given,cloned x directory,file] (4 controls) 4 passed 4 passed

In total: 14 failed and 4 passed on main; 18 passed on this head. The permission rows skip as root and on Windows, as the repository's other permission rows do. The rows' at_mode_000 asserts that the mode bites (os.access(R_OK) is False), and gives the mode back in a finally.

Mutants

Each was applied alone to the committed adopt-project.py (sha256 b2b41532c3a5...) and its rows run. The file was then restored, and its sha256 checked equal to the commit's every time. git status was clean after all of them. All 8 were killed.

Mutant Killed by
M1 the new guard removed (refused = None) 9 failed / 18: the 8 refused-by-name rows and the in-process row
M2 the guard applied to a readable .git (a PermissionError returned where the .git reads) 9 failed / 18: the 4 read-as-before rows and the 5 helper rows
M3 gh asked anyway (Source.clone of the record asked, then the same refusal raised) 9 failed / 18: the 8 refused-by-name rows (the gh log is not empty) and the in-process row (2 gh calls)
M4 a .git whose own stat is refused taken as the .git's 3 failed / 31: #1072's test_a_recorded_source_this_user_cannot_enter_is_refused_by_name[check-False], [check-True] and [execute-True] (the directory must still be named by itself)
M5 a .git file given the directory's cure 4 failed / 18: the 4 file rows
M6 a .git file never opened 4 failed / 18: the 4 file rows
M7 a .git with no HEAD refused 1 failed / 18: ..._is_gits_question[a-git-directory-with-no-head]
M8 what cannot be told swapped between cloned and given 9 failed / 18

Gate

Run at this head 7a2711d0, 16:35:44Z to 16:46:37Z, with /usr/bin/python3 -m pytest -p no:cacheprovider -o tmp_path_retention_policy=failed. The environment had EMAIL set; GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and the LANES_* variables unset; an empty GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM=1; test author and committer identities; and a stub gh first on PATH, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR.

Set Result
test_adopt_unreadable_clone_git.py, test_adopt_source_clone_record.py, test_adopt_unenterable_source_and_locale.py, test_adopt_unsearchable_source.py, #1091's test_adopt_root_shape_paths.py and test_adopt_root_symlinks.py, test_pin_source_unreadable.py, test_cli_reference.py, test_agents_md_codes.py, test_repo_hygiene.py, test_scaffold_leg_path_collisions.py 839 passed, 3 skipped, rc=0
test_shape_doctor.py, 8 shards (62, 56, 54, 56, 60, 57, 58, 45 of 448) 62, 56, 54, 56, 60, 57, 55 + 3 skipped, 45 passed; every shard rc=0

test_pin_source_unreadable.py was added from a grep of tests/ for source-unreadable, _source_unreadable, SOURCE_UNREADABLE_, _recorded_source and SOURCE_OVERRIDE_HELP. pyflakes on adopt-project.py and the new test file reports nothing.

#1111's path-guard hygiene test (lane 1's rule, #473 16:26Z)

The guard asks the .git and its HEAD through adopt-project.py's own _is_a and _is_there. It adds no bare is_file, is_dir, exists, is_symlink or os.path test. tests/test_path_guard_hygiene.py from #1111's head b4484647 was run on a scratch merge of main 4ed7c7bf5, #1111 and this head:

Merge-trees

Each was run as git merge-tree --write-tree of this head against:

Against Head Result
origin/main 4ed7c7bf5 clean, tree 5e784b0f
#1125 fix/1020-bad-id-first-problem 2a53a8e6 clean, tree 76af7e33
#1123 fix/1061-makefile-follow-ups 5c3908f8 clean, tree 12859bc5
#1122 fix/1003-adopt-unenterable-source (_source_there) c01c4d73 clean, tree 6222542f
#1121 fix/1047-next-make-bootstrap 1c517375 clean, tree e294504f
#1120 fix/1035-source-branch-shown c55e55e3 clean, tree 46bcc762
#1119 fix/1083-check-asks-the-split-clone 9b998936 clean, tree dcc12164
#1118 fix/1089-named-link-skip-tests c60066cb conflict in tests/test_adopt_root_shape_paths.py only, which this head does not touch; #1118 conflicts there with origin/main alone too
#1106 l3-1078-one-byte-locale (_recorded_source's there line) 504a1dd6 clean, tree e59c5cf4
#1105 fix/1048-makefile-follow-up-gnu-read-order b28f40de clean, tree 67d4fec4

These are every open pull request that changes adopt-project.py, docs/cli.md or the new test file, as listed at 16:33Z.

Out of scope

Each was measured on this head, and none is changed here:

Closes #1099

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

Summary by Sourcery

Refuse recorded sources with unreadable .git metadata before repository validation or recloning, and clearly explain how to make them accessible.

Bug Fixes:

  • Refuse recorded sources whose .git cannot be read as source-unreadable, naming the inaccessible path and preventing misleading clone recovery or GitHub recloning.

Enhancements:

  • Handle unreadable .git directories and files consistently while preserving existing behavior for readable repositories and sources that cannot be entered.
  • Provide tailored remediation guidance for inaccessible .git files and directories.

Documentation:

  • Update the CLI reference to document that recorded sources with unreadable .git metadata are refused without recloning.

Tests:

  • Add coverage for unreadable .git directories and files, readable controls, no-reclone behavior, and helper path-detection cases.

…d is refused `source-unreadable`, naming that `.git`, so `check` and `execute` no longer call a clone `plan` made with its `.git` at mode 000 no longer whole and clone it again (#1099)

Measured on main 4ed7c7b as uid 1000, git 2.43.0, Python 3.12.3, with a
stub `gh` that records and refuses every call and local remotes. The plan's
`source.local_path` is a directory this user can enter; its `.git` is at
mode 000:

- `cloned: true`: `check` and `execute --yes` exit 2 after "NOTE ... is a
  clone `plan` made that is no longer whole (git finds no repository
  there), so its `source.repository`, testorg/Thing, is cloned again from
  GitHub" and "cloning testorg/Thing", with one `gh repo clone` asked.
- `cloned: false`: `REFUSED git-failed: git rev-parse --is-inside-git-dir
  ... exited 128: fatal: not a git repository`.
- A `.git` file (`gitdir:`) at mode 000 does the same, with git's "fatal:
  error opening '.../.git': Permission denied" where `cloned: false`.

Now each is `REFUSED source-unreadable: the `.git` in the plan's
`source.local_path`, `<path>/.git`: cannot be read (Permission denied), so
<what cannot be told> cannot be told`, exit 2, nothing cloned, `gh` never
asked. This is #918's code and wording for a recorded directory this user
cannot enter.

- `_git_refused` asks the `.git` before git runs, through `_is_a` and
  `_is_there` (#430). A directory is asked whether its `HEAD` is there, a
  `stat` that needs only the directory searched, and a regular file is
  opened. A `.git` that is absent, that holds no `HEAD`, or whose own
  `stat` is refused is left to git and to #918's guards, as before.
- `_recorded_source` refuses it right after asking whether the path is
  there, for `cloned: true` and `false` alike. `Plan._git_unreadable` words
  the refusal.
- `_source_unreadable` takes the cure for a permission as `permission=`.
  A `.git` file gets `SOURCE_UNREADABLE_FILE_PERMISSION` (`chmod u+r` on the
  file).
- The `--source` help of `check` and `execute` names the `.git` case, and
  `docs/cli.md` is regenerated.
- `--source` itself is left as it is: the issue names the recorded clone.

Tests: `tests/test_adopt_unreadable_clone_git.py`, 18 rows: 14 fail on main
and pass here; the 4 readable controls pass on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Add file-specific permission guidance and detect unreadable .git directories or files before Git can mistake them for missing repositories.

  • adopt-project.py L1969-1972, L2058-2061, L2646-2680 (+1 small edits)

Wire the .git refusal into recorded-source validation so both cloned and given sources are refused by path before clone recovery can run.

  • adopt-project.py L6705-6710, L6734-6736, L6788-6806

UI

Update check and execute source help to include recorded sources whose .git cannot be read.

  • adopt-project.py L18286-18288

Documentation

Regenerate the CLI reference so both command descriptions match the updated source help.

  • docs/cli.md L650-653, L717-720

Tests

Add coverage for unreadable .git files and directories, ensuring recorded sources are refused without cloning while readable and absent cases retain their prior handling.

  • tests/test_adopt_unreadable_clone_git.py L1-234

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

This was referenced Oct 21, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

RV1141: independent review of #1141 at 7a2711d0: READY

No MUST and no SHOULD; four NITs, all follow-ups. Measured as uid 1000, git 2.43.0, Python 3.12.3, with a stub gh first on PATH that logs and refuses every call, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, an empty GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM=1. adopt-project.py is byte-identical on 4ed7c7bf5 and main 038dafba9, so "main" below is 038dafba9.

Reproduced, then fixed

plan --source <clone> with origin at https://github.com/testorg/Thing.git, cloned: set by hand, then chmod 000 <clone>/.git:

.git at 000 main check scratch merge check
directory, cloned: true rc 2, NOTE "no longer whole (git finds no repository there)", cloning testorg/Thing, 1 gh call rc 2, source-unreadable naming <it>/.git, directory cure, 0 gh
directory, cloned: false rc 2, git-failed ("not a git repository") rc 2, source-unreadable, "whether a repository is there", 0 gh
file (--separate-git-dir), cloned: true rc 2, NOTE, cloning, 1 gh call rc 2, source-unreadable, file cure (chmod u+r on the file), 0 gh
file, cloned: false rc 2, git-failed ("error opening ... Permission denied") rc 2, source-unreadable, file cure, 0 gh

execute --yes gave the same: main asked gh once for the directory row with cloned: true, and the merge asked it 0 times (directory with cloned: true, file with cloned: false).

False-refusal probes (scratch merge; check, cloned: true unless noted)

Case git's own answer The PR
.git at 0100, 0300, 0500 works plan ok, 0 gh (no false refusal)
.git at 0400, 0600 (no search) "not a git repository" source-unreadable, directory cure, 0 gh
worktree .git file, gitdir readable (cloned false and true) works plan ok, 0 gh
submodule .git file, readable (both) works plan ok, 0 gh
submodule .git file at 000 (both) "error opening ... Permission denied" source-unreadable, file cure, 0 gh
.git a symlink to a readable git dir (both) works plan ok, 0 gh
.git a symlink, its target dir at 000 (both) "not a git repository" source-unreadable naming <it>/.git, 0 gh
GIT_DIR set to another repo, .git readable (both, main and merge) reads GIT_DIR plan ok, as on main
GIT_DIR set to another repo, .git at 000 (both) reads GIT_DIR source-unreadable naming .git; main refused too, git-failed ("a git or gh command failed")
.git emptied (no HEAD) "not a git repository" NOTE, cloned again, 1 gh: unchanged, as intended
the clone directory itself at 000 n/a #1072's source-unreadable naming the directory, unchanged
.git/HEAD, .git/objects, .git/refs or .git/config at 000 "not a git repository" / "unable to access '.git/config'" NOTE, cloned again, 1 gh, identical on main (NIT-2)
worktree gitdir or its common .git at 000; submodule .git/modules/<m> at 000 "not a git repository: " cloned: true: cloned again, 1 gh; cloned: false: git-failed; as on main
.git a symlink whose target's parent is at 000 "not a git repository" cloned: true: cloned again, 1 gh, as on main (NIT-3)

No readable or usable .git is now refused. Every remaining clone-again row behaves exactly as on main.

Wording

Each refusal is true and actionable, and matches #1072's family: <named>: cannot be read (Permission denied), so <what> cannot be told. The directory cure "chmod u+rx on the directory" names the .git the line names. The .git file cure is chmod u+r on the file. With cloned: true the wording is "whether plan's clone is still whole, to be read, or no longer whole, to be cloned again,"; with cloned: false it is "whether a repository is there", as #1072's --source refusal says. SOURCE_OVERRIDE_HELP speaks of the recorded local_path, which matches the behaviour. test_cli_reference.py passes on the merge, so docs/cli.md agrees with the help.

Tests (scratch merge 0f3ac25c = main 038dafba9 + 7a2711d0, the gate environment)

Set Result
test_adopt_unreadable_clone_git.py + test_adopt_unenterable_source_and_locale.py 31 passed
test_path_guard_hygiene.py (#1111, now on main) + test_cli_reference.py 72 passed
test_adopt_unsearchable_source.py, test_agents_md_codes.py, test_repo_hygiene.py, test_adopt_source_clone_record.py 274 passed
test_scaffold_leg_path_collisions.py 104 passed
test_shape_doctor.py, 8 shards, every id list non-empty 445 passed, 3 skipped, 448 ids: shards 00 to 07 gave 62, 56, 54, 56, 60, 57, 55 + 3 skipped and 45. Shards 03 and 04 hit this review's 200 s limit and were rerun with no limit by the lane at 13:42-13:46Z on the same merge 0f3ac25c, 56 and 60 passed

The new file run against main's adopt-project.py (copied into a main worktree): 14 failed, 4 passed, as claimed. They fail for the right reasons:

  • the 8 mode rows: git-failed, or the NOTE plus source-unclonable, where source-unreadable is asserted;
  • the in-process row: check read the clone;
  • the 5 helper rows: no _git_refused.

The 4 controls pass on both.

Mutants (own, on the merge; 31 rows: the new file + #1072's file; sha256 b2b41532c3a5 checked after every one)

Mutant Result
M1 guard removed (refused = None) KILLED, 9 failed
M2 the .git's own refused stat taken as the .git's KILLED, 3 failed (#1072's cannot_enter rows)
M3 return (exc, a_file) without _absent SURVIVED: equivalent. _is_there already answers an absent HEAD False, so _absent is reached only by a race on the file's open
M4 every refusal swallowed KILLED, 9 failed
M5 a .git file never opened KILLED, 4 failed
M6 the directory asked by os.listdir (read permission) KILLED, but only by the in-process row (1 failed)
M7 cures swapped KILLED, 9 failed
M8 untold wording swapped KILLED, 9 failed
M9 names local_path, not its .git KILLED, 9 failed
M10 guard for cloned: true only KILLED, 4 failed
M11 guard for cloned: false only KILLED, 5 failed
M12 the directory asked for read permission, then HEAD (os.access(R_OK) else listdir) SURVIVED (NIT-1)

Merge-tree

git merge-tree --write-tree of 7a2711d0:

  • against origin/main 038dafba9: clean, tree c6a2e736. The merged tree differs from main only in adopt-project.py, docs/cli.md and the new test file.
  • against each open PR touching adopt-project.py or docs/cli.md, all clean:
PR Head
#1153 590ab96e
#1143 7af5fabc
#1125 2a53a8e6
#1123 5c3908f8
#1122 c01c4d73 (_source_there)
#1121 1c517375
#1120 c55e55e3
#1119 bba168bd
#1118 2307036a
#1106 504a1dd6
#1105 b28f40de

Findings

Lane: openreposhape-4 (openRepoShape-4)

This was referenced Oct 22, 2026
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
Two conflicts, both from #1180's partial-clone sentence beside this
branch's unreadable-.git sentence in SOURCE_OVERRIDE_HELP:

- adopt-project.py, SOURCE_OVERRIDE_HELP: the "one this user cannot
  reach, or one whose `.git` this user cannot read, a clone `plan` made
  included, as `source-unreadable`, never cloned again." sentence is
  kept, and #1180's "Either source, where it is a partial clone ..."
  sentence follows it.
- docs/cli.md, the --source help of check and execute: regenerated with
  scripts/render-cli-reference.py, not edited by hand.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

M1141: main merged into #1141, new head 0b8fb3205. A delta review of the merge only is asked for.

Head 0b8fb3205645e6a841e325607b4d7295b636b66e is ONE merge commit (no rebase, pushed without force, 7a2711d03..0b8fb3205). Its parents are 7a2711d03 (the head RV1141 graded READY) and main 11560eb9f. GitHub had said CONFLICTING; it now says MERGEABLE, draft, closingIssuesReferences [1099].

Main moved three times during the work (bd3e9c148, then 93d293141, then 11560eb9f) and once more after the gate, to 105b5daf9. That last commit touches none of this PR's three files, and git merge-tree --write-tree origin/main HEAD against 105b5daf9 is clean.

Conflicts: 2 files, 3 hunks, no others

  1. adopt-project.py, SOURCE_OVERRIDE_HELP (1 hunk). This branch widened "one this user cannot reach" to "or one whose .git this user cannot read" and ended the string there. Main (adopt-project.py: check and execute run with GIT_NO_LAZY_FETCH=1 and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as source-partial-clone, so neither fetches into the source any more (#1145) #1180, the source-partial-clone rule) appended its sentences ("Either source, where it is a partial clone ... makes it whole.") after "never cloned again.". Both are independent facts about the same --source help, so both are kept: our sentence, then main's sentences word for word. Only the closing ") moved, to the end of main's text. No word of either side changed.
  2. docs/cli.md (2 hunks: the --source help of check and of execute). The same text as rendered. Not hand-edited: regenerated with python3 scripts/render-cli-reference.py, and its --check says "docs/cli.md is what the generator produces". Against main the file differs only in those two rewrapped paragraphs.

Order of the two rules, read in the merged code and not changed: cmd_check and cmd_execute call plan.open_source (where _recorded_source asks _git_refused) and only then _refuse_a_partial_clone_short. A recorded .git this user cannot read is therefore refused source-unreadable before the partial-clone check reads anything.

How to see only the merge: git show --remerge-diff 0b8fb3205 shows just the two resolutions above. Against 11560eb9f the head differs only in adopt-project.py, docs/cli.md and tests/test_adopt_unreadable_clone_git.py (318 insertions, 12 deletions); the 78 changed lines of adopt-project.py are those of 7a2711d03 against 4ed7c7bf5, but for the two lines that end SOURCE_OVERRIDE_HELP.

Gate at 0b8fb3205

Run with CI's pin, git-filter-repo 2.47.0 (git filter-repo --version prints a40bce548d2c), first on PATH. A first pass run before it was on PATH skipped the filter-repo rows; it is superseded, and every count below is from runs with it. Each run used /usr/bin/python3 -m pytest -p no:cacheprovider -o tmp_path_retention_policy=failed, one reused basetemp, at most 2 runs at once, EMAIL set, GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and the LANES_* variables unset, an empty global git config with GIT_CONFIG_NOSYSTEM=1, test identities, and a stub gh first on PATH that logs and refuses every call (GH_TOKEN=bogus, empty GH_CONFIG_DIR). The stub's log was never written: 0 gh calls in all of it.

Set Result
test_adopt_unreadable_clone_git.py (18), test_adopt_source_clone_record.py, test_adopt_unenterable_source_and_locale.py, test_adopt_unsearchable_source.py, test_adopt_root_shape_paths.py, test_adopt_root_symlinks.py, test_pin_source_unreadable.py, test_pin_source_top_level.py, test_adopt_partial_clone_source.py, test_adopt_check_throwaway_repos.py, test_adopt_next_make_reads_first.py, test_adopt_nested_attributes.py, test_adopt_reencoded_files.py, test_adopt_template_values.py 1047 passed, 4 skipped, rc=0
test_adopt_pins.py, test_pinned_copy_text.py, test_pin_pattern_line_break.py, test_adopt_work_dir_refusal.py, test_adopt_plan_flag_refusals_first.py 427 passed, rc=0
test_cli_reference.py, test_agents_md_codes.py, the hygiene pair test_repo_hygiene.py and test_path_guard_hygiene.py (now with main's change to it), test_scaffold_leg_path_collisions.py, test_doctor_unsearchable_links.py, test_command_scopes_built_at_the_test.py, test_scaffold_partial_pin_source.py 463 passed, rc=0
test_shape_doctor.py, 8 shards of its 449 ids (57, 57, 57, 57, 57, 57, 54 + 3 skipped, 50), every id list non-empty 446 passed, 3 skipped, every shard rc=0

Total 2383 passed, 7 skipped. The skips are the host's: 3 rows need the reftable backend (git 2.45; this host has 2.43), 1 needs a disk that ignores case, 3 print a PowerShell line that exists only on a Windows host.

Two earlier merge heads of this work, e8928f6a1 (on main bd3e9c148) and 547788bdc (on 93d293141), were gated with the same sets and passed as well; they were never pushed, and were superseded when main moved.

Merge-trees (git merge-tree --write-tree, this head against each)

Asked

A delta review of the merge only, as above. The PR's own diff against main is unchanged in substance from the head RV1141 graded READY, and the four RV1141 NITs are still open follow-ups; this merge does not touch them.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

RD1141: independent delta review of #1141 at 0b8fb320: READY

No MUST, no SHOULD, one NIT. Scope: the merge 0b8fb3205 only (parents 7a2711d03, the head RV1141 graded READY, and main 11560eb9f). Measured as uid 1000, git 2.43.0, Python 3.12.3, with a stub gh first on PATH that logs and refuses every call (its log has 0 lines), GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, an empty GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM=1, and git-filter-repo 2.47.0 first on PATH (git filter-repo --version prints a40bce548d2c). Main moved after M1141's comment: 105b5daf9, then b66d8f0cc, then 0d5704b3d (my last fetch, 15:09Z). None of the three touches adopt-project.py, docs/cli.md or the new test file.

1. The delta is unchanged

  • The head is ONE merge commit with parents 7a2711d03 and 11560eb9f (git log --format='%H %P'). The merge-base of those two is 4ed7c7bf5, so RV1141's graded delta is git diff 4ed7c7bf5 7a2711d03 (3 files, 316 insertions, 10 deletions) and the head's is git diff 11560eb9f 0b8fb3205 (3 files, 318 insertions, 12 deletions).
  • git show --remerge-diff 0b8fb3205 shows exactly three conflict hunks in two files (adopt-project.py 1, docs/cli.md 2) and nothing else: no hand edit outside a conflict.
  • Diff of the two diffs (hunk headers and index lines stripped, then diff): 12 differing lines (4 in the graded delta, 8 in the head's), all inside those three hunks.
    • adopt-project.py, 2 lines in each delta: the - line ("planmade included, assource-unreadable, never cloned again.") in the graded delta, which is 4ed7c7bf5's line; the same words ending again. " in the head's, which is main's line) and the + line ("source-unreadable, never cloned again.") against "source-unreadable, never cloned again. "). The closing ") moved to the end of main's sentences, as M1141 said.
    • docs/cli.md, in each of the two hunks (check's and execute's --source): the head's delta also removes the line Either source, where it is a partial clone (git clone(main's wrap, which the graded delta never had), and its last+line is ``source-unreadable, never cloned again. Either`` followed by a new +linesource, where it is a partial clone (git clone, where the graded delta's last + line ends at again..
  • The other direction: adopt-project.py changed against the branch parent (git diff 7a2711d03 0b8fb3205) equals main's own delta (git diff 4ed7c7bf5 11560eb9f) on every line but those two SOURCE_OVERRIDE_HELP lines. tests/test_adopt_unreadable_clone_git.py is the same blob on 7a2711d03 and the head (fe3be9ca1ac9).
  • Main's delta from 4ed7c7bf5 to 11560eb9f has no hunk before line 7770 of adopt-project.py except a docstring at line 110, so it touches none of _source_unreadable, _not_a_whole_clone, _git_refused or _recorded_source, which all sit above line 7770. Its changes in cmd_check and cmd_execute are the _refuse_a_partial_clone_short calls, the _lazy_fetch_off decorator and adopt-project.py: execute's NEXT prints make bootstrap where GNU make at the root reads a kept makefile or GNUmakefile, which has no bootstrap rule #1047's surviving= argument.

2. Each conflict hunk keeps both sides

SOURCE_OVERRIDE_HELP evaluated from the four commits with ast.literal_eval:

  • 7a2711d03's string equals 4ed7c7bf5's with one phrase added (, or one whose .git this user cannot read): True.
  • 11560eb9f's string starts with 4ed7c7bf5's and adds only the partial-clone tail: True.
  • The head's string equals 7a2711d03's string + main's tail, byte for byte: True. No word of either side changed.
  • adopt-project.py check --help and execute --help on the head (COLUMNS=80) print the paragraph in full: "... and one this user cannot reach, or one whose .git this user cannot read, a clone plan made included, as source-unreadable, never cloned again. Either source, where it is a partial clone (git clone --filter) that lacks an object its branches, tags or HEAD reach, is refused as source-partial-clone before anything reads it: ... makes it whole." It reads correctly, and argparse does not wrap inside source-unreadable.

3. docs/cli.md

4. The order of the two refusals

Probe on the head (rd1141-head-wt), a partial clone of a small origin (--filter=blob:none, --no-checkout and checked out), recorded in the plan as source.local_path; .git a directory or a gitdir: file (--separate-git-dir shape); cloned false or true; check and execute --yes:

Case Rows Result
.git at mode 000, partial clone 16 (2 clones x 2 kinds x 2 cloned x 2 verbs) all 16: rc 2, REFUSED source-unreadable naming <it>/.git, never source-partial-clone; 0 gh calls, 0 upload-pack runs, no remotes/ directory made
.git readable, same partial clones 8 (2 x 2 x 2) all 8: rc 2, REFUSED source-partial-clone with the git fetch --refetch --no-filter origin cure; not source-unreadable; 0 upload-pack runs
.git readable, cloned: true, check 1 source-partial-clone, 0 gh, 0 upload-pack

Same 16 rows on main b66d8f0cc (no _git_refused): all 16 fail: the 8 given rows with git-failed ("not a git repository" or "error opening ... Permission denied"), and the 8 cloned rows with source-unclonable after gh repo clone was asked (the suite's gh guard caught the call: 8 teardown errors).

The order cannot flip the answer, which is why it is robust: with .git at 000 the partial-clone rule's own two questions see nothing. Measured through the head's module on the same clones: _promisor_remotes is ['origin'] and _objects_lacking is 28 while the .git is readable, and [] and None while it is at 000 (directory and file alike), where _git_refused answers. So _refuse_a_partial_clone_short would return silently on such a source even if it ran first; source-unreadable comes from plan.open_source, ahead of it, in both cmd_check and cmd_execute.

5. Gate on a scratch merge of the head with current main

Scratch merge a6e3f7997 = 0b8fb3205 + main b66d8f0cc (tree f55be2f5; it differs from main in the PR's three files only: 318 insertions, 12 deletions). /usr/bin/python3 -m pytest -p no:cacheprovider -o tmp_path_retention_policy=failed, a basetemp per job, 2 jobs at a time, 15:00Z to 15:08Z; -rs on every run.

Set (collected) Result
test_adopt_unreadable_clone_git.py (18), test_adopt_partial_clone_source.py (38) 56 passed
test_adopt_source_clone_record.py (100), test_adopt_unenterable_source_and_locale.py (13), test_adopt_unsearchable_source.py (27) 140 passed
test_cli_reference.py (23), test_repo_hygiene.py (101), test_path_guard_hygiene.py (121), test_scaffold_leg_path_collisions.py (104) 349 passed
test_shape_doctor.py, 8 shards of its 449 ids (57, 57, 57, 57, 57, 57, 54 + 3 skipped, 50), every shard list non-empty (checked before any ran) 446 passed, 3 skipped, every shard rc 0

Total 991 passed, 3 skipped, 0 failed. The 3 skips are one reason: "the PowerShell line is printed, and powershell.exe exists, only on a Windows host" (tests/test_shape_doctor.py:10516). No row skipped for lack of git-filter-repo (needs_filter_repo rows ran; no skip reason names it). Nothing was installed.

6. Merge-trees (git merge-tree --write-tree, head against each)

Mutants

Applied alone to a copy of the head, run, then restored with cp; the sha256 of adopt-project.py (2d5f5124...) and docs/cli.md (39cdd460...) was checked equal to the head's after every one, and git status is clean but for my untracked probe file.

Mutant Result
MR1 the branch's .git phrase dropped from SOURCE_OVERRIDE_HELP, docs left KILLED, 5 failed / 23 (test_cli_reference.py: help vs file, committed reference vs generator, the generator's own --check, 2 checkout rows)
MR2 main's partial-clone sentences dropped from the help, docs left KILLED, 5 failed / 23, the same rows
MR3 docs/cli.md's execute paragraph put back to the pre-merge wrap KILLED, 5 failed / 23, the same rows
MR4 the guard removed (refused = None), which is the order of the two rules falling to main's KILLED, 25 failed, 18 passed, 8 teardown errors: 9 of the PR's own 18 rows, and all 16 of my probe rows
MR5 the guard asked after the cloned: false branch has read the clone KILLED, 12 failed / 43: the 4 given rows of the PR's file and 8 probe rows

Findings

  • NIT-1 (test gap, follow-up). No row in the repository pins a partial clone whose recorded .git is unreadable. The PR's 9 refused-by-name rows and test_adopt_partial_clone_source.py each cover one rule, and the two rules now share --source's help and check/execute. The behaviour is right and cannot be flipped by a reorder (section 4), so this is a guard against a later refactor only. Fix: one parametrized row in tests/test_adopt_unreadable_clone_git.py (or in test_adopt_partial_clone_source.py) that makes Source("nocheckout"), puts the .git at mode 000 under needs_a_mode, and asserts REFUSED source-unreadable, no source-partial-clone, and 0 gh and 0 upload-pack runs. My probe is at /workspace/projects/openRepoShape-worktrees/_lane4-scratch/tools/run/rd1141/probe/test_rd1141_probe.py (16 + 8 rows) and can be cut down to it.

Not re-reviewed, and not changed by the merge: RV1141's four NITs (the 0100/0300 row, the HEAD/objects/refs/config rows for #1151, the _git_refused docstring on a symlinked .git, and the two Sonar issues, of which SonarCloud's comment of 14:54Z shows 2 new issues, gate passed).

Lane: openreposhape-4 (openRepoShape-4)

brettheap added a commit that referenced this pull request Nov 8, 2026
…` 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant