Repository navigation
adopt-project.py: _default_branch reads origin/HEAD and HEAD by full name and whole, and reads the source only at a branch of its own, so a git clone -b source is no longer git-failed with no remedy, and a HEAD on origin/x, on main<U+00A0> or beside a tag main no longer adopts another branch's commit (#797) - #830
Conversation
…ject.py: `_default_branch` reads `origin/HEAD` and HEAD by full ref and whole, and refuses a remote default branch with no local copy by name, so a `git clone -b` source is no longer `git-failed` with no remedy (#797) Work in progress at the coordinator's usage checkpoint. The four test files this touches passed on these bytes (93 passed, git-filter-repo on PATH); the grep's full set in both modes, the revert table and the PR texts are still to do. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ull name and whole, and reads the source only at a branch of its own, so a `git clone -b` source is no longer `git-failed` with no remedy, and a HEAD on `origin/x`, on `main<U+00A0>` or beside a tag `main` no longer adopts another branch's commit (#797) `_default_branch` read `git symbolic-ref --short` of `origin/HEAD`, then of HEAD, stripped the decoded name and took `origin/` off either, and `Source._at` ran `git rev-parse <name>`. So a `git clone -b dev` source (`origin/HEAD` naming a `main` it has no copy of) ended `plan`, `check` and `execute` `git-failed`, with no remedy and whatever `--tracking-branch` said; a HEAD on `main<U+00A0>` or on a branch `origin/x` read the branch beside it; and a tag `main` made HEAD's name `heads/main`, or answered `git rev-parse main` for `origin/HEAD`'s. Now each is read by its full name, only git's newline is taken off the bytes, and the commit is `refs/heads/<branch>`'s. A default branch the source has no branch of is read at `--tracking-branch` (for `check` and `execute`, at the plan's `source.default_branch`) where that is one of its own; otherwise `origin/HEAD`'s is refused as `source-default-branch-not-local`, with both ways through, and never read at another. The `unsafe-value` refusal of a default branch such as `café` says the way past it. This commit names the literals the new tests repeat; the change itself is the earlier WIP commit, with main 24a977c merged in. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR makes source branch selection exact and byte-preserving, restricts commit reads to verified local branch refs, and replaces ambiguous Git failures with safe fallback behavior or actionable refusals. It also carries the branch selected during planning into check and execute, with extensive regression coverage for clone-specific defaults, stale remote HEADs, detached/unborn HEADs, ref shadowing, and unusual branch names. Sequence diagram for safe source branch selectionsequenceDiagram
participant Plan
participant Source
participant Git
Plan->>Source: Source.open(..., instead=tracking_branch)
Source->>Git: symbolic-ref -q refs/remotes/origin/HEAD
alt origin/HEAD names a local branch
Source->>Git: for-each-ref refs/heads/
Source->>Git: show-ref --verify --hash refs/heads/branch
else default branch is not local
alt tracking_branch is a local branch
Source->>Git: show-ref --verify --hash refs/heads/tracking_branch
else no usable local branch
Source-->>Plan: Refusal source-default-branch-not-local
end
end
Source-->>Plan: Source with selected branch and commit
Sequence diagram for preserving the planned source branchsequenceDiagram
participant Plan
participant Source
participant Git
Plan->>Source: plan with --tracking-branch
Source->>Git: resolve default branch and local commit
Source-->>Plan: record source.default_branch
Plan->>Source: check or execute with plan
Source->>Git: resolve using source.default_branch as instead
Git-->>Source: commit from refs/heads/selected_branch
Source-->>Plan: validate or execute against planned branch
Flow diagram for source default branch resolutionflowchart TD
A[Read origin/HEAD by full symbolic ref] --> B{Valid origin branch reference?}
B -- Yes --> C[Use its branch name]
B -- No --> D[Read HEAD by full symbolic ref]
D --> E{HEAD names a local branch?}
E -- Yes --> F[Use HEAD branch name]
E -- No --> G[Guess main]
C --> H{Branch exists under refs/heads/?}
F --> H
G --> H
H -- Yes --> I[Read refs/heads/branch with show-ref --verify]
H -- No --> J{instead is a local branch?}
J -- Yes --> K[Read refs/heads/instead]
J -- No --> L[Refuse source-default-branch-not-local with remediation]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-17T19:08:24Z) Lane 1's independent review of #830 at 0a369bb: CHANGES NEEDED All five lines of #797 are fixed and none was already fixed on main. Ordinary sources come out byte-identical to main, and plans cross-check both ways. The gates and CI are green, and my re-runs of the revert table match the writer's. Four of the findings need changes:
Each fix is small. Setup:
The issue's lines (measured on main and on the merge)
Findings, ranked1. Where the source has no branch of the name, When Repro:
The same happens through With a
Fix, either of:
2. The new remediation prints The detail spells the name; the remediation line does not. The case below is
Fix: print the 3. The first way through is wrong in the case the tool's own comment describes: a clone made before GitHub renamed The comment is above
The detail's "the default branch of its remote Fix:
4. SonarCloud: one new issue, S1192, at The annotation reads "Define a constant instead of duplicating this literal "refs/heads/" 5 times." The PR adds 5. The remediation promises " Repro: The plan's Fix, either of:
6. Test honesty:
Fix: write 7. (Low) An Setup:
The body names this behaviour change. Under "never at another nobody named", a refusal or a NOTE would fit better. This is the coordinator's call. Tests
Windows
CI
Texts
For the checklist (outside #797)
Lane: openreposhape-1 (openRepoShape-1) |
…h the source has no branch of, reads its commit by `git show-ref --verify`, spells a name its remediation cannot print, and asks a stale `origin/HEAD` again, so no ref named `refs/heads/<b>` is read for a missing branch (#797) r797's review of #830 at 0a369bb: 1. A detached HEAD's guessed `main`, or an orphan HEAD's branch, was returned though the source has no such branch, and `git rev-parse refs/heads/<b>` then found a tag named `refs/heads/main`, a ref `refs/remotes/refs/heads/main` or a branch named `refs/heads/new`. `_default_branch` now returns only a branch of the source's own and refuses any other name as `source-default-branch-not-local`, with `--tracking-branch` as the way through, and `Source._at` reads the commit with `git show-ref --verify --hash`. 2. The remediation prints `git branch <b> origin/<b>` only where the name is printable (a lone surrogate never is); otherwise it spells the name and says the step in words. 3. It offers that command only where `refs/remotes/origin/<b>` exists, and otherwise `git remote set-head origin --auto`; the detail says "the default branch its remote `origin` had when it was last asked". 4. `LOCAL_BRANCHES` is gone for the existing `BRANCH_REFS`, which `_branches_of` now uses too (S1192). 5. The remediation says `check` and `execute` read the source at that branch "while the source has no branch `<b>` of its own". 6. `NBSP_BRANCH` is written with its escape. 7. An `origin/HEAD` naming a ref outside `refs/remotes/origin/` is named in a NOTE as it is passed over. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T19:51:16Z) Lane 1's re-verification of #830 at c2bd75c: PR-READY All seven findings are resolved. This holds once two things happen: CI's I checked on a local merge of Findings 1-71. A ref other than the branch answering for it: resolved. It is fixed both ways the review proposed: (a)
2. Raw names in the remediation: resolved. For U+0085, U+2028, 3. Stale
4. S1192: resolved. SonarCloud at 5. The promise: resolved. It now reads " 6. The U+00A0 literal: resolved. The line is 7.
The ", nor one named Other checks
Text replacement (Why table)This row's "main" cell is false. On main,
I checked the rest of What, Why, How, Tests and Known limits against my probes, and they hold at this head. The U+2028 row is true too: on main it is For the checklist
The Why-table replacement above is applied (the PR body is edited; no push). Lane: openreposhape-1 (openRepoShape-1) |
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="adopt-project.py" line_range="1896-1897" />
<code_context>
+ # (#797): `git rev-parse refs/heads/<b>` looks further, and a tag
+ # named `refs/heads/<b>` answers it. As the bytes git keeps the name,
+ # under any locale (#653).
+ commit = _git_out_exact(["show-ref", "--verify", "--hash",
+ f"{BRANCH_REFS}{branch}"], path).lower()
return cls(path, repository, branch, commit, work_root, cloned)
</code_context>
<issue_to_address>
**Symbolic branch refs can read another ref**
When `refs/heads/<branch>` is a symbolic ref pointing to a tag or another ref, rather than a direct branch ref, `_branch_names` accepts a `refs/heads/<branch>` symbolic ref as a local branch, and `git show-ref --verify --hash` follows that symbolic ref. A `refs/heads/main` symbolic ref pointing to `refs/tags/main` therefore lets the tag’s commit be recorded as the branch’s commit, contrary to the claim that only the branch’s own commit is read.
Reject symbolic refs under `refs/heads/` when determining available local branches, or obtain and read only a direct ref object ID without following symbolic refs.
Also at `adopt-project.py:2188-2191`.
</issue_to_address>
### Comment 2
<location path="adopt-project.py" line_range="2089" />
<code_context>
+ BRANCH_REFS)
+ branch = origins or head or "main"
+ branches = _branch_names(path, BRANCH_REFS)
+ if branch in branches:
+ return branch
+ if instead in branches:
</code_context>
<issue_to_address>
**Deleted branch causes generic failure**
When another process deletes the selected local branch after `_branch_names` lists it but before `show-ref` reads it, `_default_branch` returns the deleted branch, then `Source._at` runs `show-ref --verify` and `_git_out_exact` raises `git-failed`. The command exits with a generic error instead of reselecting an available fallback or returning the missing-branch refusal.
Handle a selected branch disappearing before verification by rechecking the available fallback or returning the named missing-branch refusal.
Also at `adopt-project.py:1896-1897`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and if the branch-selection logic chooses the wrong source branch, the plan can record the wrong commit and execute can create and push split branches based on that commit. Reverting the code would not remove those artifacts, but the plan and pushed branches can be corrected or deleted and the adoption rerun.
Blocking findings: adopt-project.py:1897, adopt-project.py:2089
| commit = _git_out_exact(["show-ref", "--verify", "--hash", | ||
| f"{BRANCH_REFS}{branch}"], path).lower() |
There was a problem hiding this comment.
🟡 Medium · Symbolic branch refs can read another ref
When refs/heads/<branch> is a symbolic ref pointing to a tag or another ref, rather than a direct branch ref, _branch_names accepts a refs/heads/<branch> symbolic ref as a local branch, and git show-ref --verify --hash follows that symbolic ref. A refs/heads/main symbolic ref pointing to refs/tags/main therefore lets the tag’s commit be recorded as the branch’s commit, contrary to the claim that only the branch’s own commit is read.
Reject symbolic refs under refs/heads/ when determining available local branches, or obtain and read only a direct ref object ID without following symbolic refs.
Also at adopt-project.py:2188-2191.
Prompt for AI agents
In `adopt-project.py` at lines 1896-1897:
**Symbolic branch refs can read another ref**
When `refs/heads/<branch>` is a symbolic ref pointing to a tag or another ref, rather than a direct branch ref, `_branch_names` accepts a `refs/heads/<branch>` symbolic ref as a local branch, and `git show-ref --verify --hash` follows that symbolic ref. A `refs/heads/main` symbolic ref pointing to `refs/tags/main` therefore lets the tag’s commit be recorded as the branch’s commit, contrary to the claim that only the branch’s own commit is read.
Reject symbolic refs under `refs/heads/` when determining available local branches, or obtain and read only a direct ref object ID without following symbolic refs.
Also at `adopt-project.py:2188-2191`.| BRANCH_REFS) | ||
| branch = origins or head or "main" | ||
| branches = _branch_names(path, BRANCH_REFS) | ||
| if branch in branches: |
There was a problem hiding this comment.
🟡 Medium · Deleted branch causes generic failure
When another process deletes the selected local branch after _branch_names lists it but before show-ref reads it, _default_branch returns the deleted branch, then Source._at runs show-ref --verify and _git_out_exact raises git-failed. The command exits with a generic error instead of reselecting an available fallback or returning the missing-branch refusal.
Handle a selected branch disappearing before verification by rechecking the available fallback or returning the named missing-branch refusal.
Also at adopt-project.py:1896-1897.
Prompt for AI agents
In `adopt-project.py` at line 2089:
**Deleted branch causes generic failure**
When another process deletes the selected local branch after `_branch_names` lists it but before `show-ref` reads it, `_default_branch` returns the deleted branch, then `Source._at` runs `show-ref --verify` and `_git_out_exact` raises `git-failed`. The command exits with a generic error instead of reselecting an available fallback or returning the missing-branch refusal.
Handle a selected branch disappearing before verification by rechecking the available fallback or returning the named missing-branch refusal.
Also at `adopt-project.py:1896-1897`.…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715) Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path: - `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept. `scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main at 3fb30a8, 37 commits past 24a977c, with #830 (#797, 51414d2) and #833 (#799, 3fb30a8). adopt-project.py conflicted in eight hunks; docs/cli.md merged by itself. Each keeps both sides: - `Source.open`, `Source.of` and `Source.in_place` take #797's `instead` and hand it on to `clone`, `in_place` and `_at`; `in_place` still takes the top level `_top_level` resolved. - `_source_named` (#796) and #799's `OUT_UNWRITABLE` block and `_write_the_plan` stand side by side. - `cmd_plan` asks `--out` first (#799: the flags and the file system alone), then `_source_spec` (#796), both before `_work_root`, and opens the source with `Source.of(named, ..., instead=args.tracking_branch)`. - `Plan.open_source` keeps `override is not None` and the empty `local_path` refusal (#796) and passes `self.read_at` (#797), and `_recorded_source` holds a path the person gave to `_top_level` and passes `self.read_at` at both `Source.in_place` calls. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names no repository's top level is refused by name before git reads anything else in it, so a file, a directory inside a repository, an unknown `~user`, an empty value, `owner/.git` and a lost blob no longer end in a traceback, plan a subtree or reach `gh` (#796) (#831) `adopt-project.py` took a `--source`, or a plan's `source.local_path`, that names no repository's top level as if it did (#796). Measured on main `24a977c0`: - a file (L1-64) ended in `NotADirectoryError`, exit 1, after `plan` had made its work directory; - a directory inside a repository, or `.` run in one (L1-88), planned only that subtree under the repository's name; a directory inside a git directory (`<repo>/.git/objects`, `<bare>.git/refs`) planned the whole repository and recorded the inner path, and `check` and `execute` then failed on it; - `~nouser/x` (L1-95) ended in `RuntimeError`; `--source ''` (L1-96) planned the directory `plan` ran in, and a plan recording `local_path: ""` had `check` clone the plan's repository from GitHub; - `owner/.git` in any case (L1-100) reached `gh repo clone`; `www.github.com/o/r` (L1-101) was refused; and a source that lost a blob (L1-77) ended in `ValueError` over git's `BAD` size. Now: - **`_top_level`**, new: a local source must be a directory; inside a git directory it must be that git directory itself; and `git rev-parse --show-prefix` must print nothing. Otherwise it is refused under a new code, `source-not-top-level`, which names what was given (a file, `src/` inside the repository at its top level, or the git directory it is inside), with the way out: pass the top-level directory, which `git rev-parse --show-toplevel` prints, or its `org/repo`. A git directory itself (`<repo>/.git`, a bare repository, `.git/modules/<m>`, `.git/worktrees/<w>`) is read whole, as on main. - **`_source_spec`** refuses an empty value, expands `~` with `os.path.expanduser` (so an unknown `~user` is `source-unresolvable`, not a traceback), and hands every path that exists to `_top_level`. `plan` reads `--source` before it makes its work directory; only the `--pin-source` refusals and `--out` (#833) come first. In `check` and `execute` the work directory is still asked first, as #650 ordered. - **A recorded `source.local_path`** the person gave is held to `_top_level` too, named by its key, and an empty one is `plan-source-gone`; nothing is cloned for it. A clone `plan` made keeps #749's handling. - **`_github_name`** refuses the repository `.git` in any case, with no `gh` call, and drops a trailing `.git` in any case; `www.github.com` is read as `github.com`. `scripts/repo_shape.py` is unchanged. - **`Source.tree`** refuses a size that is not `-` or digits as `git-failed`, naming the path, the blob and `git fsck`. - **The `source` report line** names the directory read, with the repository its `origin` names beside it; a clone `plan` made is still named by its repository. - **The help** of `plan`'s, `check`'s and `execute`'s `--source` names the new code, and docs/cli.md is regenerated. Review: lane 1's independent adversarial review found defects at `adbc5e37`. The fix round fixed them, and the re-verification found `2d51f181` PR-READY. Main was merged twice, the second time (`db202f53`) over #830's and #833's changes to `adopt-project.py`, and the re-verification found that head PR-READY too. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main three times and was never rebased; the last merge is update-branch's, clean, giving 7f4d015, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 240 test files, 14,677 passed and 69 skipped. Closes #796 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in `Plan.open_source`'s `--source` branch. Main (#830 for #797, #831 for #796) passes `self.read_at` to `Source.open`, tests `override is not None`, and adds the docstring paragraph on an empty override; the branch keeps the clone made at the plan's own `source.local_path` (`_keep_the_clone_at_the_record`). Both are kept: main's docstring, condition and argument, then the branch's call, then `return source`. Auto-merged as both sides wrote it: main's `instead` reaches `Source._at` through the branch's rewritten `Source.clone`; `cmd_plan` asks `--out` (#833) and `_source_spec` (#831) before `_work_root`'s shared-chain check; both sides' identical `import errno` is one line. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he run's own `mkdir` and removed only while it is still that directory, and a `--work-dir` another user can rename in is refused first, so a swapped `source-readonly` no longer turns the removal onto another directory (#803) (#843) `plan` and `check` make a clone in the work directory and, since #749, remove it when no plan records it. That removal went by path, so it could be turned onto another directory (#803). Measured on main `24a977c0`: - **L1-99 (b).** With `source-readonly` swapped for a symlink to a victim, or for the victim itself, at the start of the removal, `check` exited 0 and removed the victim's files. A skipped removal (L1-99 (a)) printed nothing. - **L1-98.** Two runs in one `--work-dir`: run A removed the clone run B had just planned from. - **L1-97.** A clone `check --source` made at the plan's own `source.local_path` was removed. - **L1-102.** On Windows spellings, the clone was kept in silence. - **L1-65.** A file at `source-readonly` ended `plan`, `check` and `execute` in `FileExistsError`. - **A work directory another user could rename** (mode 0777, under a 0777 directory, or a `source-readonly` another user owns) was used as any other, and a swap from the directory above turned the removal onto another tree. Now: - **A work directory that another user can rename in, or rename from above, is refused before anything is made in it,** under a new code, `adopt-work-dir-shared`. `_work_root` asks each directory above the real `--work-dir`, before and after it makes any, then the work directory itself: a directory owned by a user other than this one or root, or that every user can write in without the sticky bit, is refused by name. This is OpenSSH's StrictModes rule, with the sticky bit counted (so `/tmp` stays open) and group write not asked. Where the shared directory is this user's, the way out also names `chmod +t <A>`. A symlink loop or a file above the work directory keeps #650's refusal. Not asked on Windows, nor of the default work directory. - **`source-readonly/` is asked before the NOTE and before `gh`:** a symlink or a file there is `adopt-work-dir-occupied`, a shared one `adopt-work-dir-shared`, and one this user cannot make a directory in `adopt-work-dir-unwritable`. - **The clone's directory is claimed before `gh` runs,** with an exclusive `os.mkdir`; any target already there, an empty one included, is refused as `adopt-work-dir-occupied`. Every directory a run makes for its work directory, `source-readonly/` and the clone is made 0755 whatever the umask, which can still narrow it (0700 under `umask 077`). - **The removal is held to what this run made.** The identity (`st_dev`, `st_ino`) of the clone's directory and of `source-readonly/` is recorded right after the `mkdir`, and the removal goes ahead only while both match, never through a symlink: relative to a descriptor of `source-readonly/` from Python 3.11 on Linux and macOS, and by path after the same checks elsewhere. A skipped removal gets a NOTE; paths are compared with `os.path.normcase`; the plan's own record is kept; and `execute` removes exactly the claim its own `mkdir` made when the clone does not finish. - **What stays open is said in the body's "The residual window" and known limits:** group write, Windows, root, this user's own processes, drives mounted without ownership, and root over another user's directory. - The `--work-dir` help of all three commands names the new refusals, and docs/cli.md is rendered again. Review: lane 1's independent adversarial review found defects at `cf36411e`. Fix round 1 (`bba7e445`) and fix round 2 (`08f7a283`) fixed them, the second after `/workspace/projects` was given the sticky bit on Brett Heap's word. The merge round (`b1a40a64`) merged main over #830's and #831's changes to `Plan.open_source`, and the re-check found that head PR-READY. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main twice and was never rebased; the last merge is update-branch's, clean, giving cfe8e35, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 247 test files, 15,381 passed and 48 skipped. Closes #803 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed git call fails the test and an encoding that cannot spell U+202E skips the bidi test, and six template-value tests skip under a C locale where they raised `UnicodeEncodeError` (#853, #854) (#869) Six test helpers skipped on any failure of `make_source_repo` or of a git call, so a real failure of git read as a skip (#853). The three branch helpers, among them `remembered_on` (the sixth site, added by #830), now ask the system first: whether the file name encoding can spell the name, and, for a name that is not UTF-8, whether the disk can hold it. A git call that then fails fails the test. The U+202E test asks whether the encoding can spell U+202E, and skips where it cannot (#854). So do #845's two U+202E tests in `tests/test_non_utf8_oserror_sites.py`, which would otherwise skip before the disk or git is reached. Beyond the two issues, six tests in `tests/test_adopt_template_values.py` raised `UnicodeEncodeError` under a C locale (`LC_ALL=C LANG=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0`), the same kind of failure as #854, in a file this change already touches. Each now asks `skip_where_the_environment_cannot_carry` first and skips there: - `test_plan_refuses_a_flag_no_file_can_hold`, for `--elected-by` with U+2028, for `--elected-by` with U+202E, and for `--reference` with U+2066 and U+2069; - `test_a_valid_non_ascii_elector_still_adopts`, accented and with a zero-width non-joiner; - `test_a_utf8_source_directory_still_adopts_with_local_remotes`, for the directory `caf` with U+00E9. `windows_fsencode` takes a path, as `os.fsencode` does. Python 3.12 coerces a plain `LC_ALL=C` to UTF-8, so the skips show only with both variables set to 0. Lane 3's independent reviewer RV869 (opus) found it READY (#869 issuecomment-6245399807), and RV869b (opus) found the fix round after it READY (issuecomment-6245721915). After #845 landed, it was restacked onto #845's squash by two merges, with `tests/conftest.py` merged by hand to keep main's newer helpers, and its changed lines equal the reviewed delta (#869 issuecomment-6248744839). It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag" (2026-10-17T13:00:57Z). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-1 (openRepoShape-1)
Claim: #797 (comment)
What
adopt-project.py's_default_branchnames the branchplan,checkandexecuteread the source at. It askedgit symbolic-ref --shortofrefs/remotes/origin/HEAD, then ofHEAD, stripped the decoded name, tookorigin/off whichever answered, andSource._atthen rangit rev-parse <name>. Five things went wrong (#457's L1-68, L1-69 and L1-71, and one more measured beside them):git clone -b dev,origin/HEADnamesmainand onlydevis local.plan,plan --tracking-branch dev, andcheckandexecuteof a plan made before a localmainwas deleted, all endedREFUSED git-failed: git rev-parse main … ambiguous argument 'main', exit 2, in git's words, with no remedy.main<U+00A0>beside a branchmain. The strip took the U+00A0 off, andplanexited 0 recordingmainand the OTHER branch's commit.origin/xbeside a branchx.origin/came off HEAD's name too, andplanexited 0 recordingxand its commit.main, HEAD's short name washeads/main:planrecordedtracking_branch: heads/main, andcheckfound itplan-tracking-branch-missing. Read fromorigin/HEAD, the name wasmain, andgit rev-parse mainanswered with the TAG's commit, whichplanrecorded without a word.café. It is refusedunsafe-value, rightly, with only the pattern as its remedy, though a way past it works.Now:
symbolic-ref -q refs/remotes/origin/HEADandsymbolic-ref -q HEAD; exactlyrefs/remotes/origin/andrefs/heads/come off. Only git's trailing newline is taken off, from the BYTES, before they are decoded withsurrogateescapeas adopt-project.py_default_branchreads the source's branch name throughgit_out, which decodes strictly, so a source whose HEAD (ororigin/HEAD) names a branch that is not UTF-8 endsplan,checkandexecutein a UnicodeDecodeError traceback, exit 1, fromSource.open, with no plan written and no refusal naming the branch #425 decodes them._default_branchreturns only a name the source has underrefs/heads/, andSource._atreads its commit withgit show-ref --verify --hash refs/heads/<branch>, which no other ref can answer:git rev-parse refs/heads/<b>looks further, and a tag namedrefs/heads/mainanswers it.--tracking-branch, or refused by name. Whereorigin/HEADnames a branch with norefs/heads/copy, and also where there is noorigin/HEADand HEAD is detached (somainis guessed) or on a branch with no commit yet, the source is read at the branchplanwas given with--tracking-branch, where that is a branch of its own.checkandexecuteread it at the branch the plan records reading it at (source.default_branch). Otherwise all three refuse it by name under a new code,source-default-branch-not-local, with the ways through, and never read HEAD's branch or any other in its place. Covering the detached and orphan HEAD is wider than the issue asked; the lane's coordinator accepted it.origin/HEAD's branch:git branch <b> origin/<b>where the source hasrefs/remotes/origin/<b>, and otherwisegit remote set-head origin --auto, as after the remote renamed its default branch. The command is printed only where the name is printable; a name holding U+0085, U+2028 or adopt-project.py_default_branchreads the source's branch name throughgit_out, which decodes strictly, so a source whose HEAD (ororigin/HEAD) names a branch that is not UTF-8 endsplan,checkandexecutein a UnicodeDecodeError traceback, exit 1, fromSource.open, with no plan written and no refusal naming the branch #425's byte 0xE9 is spelled as the plan spells it, and the step said in words. For a detached or orphan HEAD: check out the branch to adopt. For all:--tracking-branch.origin/HEADset by hand to a ref outsiderefs/remotes/origin/(another remote's branch) is passed over for HEAD's branch, as no branch oforigin, and a NOTE names it.caférefusal says the way past it: make a branch whose name matches at the same commit (and push it where the source has a remote), then pass it as--tracking-branch.Why (repro on main)
Measured on main
24a977c0and on this branch's head, git 2.43.0, Python 3.12.3, local sources, a stubgh,GIT_CONFIG_GLOBAL=/dev/null,--local-remote-dir. Each source is a few files in one commit, plus the commits a row names. The seed for the clone rows hasmain, anddevone commit ahead of it.git clone -b dev:planREFUSED git-failed: git rev-parse main … ambiguous argument 'main', no remedyREFUSED source-default-branch-not-local(below), no plan fileplan --tracking-branch devgit-faileddefault_branch: dev,dev's commit,tracking_branch: devcheck, thenexecuteplan ok; exit 0adoption verifiedmain, planned, thenmaindeleted withdevchecked out:checkgit-failedREFUSED source-default-branch-not-localexecutegit-failedgit branch main origin/main:checkplan okplan okmaster, renamedmainon the remote and in the clone and pruned,origin/HEADstill onmaster:plangit-failed,git rev-parse master, no remedygit remote set-head origin --autoand nogit branch master origin/master, which cannot run theremain<U+00A0>, a commit ahead ofmain:plandefault_branch: mainandmain's commitREFUSED unsafe-value: --tracking-branch (not given, so the source's default branch) is 'main\xa0', with the way past itplan --tracking-branch maindefault_branch: mainandmain's commitdefault_branch: "main\xa0"and ITS commit,tracking_branch: mainorigin/x, a commit ahead ofx:plandefault_branch: x,x's commit,tracking_branch: xdefault_branch: origin/x, its commit,tracking_branch: origin/xmain, a tagmaina commit behind:plan, thencheckheads/mainas both branches;checkexit 1,plan-tracking-branch-missingmainand the branch's commit;checkexit 0origin/HEADnamingorigin/main:plandefault_branch: mainand the TAG's commitmainand the branch's commitorigin/HEAD, only branchtrunk, a tagrefs/heads/maina commit behind:plangit-failed,git rev-parse mainREFUSED source-default-branch-not-localnamingmain, nothing read at the tagnew, beside a branch namedrefs/heads/new:plangit-failed,git rev-parse newneworigin/HEAD, only branchtrunk:plan --tracking-branch trunkgit-failed,git rev-parse maintrunkorigin/HEADset torefs/remotes/upstream/main, HEAD onfeature:plandefault_branch: upstream/main, that ref's commit, and only a NOTE thatupstream/mainis no branch of the sourcedefault_branch: feature, its commit, and a NOTE namingrefs/remotes/upstream/mainorigin/HEADnamingorigin/ma<U+2028>in, which the source has no branch of:plangit-failed, no remedyma\u2028inthroughout, two lines forstr.splitlinescafé:planunsafe-value, the pattern as the only remedyunsafe-value, with the way past it (below)git branch main:plan --tracking-branch maindefault_branch: "café",tracking_branch: mainmain(control):planThe new refusal, as
planprints it for thegit clone -b devsource (checkandexecuteprint the same):Where
originhas noorigin/<b>, the first sentence of the remediation is instead: "its remoteoriginhas no branch<b>either, as the source last fetched it: askoriginfor its default branch again,git remote set-head origin --auto, run in the source." Where--tracking-branchnames no branch of the source either, the detail adds, nor one named <it>before the branch list. For a detached HEAD:The
caférefusal's remediation now ends:The push is needed for a clone:
checkreads the branches of the source's remoteorigin(#479), so amainmade only in the clone isplan-tracking-branch-missinguntil it is pushed, andplan okafter (probed).How
All in
adopt-project.py;scripts/repo_shape.py(pinned) is unchanged._symbolic_ref(path, ref)readsgit symbolic-ref -q <ref>as bytes, removes one trailing\n, and decodes withsurrogateescape;_branch_under(name, prefix)takes off exactlyprefix, or gives None._branch_names(path, prefix)readsgit for-each-ref --format=%(refname) <prefix>as bytes, each name decoded as_branches_ofdecodes them (_git_path), so membership is exact for every name git keeps, adopt-project.py_default_branchreads the source's branch name throughgit_out, which decodes strictly, so a source whose HEAD (ororigin/HEAD) names a branch that is not UTF-8 endsplan,checkandexecutein a UnicodeDecodeError traceback, exit 1, fromSource.open, with no plan written and no refusal naming the branch #425'scaf\udce9included.BRANCH_REFS(the existingrefs/heads/) is the prefix for the source's own branches, and_branches_ofnow uses it too._default_branch(path, instead=None):origin/HEAD's branch, else HEAD's, elsemain. That branch where it is one of the source's own; elseinsteadwhere that is; else_no_branch_of_its_ownrefuses it, naming what named it (origin/HEAD, an orphan HEAD, or the guess for a detached HEAD), with the ways through;_from_the_remotewords the first way fororigin/HEAD's branch:git branch <b> origin/<b>only whererefs/remotes/origin/<b>exists and the name is printable, the same in words for a name that is not, andgit remote set-head origin --autowhere it does not exist.Source._atreads the commit withgit show-ref --verify --hash refs/heads/<branch>(still through_git_out_exact, adopt-project.py underLC_ALL=C PYTHONUTF8=0: the--descriptionem dash,plan'sgit grepandcheck's submodule url still end in a UnicodeEncodeError before any leg #653).Source.open,in_placeandclonepassinsteadthrough.planpasses--tracking-branchasinstead, as given; it is only compared with the source's branch names here and is checked as before (unsafe-value,adopt-bad-tracking-branch) once the source is open.checkandexecutepass the plan'ssource.default_branch(Plan.read_at), whichplanrecords as the branch it read the source at._checked_default_branchwrapschecked_value(pinned) for the default branch only, and addsDEFAULT_BRANCH_WAY_PASTto its remediation. A--tracking-branchthe person passed is refused as before, word for word._branches_named: the "its branches: …" list_tracking_branch_problembuilt, factored out so the new refusal names branches the same way. The finding's text is unchanged.One behaviour changes beside the issue's rows, probed and not a test here: a
--sourcethat is no git repository is stillgit-failed, now naminggit for-each-ref … refs/heads/where it namedgit rev-parse main.Tests
21 new cases, each failing on main:
tests/test_adopt_tracking_branch.pyclone -b devrefusal and its first way through;--tracking-branch devandcheck;checkaftermainis deleted;executeadopting atdev(needsgit-filter-repo); a renamed remote default, asked again withgit remote set-head origin --auto; detached and orphan HEAD read at--tracking-branch; thecaféway past it, followedtests/test_adopt_branch_names.pyorigin/x; a tagmain, from HEAD and fromorigin/HEAD; a tagrefs/heads/main, a refrefs/remotes/refs/heads/mainand a branchrefs/heads/new, none read for the missing branch; anorigin/HEADonrefs/remotes/upstream/main, named in a NOTEtests/test_adopt_git_reads.pymain<U+00A0>(written"main\u00a0") as HEAD and asorigin/HEAD: refused when not given, read whole with--tracking-branch main; the way through forma<U+2028>inand #425'scaf<E9>, spelled, in two linestests/test_adopt_git_reads.py's #425 helperswith_default_branchandplanned_pasttake the branch name as an argument, defaulting to #425's own, and #425's tests pass unchanged.tests/test_adopt_c_locale_bytes.py'sgonecase, a clone whosecaféwas deleted, now ends insource-default-branch-not-local, said alike underLC_ALL=C PYTHONUTF8=0and in UTF-8 mode; only its two docstrings change.Revert table, at this head: each change reverted alone in
adopt-project.py, against the 21 new cases.adopt-project.py)0a369bba)refs/heads/<b>, the renamed remote default, the NOTE, the two spelled names, and the two whose refusal text changed--short, stripped,origin/off either)origin/x, the tag from HEAD, the NOTE, the fourmain<U+00A0>strip()put back after decodingmain<U+00A0>origin/taken off HEAD's branch tooorigin/xgit rev-parse <branch>by short nameorigin/HEADgit rev-parse refs/heads/<branch>in place ofshow-ref --verifyrev-parsetakes that ref firstrefs/heads/<b>(refusedgit-failedbyshow-ref, not by name)origin/HEAD's branch taken with no local copyclone -b devrefusal,--tracking-branch dev,checkafter the deletion,executeatdev, the renamed remote default, the two spelled namesinsteadignored--tracking-branch dev,executeatdev, detached and orphan HEADinsteadfororigin/HEAD's branch onlycheckandexecutepassing noinstead(Plan.read_atNone)--tracking-branch dev(itscheck),executeatdev, detached and orphan HEAD (theircheck)git branch <b> origin/<b>printed for any namegit branch <b> origin/<b>offered whereorigin/<b>is goneorigin"clone -b devrefusal,checkafter the deletion, the renamed remote defaultcheckandexecutethen read it where the plan did"clone -b devrefusalorigin/HEADoutsideorigincaféway past it droppedcafécaseclone -b devrefusal,checkafter the deletion, the renamed remote default, the two spelled namesTargeted runs at
c2bd75ce(main24a977c0merged in), clean tree before and after, a stubghfirst on PATH, a bogusGH_TOKEN, an emptyGH_CONFIG_DIR,/usr/bin/python3 -m pytest -o tmp_path_retention_policy=failed, over 97 files: the 96 thatgrep -l -E 'adopt-project|adopt_project|\bADOPT\b' tests/*.pyfinds (tests/test_shape_doctor.pyandtests/test_scaffold_leg_path_collisions.pyamong them) andtests/test_cli_reference.py, in four shards per mode:git-filter-repoon PATHgit-filter-repohidden (PATH=/usr/local/bin:/usr/bin:/binafter the stub dir)Each is the previous head's gate (
0a369bba: 6877 passed, and 6497 passed with 393 skipped) plus the 7 new cases, with the same skips.Windows
CI is Linux only (#597). The new tests create branches named
dev,trunk,new,x,origin/x,main,feature,refs/heads/new,café,main<U+00A0>andma<U+2028>in, the tagsmainandrefs/heads/main, and the refsrefs/remotes/refs/heads/mainandrefs/remotes/upstream/main, all legal on NTFS, and read every plan back withload_yaml. Nothing compares"…\n"in bytes of a file written withwrite_text. #425's byte-0xE9 cases skip where git cannot name such a branch, as before. Not run on Windows.Not in this PR
scripts/repo_shape.py'sgit_outandchecked_value(pinned): unchanged.docs/release-notes/: unchanged.update-shape.py,scripts/family.pyandbump-leg.py: update-shape.py, family.py and bump-leg.py: the branch HEAD is on is read with.strip(), so a branch ending in U+00A0, U+2028 or U+0085 is read as its neighbour, and bump-leg.py (and update-shape.py) decode it witherrors="replace", so the push line names a branch that is not there #765, which lane 1 also holds.--helpanddocs/cli.md: unchanged; the refusal carries the way through.checkandexecutestill read the source at its default branch whenever that is a branch of its own; they read at the plan's recorded branch only while it is not (r797's fifth finding; the printed promise says so).Known limits
source.default_branchrecords the branch the source was read at. Read at--tracking-branch devbecause its remote'smainhas no local copy, the plan recordsdefault_branch: dev, andplan-tracking-branch-missingnamesdevas "the source's default branch".--tracking-branchgoes stale once the default branch becomes local. Aftergit branch main origin/main,checkreads the source atmainagain and findsplan-stalewheremain's commit is not the plan's:planis run again.shlex.quote), as the tool's other printed commands are.git branch <b> origin/<b>can bring back a branch the remote has since deleted, where the clone was fetched without--prune; the remediation says to rungit remote set-head origin --autofirst whereoriginmay have changed its default branch, andexecuteasks the remote itself before any leg (adopt-project.py: the tracking branch defaults to the source's default branch, topics are set before the pull request, and NEXT names scripts/bootstrap.py when the shape's Makefile went aside (#479, #480, #481) #493's review, F1).Closes #797
🤖 Generated with Claude Code
Summary by Sourcery
Make source branch selection exact and safe, with reliable fallback behavior and actionable remediation when the default branch is unavailable locally.
Bug Fixes:
origin/, when selecting and validating the source branch.Enhancements:
checkandexecuteso plans remain readable when the source's default branch is unavailable.Tests: