Skip to content

adopt-project.py: execute unsets the leg clone's remote.origin.url 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

Merged
brettheap merged 3 commits into
mainfrom
fix/1247-filter-repo-origin-url
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1247

(lane openRepoShape-4, 2026-11-08T14:44:07Z) Lane 3 places this PR; it carries no milestone here.

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

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:

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 adopt-project.py: under a one-byte locale such as Latin-1, execute stops extracting the spec leg of a source whose path holds U+00E9, because git filter-repo prints the clone's origin url as U+FFFD to a stdout that cannot hold it #1247's source (a directory caf plus the byte 0xE9) in a Latin-1 locale the test builds as adopt-project.py: under a one-byte locale such as Latin-1, check reads a plan written in UTF-8 whose source is there, asking the plan's UTF-8 bytes where the spelling as written names nothing, rather than refusing plan-source-gone (#1078) #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

Merge-trees

git merge-tree --write-tree of this head against:

Overlap with #1214

#1214's WIP guard, in lane 4's unpushed work on fix/1214-latin1-locale-tests, skips test_a_utf8_source_directory_still_adopts_with_local_remotes under Latin-1 by its UTF-8 premise.

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.

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

Summary by Sourcery

Make project adoption robust to source paths that cannot be represented by the locale used by git-filter-repo.

Bug Fixes:

  • Prevent adoption from failing under non-UTF-8 locales when a source path contains bytes that git-filter-repo cannot encode in its output.
  • Preserve branch rewriting and existing behavior when extracting adoption legs, including sources with additional branches.

Enhancements:

  • Safely remove only the cloned origin URL before filtering, while isolating the change from externally configured Git config files and tolerating an absent origin URL.
  • Add coverage for locale-sensitive source paths, config isolation, branch preservation, and error handling for unexpected Git config failures.

Tests:

  • Add regression tests covering origin URL handling and Latin-1 adoption scenarios.

…` 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)

`_extract_leg` clones the source, and the clone's `remote.origin.url` is the source's path, byte for byte. `git filter-repo` prints that url as it removes `origin` (`NOTICE: Removing 'origin' remote ... (was <url>)`), decoded as UTF-8 with `errors='replace'`. Under a Latin-1 locale a directory `caf` plus U+00E9 is the byte 0xE9, the decode makes it U+FFFD, and a Latin-1 stdout cannot hold it: `execute` made both bare leg repositories and stopped at `REFUSED extracting the spec leg`, exit 2, with filter-repo's `UnicodeEncodeError` from `_migrate_origin_to_heads`.

- `_extract_leg` now runs `git config --unset remote.origin.url` in the clone right after its `checkout -B`. filter-repo returns before its NOTICE when the clone has no url, and leaves the remote alone. It still makes each of the clone's `refs/remotes/origin/*` a branch before it asks for the url, so it rewrites every branch of the source as on main.
- Not `git remote remove origin` (#1247's candidate 1): that takes the remote-tracking branches with it, and a leg whose tracking branch holds a `cherry-pick -x` of a commit only another branch reaches then keeps that commit's old id in its message, where main rewrites it. Measured: a different spec-leg tip, the same tree.
- Not `LC_ALL=C.UTF-8` for the child (#1247's candidate 2): it depends on the host's locales, and on Windows `LC_ALL` does not set a pipe's encoding (read, not run). No `PYTHON*` variable reaches a launcher run with `python -E`.
- `tests/test_adopt_filter_repo_origin_url.py`, two rows: `_extract_leg` itself under any locale (no url and every remote-tracking branch when filter-repo starts, no url in its output, the second branch made a branch, the leg pushed), and `plan`, `check` and `execute` for #1247's source in a Latin-1 locale the test builds, with no `PYTHON*` variable but `PYTHONUTF8=0`.

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

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Unset only the cloned leg’s remote.origin.url before git filter-repo, preserving its remote-tracking branches while avoiding locale-sensitive URL output; scope the unset to the clone’s config and continue only when the key is absent.

  • adopt-project.py L1288-1291, L14812-14848

Tests

Add regression coverage for the Latin-1 failure, retained branch rewriting, config-file isolation, missing-origin handling, and refusal of other unset failures.

  • tests/test_adopt_filter_repo_origin_url.py L1-389

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

@brettheap

Copy link
Copy Markdown
Contributor Author

RV1251: independent review of #1251 at 825923c7: NOT READY

(lane openRepoShape-4, independent reviewer RV1251, 2026-11-08T15:34:15Z) One SHOULD and two NITs. The fix works: the defect reproduces on main, the head cures it with both launchers, and the legs come out byte-identical to main's. But the new git config --unset names no file, so the GIT_CONFIG environment variable sends it to a file outside the work directory. Measured: it deletes that file's remote.origin.url, and execute fails where main verifies. The fix is one argument, measured below.

What was measured

Host: git 2.43.0, Python 3.12.3. git-filter-repo 2.47.0 (a40bce548d2c) from lane 4's frvenv (pip's launcher, no -E), and the writer's -E copy of it, used read-only. Latin-1 was built in my own scratch folder: this host has no /usr/share/i18n, so localedef -c -i en_US -f ISO-8859-1 fails ("character map file ISO-8859-1' not found"). I wrote my own charmap and an LC_CTYPEsource, ranlocaledef -c, and set LOCPATH; a child reports iso8859-1for its file system, preferred and stdout encodings. Main was tested atb1f45fa(the PR's base) and at11560eb (origin/mainwhen the review began). The scratch mergee7c11535is11560ebplus825923c`.

1. The defect reproduces on main, with no PYTHON* variable set. My own probe (plan, check, execute --local-remote-dir for a source under caf plus the byte 0xE9, Latin-1, env | grep ^PYTHON empty):

code pip launcher -E launcher
main b1f45fa7 plan 0, check 0, execute 2 plan 0, check 0, execute 2
main 11560eb9 plan 0, check 0, execute 2 plan 0, check 0, execute 2
head 825923c7 execute 0, adoption verified execute 0, adoption verified
merge e7c11535 execute 0, adoption verified execute 0, adoption verified

On main the run ends REFUSED extracting the spec leg with UnicodeEncodeError: 'latin-1' codec can't encode character ... (U+FFFD), raised at print(m) in _migrate_origin_to_heads (git_filter_repo.py line 4462). Both bare legs are left behind.

The existing row test_adopt_template_values.py::test_a_utf8_source_directory_still_adopts_with_local_remotes, under Latin-1:

  • On main, at both b1f45fa7 and 11560eb9: it passes with pip's launcher (1 passed) and fails with the -E copy (1 failed, the same UnicodeEncodeError).
  • On the head it passes with both launchers.
  • With PYTHONIOENCODING=bogus-codec, pip's launcher dies at start-up and the -E copy prints a40bce548d2c.

The writer's claim holds.

2. filter-repo 2.47.0's order (read in frvenv's git_filter_repo.py, sha256 67447413...):

  • _migrate_origin_to_heads (line 4389) first turns every refs/remotes/origin/* into refs/heads/* with one git update-ref --no-deref --stdin. That deletes each remote-tracking ref, origin/HEAD included.
  • Only then does it check if b'remote.origin.url' not in self._config_settings: return (line 4418).
  • The early return skips three things: the --sensitive-data-removal fetch (execute never passes that flag), the NOTICE that prints the url, and git remote rm origin.
  • It skips no ref deletion. By then the migration has deleted every refs/remotes/origin/* on both main and the head. On main, git remote rm origin deleted only config: the [remote "origin"] section, plus branch.main.remote and branch.main.merge.
  • Measured: the leg clones' refs are identical on main and on the head (8, 10, 10 and 14 refs over the four sources below). Only their config differs.
  • What execute pushes does not change. _push_the_leg pushes HEAD:refs/heads/<tracking> to the leg's url.
  • remote.origin.url is used in two other places: the abort message of sanity_check, which --force skips, and the SDR block.

3. The legs are byte-identical to main's (my own probe, rvprobe.py, not probe_legs.py). The sources were built with fixed identities and dates. For each leg's bare repository the probe dumps for-each-ref, cat-file --batch-all-objects --batch-check (every object, reachable or not), rev-list --all --objects, and cat-file -p of every commit, tree and tag. Each row compares the sha256 of that dump:

source main b1f45fa7 = head main 11560eb9 = merge candidate A (git remote remove origin)
standard (3 commits) identical identical identical
second branch release (2 commits) identical identical identical
release + git cherry-pick -x of R onto main identical identical spec leg differs
release + an annotated and a lightweight tag identical identical (not run)
  • The cherry-pick source. On main and on the head the spec leg's tip is 0339cafc, and its message reads (cherry picked from commit d04f2fa8...), which is R as rewritten. Under candidate A the tip is 35b157a7, with the same tree e8315cc4, and the message keeps 71f6473f..., R's id in the source. The rejection of candidate A holds.
  • The mounted submodules (config with the path normalised, and refs) hash the same on main and on the head (649930ba...).

4. git config --unset edge cases.

  • GIT_CONFIG (the constant) is "config" (adopt-project.py:1271), so the command is git config --unset remote.origin.url. It names no file.
  • When the key is absent, git exits 5. run() raises CommandFailed, which is in LATE_FAILURES, so execute prints REFUSED extracting the <role> leg and exits 2, with both legs made.
  • How _extract_leg can reach the unset with no url. A re-run cannot: _extract_leg always clones afresh, and git clone refuses a directory that is not empty. Every form of source becomes a local path that git clone records as remote.<name>.url. Two settings can still leave no remote.origin.url behind, and I measured both:
    • A global clone.defaultRemoteName = upstream. Main ends REFUSED mounting the legs with git remote set-url origin (error: No such remote 'origin'), exit 2, both legs pushed. The head ends earlier, REFUSED extracting the spec leg, with git config --unset remote.origin.url exit 5 and an empty --- output ---, before any push. See NIT-1.
    • The GIT_CONFIG environment variable. See SHOULD-1.
  • The url-less [remote "origin"] is not read again. _push_the_leg pushes to an explicit url, and git submodule add clones the work clone by its path. The mounts' config and refs match main's (above). Afterwards the work clone is read only by rev-parse, rev-list, tree_digest and _tree_of. shape-doctor.py reads no work clone.

5. The rows.

  • Both new rows against main's adopt-project.py (copied into a worktree at the head, from b1f45fa7 and from 11560eb9): 2 failed in each of 4 runs per base (UTF-8 and Latin-1, each with the pip and the -E launcher).
  • On the head: 2 passed in all four combinations.
  • The any-locale row runs (PASSED, not skipped) under LC_ALL=C.UTF-8, under LANG=C.UTF-8 with no LC_ALL (as CI runs), and under LC_ALL=C.
  • The Latin-1 row fails on main under a UTF-8 gate, as claimed.
  • It skips by name where no locale can be built:
    • with a stub localedef that exits 4: localedef could not build en_US.ISO-8859-1 here (exit 4: '[error] rv1251 stub localedef: cannot build here'); a child in it reports ['ascii', 'ascii'];
    • with no localedef on PATH: "no localedef here to build en_US.ISO-8859-1 with".
  • Hygiene: the new file is ASCII, holds no backslash, and is clean under pyflakes and flake8.

6. Mutants (in my own worktree at the head). The rows are both new rows plus the two test_the_extraction_stops_at_a_source_branch_in_its_way rows. After each mutant the file was restored with git checkout and its sha256 checked (a5a3c67b...: OK, tree clean):

mutant UTF-8 Latin-1 killed by
M1 drop the unset 2 failed, 2 passed 2 failed, 2 passed both new rows
M2 the unset moved after git filter-repo 2 failed, 2 passed 2 failed, 2 passed both new rows
M3 git remote remove origin instead 3 failed, 1 passed 3 failed, 1 passed the any-locale row and both split-branch rows
M4 the unset for the code leg only 1 failed, 3 passed 1 failed, 3 passed the Latin-1 row
M5 the unset, then each refs/remotes/origin/* deleted 3 failed, 1 passed 3 failed, 1 passed the any-locale row and both split-branch rows

7. Gate, UTF-8, pip's launcher, with the rules' environment.

origin/main moved twice during the review, so the gate ran on two scratch merges. Neither move touched adopt-project.py.

  • e7c11535 = main 11560eb9 + the head.
  • b073dbcc = main 53bd3171 + the head.
  • After the gate, main moved again to dffc4226, a docstring-only change to scripts/shape_materialize.py. merge-tree with it is clean, and the merged tree differs from main only in the PR's two files.

Gate A, one pytest over 19 files:

  • the new file and test_adopt_template_values.py;
  • the five that name _extract_leg or _push_the_leg: test_adopt_late_failure.py, test_adopt_history_gitlinks.py, test_adopt_filter_repo_failure.py, test_adopt_split_branch.py and test_adopt_plan_escapes.py;
  • test_adopt_e2e.py and test_adopt_partial_clone_source.py;
  • both hygiene files and test_cli_reference.py;
  • test_scaffold_leg_path_collisions.py;
  • test_adopt_git_rm_bytes.py, which the new file imports from, in the same pytest;
  • test_adopt_check_throwaway_repos.py;
  • the files that scan every test or script file: test_no_auto_maintenance.py, test_non_utf8_git_failures.py, test_git_output_decoding.py and test_shape_advisory.py.

tests/test_shape_doctor.py: 449 ids, in 4 shards (118/110/118/103) from a bash script.

tree gate A test_shape_doctor.py
main 11560eb9 (18 files, no new one) 1111 passed, 3 skipped 446 passed, 3 skipped
merge e7c11535 1113 passed, 3 skipped 446 passed, 3 skipped
merge b073dbcc 1113 passed, 3 skipped 446 passed, 3 skipped
  • The two extra passes are the new rows.
  • The skips are the same 6 everywhere, and none is for git-filter-repo (-rs): test_adopt_check_throwaway_repos.py:482 (a disk that ignores case), test_no_auto_maintenance.py:222 x2 (GIT_CONFIG_GLOBAL set), and test_shape_doctor.py:10516 x3 (Windows only).

Latin-1 (new file + test_adopt_template_values.py):

run main 11560eb9 merge e7c11535
pip launcher 4 failed, 69 passed, 5 skipped 4 failed, 71 passed, 5 skipped
-E launcher 5 failed, 68 passed, 5 skipped 4 failed, 71 passed, 5 skipped

8. Merge-trees.

Findings

SHOULD-1: the unset names no file, so GIT_CONFIG in the environment redirects it outside the work directory. git config with no --file, --local or --global takes the file that GIT_CONFIG names (git-config(1)). So run(["git", GIT_CONFIG, "--unset", "remote.origin.url"], cwd=work) unsets the key in that file, not in the clone's own config. Measured with my probe (standard source, UTF-8, GIT_CONFIG naming a file that holds [remote "origin"] url = /home/someone/keep-me.git):

  • Main 11560eb9: plan 0, check 0, execute 0, adoption verified. The file is unchanged.
  • Head 825923c7: execute 2, REFUSED extracting the code leg, git config --unset remote.origin.url, exit 5, with an empty --- output ---. The spec leg is pushed. The file has lost its url line: the spec leg's unset removed it there, and the clone kept its own url. The code leg's unset then found no key.
  • With GIT_CONFIG naming an empty file, both fail execute. Main's filter-repo dies in get_config_settings (ValueError); the head stops at exit 5.

GIT_CONFIG is a legacy variable, and main already writes through it on one narrow path. That path is _resolved's throwaway git config remote.origin.url <base>, for kept relative submodule urls (read, not run). But this PR is the first to write through it on every execute. It also contradicts the comment's and the body's "depends on no locale and no variable".

  • Fix: name the clone's own file, run(["git", GIT_CONFIG, "--file", str(work / ".git" / "config"), "--unset", "remote.origin.url"], cwd=work). work is a fresh, non-bare git clone made with no --separate-git-dir.
    • Measured as a variant: the GIT_CONFIG case gives adoption verified and leaves the file unchanged. The standard and cherry-pick legs are byte-identical to the head's. The Latin-1 0xE9 probe verifies. The four rows used for the mutants give 4 passed under UTF-8 and 4 passed under Latin-1.
    • --local is not the fix: with GIT_CONFIG set, git 2.43 exits 129, error: only one config file at a time.
  • Add a row: _extract_leg (or execute) with GIT_CONFIG naming a file that holds remote.origin.url. The leg is extracted, and the file's bytes are unchanged. On the head it fails, as above.

NIT-1: under clone.defaultRemoteName, the failure moves earlier and says nothing. With a global clone.defaultRemoteName = upstream, main already fails execute at the mount (both legs pushed). The head fails at the unset (exit 5, empty output, nothing pushed). The outcome is not worse, but the new refusal names no cause. Follow-up issue: _extract_leg could clone with --origin origin, and the mount's git remote set-url origin, which fails on main, needs its own answer.

NIT-2: the body says the only difference left is a url-less [remote "origin"]. The work clones also keep branch.main.remote=origin and branch.main.merge=refs/heads/main, which main's git remote rm unset. Nothing reads them: the mounts match main's. The body also calls #1141 a conflict, and it is now clean at 0b8fb320. Body only; no code change is needed.

Not run: Windows, another one-byte locale, a real pipx install, a filter-repo other than 2.47.0, and GIT_CONFIG on a git other than 2.43.

Lane: openreposhape-4 (openRepoShape-4)

…mes the clone's own config file, so a `GIT_CONFIG` in the environment cannot send it to a file of the person's, and a clone with no such url (a global `clone.defaultRemoteName`) goes on, to fail where main fails (#1247)

RV1251 reviewed #1251 at 825923c: one SHOULD and two NITs.

- `_extract_leg` ran `git config --unset remote.origin.url` with no file. With `GIT_CONFIG` in the environment, git writes the file that variable names, so `execute` took the url out of a file of the person's, outside the clone, and ended REFUSED (exit 2) with both legs made; main ends `adoption verified` and leaves that file alone. The call now has `--file <work>/.git/config`. `--local` is no way round it: with `GIT_CONFIG` set, git 2.43 exits 129 (only one config file at a time). `.git` is a directory wherever `execute` reaches the clone: one `git clone` makes it, `work` is an absolute path (`_work_root`), and a `GIT_WORK_TREE` in the environment, which would make the clone another shape, stops `execute` at the legs' `git init --bare` first (measured on main).
- With a global `clone.defaultRemoteName`, the clone's remote is not `origin`, there is no url, and git exits 5: the previous commit refused there, before any leg was pushed and with an empty output block, where main goes on and fails at `mounting the legs` (`No such remote 'origin'`, both legs pushed). `_extract_leg` now goes on from exit 5 and from no other exit; any other is refused as any failed command is. That setting is its own defect (main fails on it too) and is not mended here.
- tests/test_adopt_filter_repo_origin_url.py, three new rows: `execute` with `GIT_CONFIG` naming a file that holds a `[remote "origin"]` url ends `adoption verified` and leaves the file byte for byte; `_extract_leg` itself goes on from exit 5 under a global `clone.defaultRemoteName` and pushes the leg; and an unset that fails for another reason (a `config.lock` in the clone, exit 255) is refused before `git filter-repo` runs. The shared setup of the earlier rows is now two helpers.

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

Copy link
Copy Markdown
Contributor Author

F1251: fix round on #1251 for RV1251's NOT READY. New head abeac66d (one new commit on top of 825923c7, no rebase, no amend, pushed without force). The PR body is corrected to match.

Please do a DELTA review of abeac66d only: git diff 825923c7 abeac66d is two files: adopt-project.py (+22 -1) and tests/test_adopt_filter_repo_origin_url.py (+155 -28). 825923c7 is unchanged beneath it.

SHOULD-1 (the unset named no config file): fixed.

  • The call is now 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.
  • Reproduced with GIT_CONFIG naming a file that holds [remote "origin"] url = ...: main adoption verified and the file unchanged; 825923c7 exit 2 (REFUSED extracting the spec leg, both bare legs made, nothing pushed, the file emptied); this head adoption verified and the file byte for byte as it was.
  • work / ".git" is a directory wherever execute reaches the clone: one git clone makes it, work is an absolute path (_work_root), an exported GIT_DIR does not move a clone, and an exported GIT_WORK_TREE (which would) stops execute first at the legs' git init --bare, on main and on this head alike. A path with no directory gives exit 255, which is refused.

NIT-1 (a clone with no origin url): measured, and handled.

  • A global clone.defaultRemoteName = upstream does make the unset exit 5. Main then fails at mounting the legs (No such remote 'origin', both legs pushed); 825923c7 failed earlier, at the unset, with nothing pushed.
  • _extract_leg now goes on from exit 5 and from no other exit (GIT_CONFIG_KEY_ABSENT = 5), so execute fails where main fails, no earlier and no worse. Every other exit is refused as before.
  • clone.defaultRemoteName itself is NOT mended here. It is a follow-up for its own issue (main fails on it too).

NIT-2: the PR body is corrected. The claim "depends on no locale and no variable" now says the fix names its config file, so GIT_CONFIG cannot redirect it. It lists what stays in the work clones (the url-less [remote "origin"], branch.main.remote, branch.main.merge). The stale "#1141 conflicts" note is dropped: it merges clean at 0b8fb320.

Rows (tests/test_adopt_filter_repo_origin_url.py, now 5; the earlier rows' setup is two helpers):

  • new test_execute_leaves_the_file_git_config_names_alone: execute ends adoption verified with GIT_CONFIG naming a file that holds an origin url, and that file is byte for byte as it was.
  • new test_a_clone_with_no_origin_url_goes_on_to_filter_repo: _extract_leg itself under a global clone.defaultRemoteName; the unset exits 5, is gone on from, and the leg is pushed.
  • new test_an_unset_that_fails_for_any_other_reason_stops_the_leg: a config.lock makes the unset exit 255; CommandFailed, nothing pushed. It needs no git-filter-repo.
  • Counts, the same under UTF-8 and Latin-1 and with pip's launcher and the -E copy: main 4 failed, 1 passed; 825923c7 2 failed, 3 passed (the GIT_CONFIG row and the exit-5 row fail); this head 5 passed. The two earlier rows still fail on main and pass here.

Mutants (each restored, sha256 checked; the five rows plus the two split-branch rows, UTF-8 and Latin-1 alike):

  • M1 drop the unset: 4 failed, 3 passed. M2 unset after filter-repo: 2 failed, 5 passed. M3 git remote remove origin: 5 failed, 2 passed.
  • M4 drop --file: 1 failed (the GIT_CONFIG row). M5 --local: 1 failed (the same row).
  • M6 accept every exit: 1 failed (the lock row). M7 accept none: 1 failed (the exit-5 row).
  • M8 a wrong file (<work>/config, exit 5, carried on): 3 failed (the any-locale row, the Latin-1 row, the lock row).

Gates, on a scratch merge of this head and origin/main 5926ccca (diff against main: the PR's two files):

Merge-trees of abeac66d: clean against main 5926ccca, #1177 (5208e874), #1225 (73c9f57e) and #1141 (0b8fb320); each merged adopt-project.py keeps the --file call.

Left, outside this PR: a global clone.defaultRemoteName still makes execute fail at mounting the legs (main and this head alike); it wants its own issue.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

RD1251: independent delta review of #1251 at abeac66d: READY

(lane openRepoShape-4, independent delta reviewer RD1251, 2026-11-08) No MUST and no SHOULD. RV1251's SHOULD-1 is cured and pinned by a row, the exit-5 carry-on is narrow, the two earlier rows are as strong as before, and the gate is green on a scratch merge with the current origin/main. Four NITs, none blocking. Scope: git diff 825923c7 abeac66d only (adopt-project.py +22 -1, tests/test_adopt_filter_repo_origin_url.py +155 -28).

What was measured

Host: git 2.43.0, Python 3.12.3, git-filter-repo 2.47.0 (frvenv, pip's launcher) and the -E copy, read-only. Latin-1: the w1247 locale via LOCPATH (a child reports iso8859-1 for its file system, preferred and stdout encodings). Trees: head abeac66d; old 825923c7; main 12122952 (origin/main when I began); scratch merge e9d46e0d = 12122952 + abeac66d (merge-tree --write-tree + commit-tree; its diff against main is the PR's two files). origin/main moved twice while I worked, to 9ec0dc79 and then 9729315f (current at posting), so I gated again on a second scratch merge fab44a0d = 9ec0dc79 + abeac66d and merge-tree'd the head with 9729315f; neither move touches _extract_leg. Environment per the lane rules (stub gh, GH_TOKEN=bogus, empty GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM=1, no GIT_SSH_COMMAND).

1. SHOULD-1 is fixed. My own probe (plan, check, execute --local-remote-dir; fixed identities and dates; GIT_CONFIG naming a file that holds [remote "origin"] url = https://example.invalid/keep-me.git, set for execute only):

tree execute verdict the named file
old 825923c7 exit 2 REFUSED extracting the spec leg (filter-repo exit 1, ValueError on the emptied file) CHANGED (now 0 bytes)
main 12122952 exit 0 adoption verified unchanged
head abeac66d exit 0 adoption verified byte-identical (cmp)
merge e9d46e0d exit 0 adoption verified byte-identical (cmp)

The old head fails a little differently from RV1251's run (the spec leg, not the code leg: my file held only the url, so the first unset left it at 0 bytes), the same root. The legs from head and merge hash the same as main's: legs-sha256 322331d052aafd27 for all three. The same equality holds for a release-branch source, a cherry-pick -x source and a tag source (UTF-8, main against head: 0e6aec0a..., 523db0bb..., 5cd20b0c..., each identical), and for a source under caf plus the byte 0xE9 in Latin-1 (old, head and merge verify under both launchers with the same hash; main ends UnicodeEncodeError on U+FFFD, as #1247 says).

2. The exit-5 carry-on is narrow.

  • Exit codes of git config --file <f> --unset remote.origin.url, git 2.43, measured by condition: key present 0; key absent 5; TWO values 5 (with warning: remote.origin.url has multiple values); an existing .git with no config file 5 (silent); config.lock present 255; .git is a file 255 (Not a directory); directory missing 255; syntax error or unreadable file 3; --local with GIT_CONFIG set 129 (only one config file at a time). So only 5 goes on, and a lock, a bad file or a usage is refused. The row for the lock (255) and the mutants below pin that.
  • The comment above the call tells the truth on each point I could test: the 129 under --local, .git a directory made by the clone, work absolute (_work_root: os.path.realpath or mkdtemp; the only caller passes work_root / name), the 255 for a lock, and execute failing where main fails under clone.defaultRemoteName. Probe: a global clone.defaultRemoteName = upstream: main and head both end REFUSED mounting the legs, git remote set-url origin ... exit 2, error: No such remote 'origin', both legs pushed, the same leg hashes. One overstatement, NIT-1 below.
  • Two remote.origin.url values: going on is right. I put the clone's own url last and a first value before it (a stand-in: no git clone of a source leaves two, and a template config with a url did not add a second one to a clone, measured), then ran _extract_leg on main and on the head:
    • UTF-8, source under 0xE9: both extracted.
    • Latin-1, source under 0xE9: both end CommandFailed ... UnicodeEncodeError: 'latin-1' codec can't encode character U+FFFD, filter-repo having met a url, as main does. The unset exited 5 and removed nothing.
    • So going on is exactly main's behaviour and no worse; refusing exit 5 would turn a UTF-8 run that passes on main into a refusal. (--unset-all would also cure the Latin-1 case. Optional, NIT-2.)
  • Other environment variables, main against head, identical outcomes: GIT_DIR naming an empty directory (stops at git rev-parse --is-inside-git-dir), GIT_DIR naming the source's .git (stops at git checkout -B in the clone: reference is not a tree, before the unset), GIT_WORK_TREE naming the out directory (stops at source-not-top-level), and GIT_WORK_TREE with GIT_DIR naming the source's .git (stops at git init --bare), GIT_CONFIG naming an empty file or a file that is not there (filter-repo itself dies, ValueError or unable to read config file), and a work directory named with the byte 0xE9 (verifies, UTF-8 and Latin-1, hash 322331d052aafd27).

3. The refactor keeps the two earlier rows as strong as before. I diffed 825923c7:tests/... against the head by eye and by run. The assertions of both rows are unchanged line for line. Row 1 (test_filter_repo_starts_with_no_origin_url_and_every_branch) keeps its spy on adopter.run and every assertion after the call; its setup moved into extract_the_code_leg, which makes head, the bare leg and work = tmp_path / "work" (WORK == "work") and calls _extract_leg with the same literals. The helper's two git(...) calls now run after the monkeypatch.setattr, but they are conftest.git, not adopter.run, so started is unchanged. Row 2 (the Latin-1 execute row) calls adopt_each_step and assert_adopted: the same four runs with the same arguments, and the same assertions in the same order. Runs:

  • The old two rows' bodies (the file at 825923c7) against the head's adopt-project.py: 2 passed in all four combos (UTF-8 and Latin-1, pip and -E).
  • The new helpers against 825923c7's adopt-project.py: 2 failed (GIT_CONFIG row, exit-5 row), 3 passed, in all four combos; the two earlier rows pass there, as they did before.
  • Strength on mutants, the old file and the new one side by side (both files in one pytest in the mutant worktree, so the same adopt-project.py meets both): M1 (drop the unset) fails both earlier rows of each file; M2 (unset after filter-repo) fails both earlier rows of each file; M3 (git remote remove origin) fails the any-locale row of each file and passes the Latin-1 row of each. The pattern is the same in the old bodies and the refactored ones, in UTF-8 and in Latin-1 (table in point 5).

4. The rows (new file, 5 rows, the same result in UTF-8 and Latin-1 and with pip's and the -E launcher; the Latin-1 row ran, not skipped):

tree UTF-8 pip, E Latin-1 pip, E
main 12122952 4 failed, 1 passed 4 failed, 1 passed
old 825923c7 2 failed, 3 passed 2 failed, 3 passed
head abeac66d 5 passed 5 passed
merge e9d46e0d 5 passed 5 passed

On main the failures are the two earlier rows, the exit-5 row and the lock row; the GIT_CONFIG row passes (main leaves the file alone). On 825923c7 the GIT_CONFIG row and the exit-5 row fail; the lock row passes by design (it refuses every exit). The lock row needs no git-filter-repo: with it off PATH, 1 passed on head and on old, 1 failed on main, UTF-8 and Latin-1; the whole file with it off PATH is 1 passed, 4 skipped, each skip by name ("git filter-repo is not installed").

5. Mutants in my own worktree at the head (sha256 2528d02bcac3 restored and checked after each). Rows: the five plus test_adopt_split_branch.py::test_the_extraction_stops_at_a_source_branch_in_its_way (2 ids), 7 in all; UTF-8 and Latin-1 gave the same result for every mutant. BASE (no mutant): 7 passed in each.

mutant result killed by
M1 drop the unset (F1251) 4 failed, 3 passed (new file + split rows); old-file rows 1 and 2 fail too both earlier rows, the exit-5 row, the lock row
M2 unset after git filter-repo (F1251) 2 failed, 5 passed; old-file rows 1 and 2 fail too both earlier rows
M3 git remote remove origin (F1251) 5 failed, 2 passed (new file + split rows); old-file row 1 fails, its row 2 passes the any-locale row, the exit-5 row, the lock row, both split-branch rows
M4 drop --file (F1251) 1 failed, 6 passed the GIT_CONFIG row
M5 --local (F1251) 1 failed, 6 passed the GIT_CONFIG row
M6 accept every exit (F1251) 1 failed, 6 passed the lock row
M7 accept none (F1251) 1 failed, 6 passed the exit-5 row
M8 --file <work>/config (F1251) 3 failed, 4 passed the any-locale row, the Latin-1 row, the lock row
MA accept 255 as well 1 failed, 6 passed the lock row
MC unset for the code leg only 1 failed, 6 passed the Latin-1 row
MG unset remote.origin.pushurl instead 2 failed, 5 passed the any-locale row and the Latin-1 row
MB accept exit 3 as well 7 passed SURVIVES (see NIT-3)
MJ carry on from 5 for the code leg only 7 passed SURVIVES (see NIT-3)
ME --unset-all (an improvement, not a defect) 2 failed, 5 passed the exit-5 row and the lock row (see NIT-2)

M1 to M8 match F1251's table exactly (M1 to M3 re-run with the old file's two rows alongside, as point 3 says).

6. Gate on the scratch merges e9d46e0d (main 12122952) and fab44a0d (main 9ec0dc79), UTF-8, pip's launcher, at most 2 pytest runs at a time; both gave the same counts:

  • Gate A, one pytest, 23 files: 1518 passed, 3 skipped on e9d46e0d (923 s) and 1518 passed, 3 skipped on fab44a0d (832 s). The files: the new file, test_adopt_split_branch.py, test_adopt_template_values.py, test_adopt_e2e.py; the 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, test_adopt_plan_escapes.py); test_adopt_check_throwaway_repos.py, test_adopt_plan_partial_clone.py, test_adopt_partial_clone_source.py, test_adopt_pins.py, test_adopt_git_rm_bytes.py; test_repo_hygiene.py, test_path_guard_hygiene.py, test_cli_reference.py, test_scaffold_leg_path_collisions.py; 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, test_openreposhape_package.py. This matches F1251's 1518 on 5926ccca.
  • -rs, both runs: 3 skips, 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). grep -c "filter-repo is not installed" over the log: 0.
  • tests/test_shape_doctor.py, 449 ids in 4 shards from a bash script (118/110/118/103): 118, 110, 118 and 100 passed with 3 skipped in the last shard (the PowerShell line, Windows only): 446 passed, 3 skipped, on e9d46e0d; the same shard counts and the same 446 passed, 3 skipped on fab44a0d.
  • Latin-1 (the new file + test_adopt_template_values.py; main runs the template-values file only, so its counts exclude the 5 rows): main pip 4 failed, 69 passed, 5 skipped; scratch merge pip 4 failed, 74 passed, 5 skipped; main -E 5 failed, 68 passed, 5 skipped; merge -E 4 failed, 74 passed, 5 skipped. The 4 are the same rows on both sides (test_plan_refuses_a_flag_no_file_can_hold x2, test_execute_refuses_a_local_source_directory_that_is_not_utf8, test_plan_and_check_say_the_same_sentence), tests: 50 tests in 12 files fail under a Latin-1 locale before and after #1209 (ten of them rows of test_stdout_encoding.py), and whether each is a test that cannot judge under a one-byte locale or a tool defect is not measured (W1196's leftovers; follows #1196) #1214's. The fifth on main under -E is test_a_utf8_source_directory_still_adopts_with_local_remotes, which the merge passes. All as F1251's table.
  • Static: the new file is ASCII, holds no backslash, is clean under pyflakes and flake8; no line is over 79 columns.
  • CI at my last look: SonarCloud Code Analysis, package and tests-git-bash pass; tests was still pending.

7. Merge-trees (git merge-tree --write-tree), all clean:

Findings

NIT-1 (a comment that overstates). GIT_CONFIG_KEY_ABSENT's doc reads "What git config --unset exits with when the key is not there (git 2.43). Every other failure of it is a different number ... which _extract_leg does not go on from." Exit 5 is also what git exits with for two values and for an existing .git with no config file (measured above), and _extract_leg does go on from both. The call's comment is accurate. Fix: say "5: the key is not there, is there twice, or the file is not there (git 2.43); the unset removes nothing in each, and filter-repo then runs as on main". Going on is right in all three (point 2).

NIT-2 (optional hardening). --unset-all instead of --unset would also remove a second url (the stand-in case above: the Latin-1 UnicodeEncodeError would not appear). No clone execute makes holds two values, so this is hypothetical; if taken, the exit-5 row and the lock row, which find the unset by the literal list element "--unset" in args, need ("--unset", "--unset-all") (mutant ME shows both fail on the new spelling).

NIT-3 (two survivors, both low value). MB (exit 3 gone on from) and MJ (exit 5 gone on from for the code leg only) survive: the lock row stands for "any other exit" (255) and the exit-5 row calls the code role only. If wanted, one execute row under a global clone.defaultRemoteName asserting REFUSED mounting the legs with both legs pushed would pin the claim the module docstring and the comment make ("fails where main fails") and kill MJ; I measured that outcome on main and on the head by probe, not in a row. Not needed to land.

NIT-4 (a detail of the PR body). The body's counts and merge-trees cite main 5926ccca; main is 9729315f now and the numbers are unchanged on 12122952 and 9ec0dc79 (Gate A 1518 passed, 3 skipped; merge clean on all three). Nothing to change unless the body is edited again.

Not run: Windows, another one-byte locale, a real pipx install, a git other than 2.43, a filter-repo other than 2.47.0.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap brettheap added this to the v1.2.1 milestone Nov 8, 2026
brettheap pushed a commit that referenced this pull request Nov 8, 2026
…does_not_spell_it_as_utf_8` are held under emulated UTF-8, Latin-1 and ASCII codecs, `validate-family.py` is held to call `escape_unencodable_stdout()` first, and the two rows that can judge under Latin-1 run there (#1214)

RV1258's review of #1258 (S1, S2, N1, N3):

- tests/test_adopt_template_values.py (S1): the premise guard on
  `test_a_utf8_source_directory_still_adopts_with_local_remotes` is gone,
  with the `A_NAME_HELD_AS_UTF_8` import it left unused there. The row
  passes under Latin-1 with the pip-installed git-filter-repo; with a
  launcher started as `python -E` it fails on #1247's traceback until #1251
  lands, which is the row's own failure and not a premise it cannot judge.
  #1258 no longer carries a guard for #1251 to drop.
- tests/test_non_utf8_name_helpers.py (S2): 20 rows hold
  `skip_where_the_locale_does_not_spell_it_as_utf_8` under `os.fsencode`,
  `os.fsdecode` and `sys.getfilesystemencoding` emulated as UTF-8, Latin-1
  and ASCII have them: 15 for the question that skips each of five texts in
  each locale (none, "cannot spell", "writes", "reads") and for the reason's
  encoding, ending and ASCII, and 5 that hold the emulation to this run's
  real locale. A helper that built UTF-8's bytes with `surrogatepass`, or
  dropped, reordered or inverted a question, now fails them under CI's
  UTF-8 locale.
- tests/test_bootstrap_spelling.py (N1): the first-statement row also reads
  the holder's `validate-family.py`, so a call moved after `parse_args`
  fails it.
- tests/test_family_head_by_hand_bytes.py (N3):
  `test_surrogateescape_reads_the_issues_branch_as_caf_and_u_dce9` asks
  `os.fsdecode(b"\xe9")` in its `if`, in place of the helper, so under
  Latin-1 it passes where it skipped; under UTF-8 and ASCII it asserts as
  before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review November 8, 2026 21:16
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026

Copy link
Copy Markdown

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 by commenting @sourcery-ai review. Upgrade to get a review now.

@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap
brettheap merged commit 8e6f532 into main Nov 8, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1247-filter-repo-origin-url branch November 8, 2026 21:18
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