Repository navigation
adopt-project.py: check finds, and execute refuses before any leg, the branch names execute failed on, so adopt_branch: a..b, a source branch Adopt or one hidden by hideRefs no longer pass check and fail after both legs exist (#520, #420) - #583
Conversation
…, the branch names `execute` failed on, so `adopt_branch: a..b`, a source branch `Adopt` or one hidden by `hideRefs` no longer pass `check` and fail after both legs exist (#520, #420) `check` now asks of the plan's `adopt_branch:` and `tracking_branch:` what `execute` asks: `checked_value`'s alphabet first (`unsafe-value`, execute's own words, L2-1 on #457), then git (`git check-ref-format --branch`, #283's `refuse_bad_branch`), as `plan-bad-adopt-branch` and `plan-bad-tracking-branch` in `_entry_findings`, which `execute` refuses before any leg. One finding per value: a bad tracking branch name is no longer also `plan-tracking-branch-missing`, and a bad split branch name is not looked for where the split is pushed. `plan` refuses a `--tracking-branch` git will not create as `adopt-bad-tracking-branch` and writes no plan. `_in_the_way` compares branch names without case too (A4, ruled 2026-10-12 for every system), with the finding saying so. `_branches_at` reads a local repository's own refs with `git for-each-ref`, so a branch hidden by `uploadpack.hideRefs` or `transfer.hideRefs` is seen; the extraction's view of the source stays `git ls-remote`, which a clone obeys, so a hidden branch is a finding about the push into the source alone, and `_refuse_a_split_branch_left_behind` reads the source's own refs under `--local-remote-dir`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 1 day and 12 hours by commenting |
Reviewer's GuideThe PR makes branch-name and split-branch conflict validation match the failures Sequence diagram for preflight branch validation before executionsequenceDiagram
participant User
participant Check as check
participant Execute as execute
participant Git
participant Source
participant Legs as Leg repositories
User->>Check: check(plan)
Check->>Git: refuse_bad_branch(adopt_branch)
Check->>Git: refuse_bad_branch(tracking_branch)
Check->>Source: Inspect split-branch conflicts
Check-->>User: Findings or plan ok
User->>Execute: execute(plan)
Execute->>Execute: _checked_plan_values()
Execute->>Git: refuse_bad_branch(...)
alt Any preflight finding
Execute-->>User: Refuse before creating legs
else Plan passes
Execute->>Legs: Create and push both legs
Execute->>Source: Push split branch
end
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Main since this branch's base bfed143 changes adopt-project.py in one commit, 9f80939 (#567, lane 3): `plan` names an elector it took from `git config user.name` by where it came from, through the hoisted `_flag_named`, and labels the tracking branch through it too. One conflicted hunk, adopt-project.py `_checked_plan_inputs`, the label of `--tracking-branch`: - main built it inline, as `_flag_named(TRACKING_KEY, {TRACKING_KEY} if given else set())`, inside the `checked_value` call; - this branch bound it to `tracking`, which both `checked_value` and #420's `refuse_bad_branch("adopt-bad-tracking-branch", ...)` are given. Resolved keeping both: `tracking = _flag_named(TRACKING_KEY, ...)`, so the label is #567's one spelling and the refusal keeps it. The text is unchanged ("--tracking-branch (not given, so the source's default branch)"). Auto-merged without conflict: docs/cli.md (this branch's `--tracking-branch` help beside main's doctor and bump-leg text; `scripts/render-cli-reference.py --check` passes) and tests/test_adopt_plan_yaml_quoting.py. `_entry_findings` and its docstring are this branch's, untouched by main. No name is bound twice at module level, and nothing this branch added is left unused. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved while this merge was being made: a61cf47 (#589) at the coordinator's request, then 70331aa (#586, shape-doctor.py only) and 2989b4c (#573: family.py, scaffold-project.py, docs/cli.md's scaffold and family sections, their tests). This merges 2989b4c, the tip at merge time. 10 commits since 2e6c6fa: 8890f57, fb786c1 (#581), 26c1600, 2bf8fd4, 654c476 (#597), 3f768a6, 024ea55, a61cf47 (#589), 70331aa (#586), 2989b4c (#573). One conflict, adopt-project.py, the end of `_entry_findings`' return: HEAD (#520) added `names_remediation` to the remediation list, so a misnamed `adopt_branch:` or `tracking_branch:` prints its remedy; main (#589, #449) added `pin_notes` to the notes, so a pin whose digest `check` could not recompute is said once at the end. Resolved by keeping both, each where its side put it: + names_remediation + tracking_remediation + moved_remediation + field_remediation, pin_notes + notes) The two edits touch different elements of the returned tuple, so neither changes what the other means: #589's `digest_findings, pin_notes = _pin_digest_findings(plan)` and its docstring paragraph auto-merged beside #520's `_branch_name_findings` call and paragraph. docs/cli.md auto-merged (#573 changed the scaffold and family sections, this branch the adopt section); `scripts/render-cli-reference.py --check` passes on the merge. Against the new main, each of this branch's six files (adopt-project.py, docs/cli.md, tests/test_adopt_branch_names.py, tests/test_adopt_plan_yaml_quoting.py, tests/test_adopt_split_branch.py, tests/test_adopt_template_values.py) differs by exactly the +/- lines it did against 2e6c6fa at c598ec3 (adopt-project.py: 399). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
One conflict, in `_entry_findings`' return in adopt-project.py: #583 (#520, #420) added its branch-name findings (`names`) between the validator's and the tracking branch's, and this branch added the nested `.gitattributes` and re-encoded root `.gitattributes` findings (#530) after `_root_symlink_findings`. Both are kept, in the order the merged docstring gives: the symlinks, then #530's two, then the unwritable values, the validator, the branch names, the tracking branch, the moved submodules and the late plan values. `names_remediation` was already merged in its place. Nothing else conflicted. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in `_entry_findings`' docstring, where #583 (#420, #520) added the branch-name findings and this branch added #418's: the list now names both, in the order the code returns them, the branch names, then #479, #190 and #418, then #293 and #294. No code conflicted; the rest of adopt-project.py merged as git wrote it. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es in Git for Windows' own bash, so README and the handbook say Git Bash runs the one-line install, tested in CI but for a typed yes, which it cannot take there (#585) (#596) **Fix round 7: main after #598 (3fb21d9) (head 52524fe).** Lane 3's #598 (#595) landed on main as 3fb21d9. It rewrote README step 3's `install.sh` paragraph and the handbook's masthead and `start` for `uv`, and for the two readinesses install.sh now ends with. It also changed `install.sh` and `tests/test_install_sh.py`, and added `tests/test_install_sh_filter_repo.py`. One merge commit, **52524fe3**, merges main 396c0ff: 55faad2 (#591), fbbbc8d (#572), 7fdec0a (#624), 3fb21d9 (#598) and 396c0ff (#570). No other change. - **Conflicts.** README.md had 1 region (step 3's `install.sh` paragraph). docs/handbook.html had 8: the consent rules' `install.sh` item and the seven § Starting markers. `tests/test_install_sh.py` merged as text, and so did the handbook's masthead and `start`. - **The head holds all of main.** `git merge-tree --write-tree 52524fe origin/main` gives 52524fe's own tree, 6d8ffea. `git diff --stat origin/main 52524fe` names only this PR's six files (436+, 147-). **The README and handbook resolution with #598 (52524fe).** - **README, before.** This PR (4c3b72a) had added "; in Git Bash it can take none" after step 3's "each behind a typed `yes` at your terminal". #598 rewrote the same paragraph: "It installs `git-filter-repo` with `pipx`, or `uv`, where it can, or prints the command, runs `openRepoShape --preflight`, and says last if the machine is ready to adopt or only to scaffold. `install.sh` never runs `sudo`; the preflight offers what is missing, each behind a typed `yes` at your terminal." - **Fitting the cap.** Keeping both made nine lines where #598 has eight, and README was at its cap. The cap was not raised. Two rewordings fit it, and no fact is dropped: - "`install.sh` is attached to every release and installs the newest, with no `gh` and no login." became "`install.sh`, attached to every release, installs the newest with no `gh` and no login." - #598's "`install.sh` never runs `sudo`;" became "It never runs `sudo` itself;". "It" is install.sh, the subject of the sentence before it, and "itself" is #564's own word for the contrast with the preflight. - **README, after** (8 lines, at most 80 characters, as 78 README lines on main already are): ``` `install.sh`, attached to every release, installs the newest with no `gh` and no login. Its command is PINNED to the release tag, recorded in `openRepoShape.ref` beside it: `setup.sh` and the standard are fetched at that tag, not at main. It installs `git-filter-repo` with `pipx`, or `uv`, where it can, or prints the command, runs `openRepoShape --preflight`, and says last if the machine is ready to adopt or only to scaffold. It never runs `sudo` itself; the preflight offers what is missing, each behind a typed `yes` at your terminal; in Git Bash it can take none. `| bash -s -- v1.0.1` names a release; the same line again upgrades. ``` #598's `pipx`/`uv` sentence and this PR's clause are word for word. README is **1449** lines, its cap (1422 + 5 for `550.txt` + 22 for `564.txt`). - **The Git Bash clause is still true after #598.** Under `curl … | bash`, `[ -t 0 ] || exec </dev/null` (install.sh:342; #598 moved it down from :277 and left its text alone) leaves the preflight no terminal on stdin. In Git Bash, setup-project.py runs on native Windows Python, where `open_terminal()` returns None (`os.name == "nt"`, setup-project.py:1372). So `can_ask()` is False, and `offer()` takes no `yes`. #598 changes what install.sh does before and after the preflight, not how the preflight reads an answer. - **Handbook, consent rules.** The `install.sh` item keeps #598's text, "installs `git-filter-repo` with `pipx` or `uv` where one is there, and itself never runs `sudo` or installs either of them", and then this PR's clause, word for word: "; in Git Bash it can take no `yes`, so it installs nothing." - **Handbook, masthead and `start`.** They merged as text: this PR's "macOS, Linux, WSL2, Git Bash" heading and PowerShell line, beside #598's "where `pipx` or `uv` can install it … and whether the machine is ready to adopt or only to scaffold". The `start` step-3 paragraph is #598's. It is `data-platform="linux mac wsl"`, as before, so it carries no Git Bash clause. - **The seven § "Starting a project in a new organisation" markers.** This PR's 278a1947 conflicted with #598's d1666fa6. All seven now record `badbb6b273215042ba0e8ae167fd760dc25430ee`, computed with `tests/test_repo_hygiene.py`'s own `readme_segments` and `readme_fingerprint`. That driver reproduces both parents' hashes (main d1666fa6, 4c3b72a 278a1947). All 23 markers match the merged README. - **`tests/test_install_sh.py`.** #598's hunks are `no_pipx` and `no_pipx_but_apt`'s printed commands, the test's docstring, and the line before the printed command, which now starts "there is neither pipx nor uv". They sit beside this PR's `bash_path` in `present_on_path` and in the same test's preflight line. Both sides' tests keep their meaning. **Fix round 6: retarget to main after #587 (22eacee) (head 4c3b72a).** #587 squash-landed on main as 22eacee at 2026-10-15T20:27:52Z, and GitHub retargeted this PR to `main`. 22eacee's tree, 831e71d, is `git merge-tree --write-tree 2b6dbb7 3d8fa05`: main holds exactly #587's final head 3d8fa05. So this round adds two merge commits and no other change: - **3080ac0d** merges #587's final head, 3d8fa05. It conflicted only in README.md and docs/handbook.html, with lane 4's #588 (709eec1), which 3d8fa05 had merged from main. The resolution keeps both sides' text (below). - **4c3b72ae** merges main, 77bb5f0: 22eacee, plus 2b6dbb7 (#584), which the squash carried but 3d8fa05 lacked, and #615's 77bb5f0, which landed after it. Four files conflicted, because the squash is not an ancestor of 3d8fa05 (merge base 62ef256). For each of them, main's copy is the same blob as 3d8fa05's, and 77bb5f0 does not touch it. Each therefore takes this branch's copy, which is 3d8fa05's copy plus this PR's change: | File | `3d8fa05e:` = `origin/main:` (77bb5f0) = `22eaceed:` | |---|---| | README.md | d164131 | | docs/handbook.html | 59ec34d | | tests/test_install_sh.py (add/add) | 34c7de5 | | tests/test_openreposhape_command.py | 5e49547 | No file was merged by hand. `git merge-tree --write-tree 4c3b72a origin/main` gives 4c3b72a's own tree, 97843ec, so the head holds all of main. `git diff --stat origin/main 4c3b72a` names only this PR's six files: `.github/workflows/tests.yml`, README.md, docs/handbook.html, `tests/conftest.py` and the two suites (430+, 141-). **The README and handbook resolution with #588 (3080ac0).** #588 ends README's "Windows, natively" and the handbook's Windows panel with one sentence: "Printed commands are quoted for a POSIX shell, so paste them into Git Bash. Only `shape-doctor.py`'s lines, `setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, `add` and `git rm -r --cached` lines are quoted for PowerShell, on Windows." That sentence now follows this PR's Git Bash sentences, word for word. `tests/test_readme_windows_quoting_sentence.py` holds the handbook to README's words, and it passes. - **README**, before (3d8fa05): "That two-liner IS the Windows way in; the `openRepoShape` command in step 3 below is a macOS, Linux and WSL2 convenience with no Windows twin, because on Windows there is nothing to install. Printed commands are quoted …" - **README**, after: "That two-liner IS the Windows way in from PowerShell, which has no twin of step 3's `openRepoShape` command. Git Bash runs that command and step 3's one line, tested in CI but for a typed `yes`, which its preflight cannot take there. Printed commands are quoted …" - **Fitting the cap.** The two texts side by side came to seven lines where #588 had six, and README was at its cap. So "no twin of the `openRepoShape` command in step 3 below" became "no twin of step 3's `openRepoShape` command". Step 3 is "Install the `openRepoShape` command", so the meaning is the same. The six lines rewrap, and README stays at **1449** lines, its cap (1422 + 5 for `550.txt` + 22 for `564.txt`). No raise. - **#588's other README hunk**, the tighter `python setup-project.py` and "Two commands rather than one pipe" paragraphs, merged as text. - **Handbook Windows panel.** It keeps this PR's "There is no `openRepoShape` command to install in PowerShell … so none of its offers can be accepted there." and then #588's sentence, unchanged. - **The seven § "Starting a project in a new organisation" markers** (this PR's 34723d9a against #588's d77a1e59) now record `278a19479524fe8aae292daaca84492902b140ed`, the merged section's hash. It was computed with `tests/test_repo_hygiene.py`'s own `readme_segments` and `readme_fingerprint`, and all 23 handbook markers match the merged README. - **Consistency.** Every Windows, Git Bash, PowerShell and WSL2 sentence was read again in the merged text. #588's sentence is true in Git Bash too: the named tools run under native Windows Python there, where `os.name` is `nt`, so their lines are quoted for PowerShell, "on Windows", as it says. See checklist candidate 7. **Fix round 5 (head fe58d66).** One comments-only commit to `tests.yml`, for the re-review's N1 and N3. The header says the FULL suite runs on Linux only, so read alone it stays true beside the exception. The job's comment gives the measured "about a minute and a half (86 s on the first run, 50 s of it pytest)" where it said about three minutes. **Fix round 4 (head 5f001de).** Brett Heap merged #597 ("ci: run the test suite on Linux only") at 2026-10-15T16:54:43Z, which removed the Windows job these suites ran in. His word at 2026-10-15T18:20:35Z: "Option 1: keep a small Git Bash job for #596". That round added two commits: - **f6552862** merged lane 3's `l3-564-carry-to-main` b23b245 (#587's branch, this PR's base until fix round 6), which held main 654c476 and so #597. `tests.yml` became that branch's, byte for byte: the guard step and the macOS comment edit went with the two jobs #597 removed. The handbook keeps #597's macOS panel beside this PR's Git Bash text, and its seven markers were moved. - **5f001de1** adds the `tests-git-bash` job, and fixes the macOS sentence that #597 made say "CI runs on Linux only". v1.0.1's one-line install (#564) is meant to work in Git Bash on Windows, but nothing ran it there: `tests/test_openreposhape_command.py` and `tests/test_install_sh.py` wore `WINDOWS_SKIP`. They now run in the `tests-git-bash` job, under Git Bash. **Which bash.** `BASH` in `tests/conftest.py` is Git for Windows' `usr\bin\bash.exe`, found from `git --exec-path`, never from PATH. Probe 1 on `windows-latest` showed why: - A bare `bash` through `subprocess` is System32's WSL launcher. It prints "Windows Subsystem for Linux has no installed distributions." in UTF-16. CreateProcess searches System32 before PATH. - `shutil.which("bash")` finds `Git\bin\bash.EXE`. That launcher puts Git's `mingw64\bin` and `usr\bin` first on PATH, so Git's real `curl` came before the suite's fake one. The old install.sh suite, run that way, reached github.com and printed the real newest release: `release: v1.0.1, the newest release of opensoft/openRepoShape (https://github.com/opensoft/openRepoShape/releases/latest)`. A `git_bash_session` fixture puts Git's `mingw64\bin` (or `clangarm64\bin` on ARM64), `usr\bin` and `MSYSTEM` where Git Bash puts them, behind any fake a test puts first. `BASH_SKIP` replaces `WINDOWS_SKIP` in these two files only. **What the tests now do on Windows.** - **Paths.** Every path handed to bash, or expected in what it prints, is spelled the way Git Bash spells it (`/d/a/...`). The conversion uses Git's own `cygpath` (`bash_path`, `native_path`). Git Bash converts `HOME`, `TMPDIR` and `PATH` itself. - **Line endings.** Every script a test writes for bash is written as LF bytes. The fake's redirect headers and its `gh` tag are written as bytes: Python on Windows prints `\r\n`, and `$(...)` keeps the `\r`. - **The executable bit.** Git Bash's drives are mounted `noacl`, so `chmod 755` changes nothing. A file that starts with `#!` runs anyway. `--install`'s output is checked by mode 755 on POSIX, and by bash's own `test -x` on Windows. - **A test's own PATH.** On Windows it holds `#!/bin/sh` wrappers. A symlink to a Git Bash program cannot load its DLL: with symlinks, `mktemp` failed and install.sh refused `could not make a temporary directory`. - **An argument holding a newline** reaches Git Bash through the environment. On the Windows command line it was split in two: `v1\nforged` arrived as "two versions given". In Git Bash, bash hands bash the argument whole, so the test now does the same. - **`ln -s`.** A new test makes a link with Git Bash's own `ln -s`, after dropping any `$MSYS` the caller set. By default that is a copy, which has no `openRepoShape.ref` beside it and so follows main. With `MSYS=winsymlinks:nativestrict` it is a real link, which reads the record. - **Two anchors (fix round 2).** - The fetched-setup.sh test now also asserts that the parent of the stub's directory exists. Its "the directory is gone" line therefore cannot pass on a path Python cannot open. Both asserts read one `ran_dir = native_path(ran_in)` (fix round 3), so the existence check cannot be reverted to `Path(ran_in)` alone. - The `gh` shim test already could not pass with the shim unreachable: it asserts that the shim's own log holds exactly `auth status`, and only the shim writes that log. Probe 4 reverted its `os.pathsep` and it still passed: the shim stayed reachable either way, most likely through MSYS's conversion of PATH, which the log does not show. **The workflow (fix round 4).** - **The header** keeps #597's text and adds one sentence: "One exception, on Brett Heap's word at 2026-10-15T18:20:35Z, "Option 1: keep a small Git Bash job for #596": the `tests-git-bash` job below runs the launcher's and the installer's suites, and nothing else, on Windows under Git Bash." - **`tests-git-bash`**: `runs-on: windows-latest`, `timeout-minutes: 15`, and no `if:`, so it runs on every trigger the workflow has: a push to main, a pull request, a manual run. Its steps: 1. checkout and setup-python, at the ubuntu job's two commits, with `fetch-depth: 0`; 2. `python -m pip install --disable-pip-version-check pytest==8.3.4`; 3. the git identity, appended to `GITHUB_ENV` in PowerShell, as the old Windows job did; 4. "Git Bash is the bash the launcher and installer suites run": fails the job when conftest finds no Git Bash, and prints the bash the suites will run; 5. `python -m pytest -q -rA tests/test_openreposhape_command.py tests/test_install_sh.py --basetemp=${{ runner.temp }}\t`. - **Its env** is the old Windows job's: `PYTHONUTF8=1`, `core.autocrlf=false` and `core.longpaths=true` through `GIT_CONFIG_COUNT`. - **Its comment** says why it exists (#585, the one-line install on Windows), why it runs two suites only (#597), why its bash is Git's `usr\bin\bash.exe` (pointing to conftest), and why it runs on a push to main. **The docs.** - **README § "Windows, natively" and the handbook's Windows panel** say: PowerShell has no twin of the command; Git Bash runs it and the one-line install, tested in CI but for a typed `yes`, which the preflight the one-line install ends with cannot take there. #588's sentence about POSIX-quoted printed commands follows (fix round 6). - Why: install.sh:342 (:277 before #598) runs `exec </dev/null` under `curl … | bash`, and setup-project.py's `open_terminal()` has no Windows path. - Nothing installs without a typed `yes`, so consent holds. - Fix round 4: this sentence was left with nothing behind it once #597 removed the Windows job. `tests-git-bash` makes it true again, so its text is unchanged. - **README's macOS sentence and the handbook's macOS panel (fix round 4).** #597 made them say "though CI runs on Linux only and no run of it checks them against bash 3.2". With this job that is false. They now say "though no CI run, on Linux or in Git Bash on Windows, checks them against bash 3.2". README rewraps inside the same four lines. - **Step 3's sentence about the preflight's offers** (README) and **the consent rules** (handbook) add that in Git Bash it can take no `yes`. Since fix round 7, both clauses follow #598's `pipx`-or-`uv` text, and README's paragraph is reworded to fit (above). - The handbook's own step-3 install paragraph is `data-platform="linux mac wsl"`, so it is already scoped away from Windows, and is left. - **The WSL2 sentence** says WSL2 "runs every bash entry point in Linux itself", instead of being "still how you run" them. - **The handbook's masthead** lists Git Bash beside macOS, Linux and WSL2. - **README keeps its line count**: 1449, the cap (1422 + 5 for `550.txt` + 22 for `564.txt`). - **The seven handbook markers** for README.md § "Starting a project in a new organisation" record its new hash, `badbb6b273215042ba0e8ae167fd760dc25430ee` (fix round 7, after #598). Fix round 4's merge set them to `a88ac2d0…`, 5f001de moved them to `34723d9a…` for the macOS sentence, and fix round 6 to `278a1947…` after #588. Every hash was computed with `tests/test_repo_hygiene.py`'s own `readme_segments` and `readme_fingerprint`. - **A separate small job, on Brett Heap's word.** At 2026-10-15T18:20:35Z he chose "Option 1: keep a small Git Bash job for #596". The option read: "A small Windows job for just these two suites. The probe runs took 2-3 minutes end to end, so it finishes well inside the 34-36 minute Linux job and adds no wall-clock time. The README claim stays true. It is an exception to #597's decision, so it needs your word." So the job runs these two suites and nothing else. It is not a matrix entry on the ubuntu job, whose actionlint download is a linux-amd64 binary. - **Every trigger, a push to main included.** A squash lands a tree no pull-request run tested (#122), and these two suites are the only run of the one-line install on Windows. At about a minute and a half beside the ubuntu job (86 s on its first run), it adds no wall-clock time. - **What the job keeps from the old Windows job, and why.** - `PYTHONUTF8=1`: the suites decode bash's output with `text=True`, which uses the locale's code page (cp1252) outside UTF-8 mode, and the launcher's `--help` prints em dashes (openRepoShape:108, :119-120). - `core.autocrlf=false` and `core.longpaths=true`: every Windows run of these suites had both, #585's four probes included, and no run without them exists. - `fetch-depth: 0`: `test_a_ref_the_doctor_would_check_out_is_validated` sets `OPENREPOSHAPE_REPO` to this checkout, so the launcher clones it, and every Windows run of it cloned a full one. - `--basetemp=${{ runner.temp }}\t`: the old job's short temp root, for MAX_PATH. - **What it leaves out.** - **git-filter-repo.** These suites never reach a real one. The install suite's PATH holds only its own fakes, `git-filter-repo` and `pipx` among them. The launcher's machine report gets a fake `git-filter-repo` on the PATH `machine_path` builds. On Linux, with every PATH directory that held a `git-filter-repo` removed (`filter-repo=none` in the run log), the two suites gave **113 passed**. `test_it_installs_the_git_filter_repo_ci_runs` still finds the ubuntu job's pin. - **actionlint.** The ubuntu job lints this file. - **`usr\bin\bash.exe`, not `bin\bash.exe`, and not PATH.** The probe-1 lines quoted above. - **`git_bash_session` is needed, though the runner's own PATH hides it.** - The runner's PATH already holds Git's `bin`, `usr\bin` and `mingw64\bin`, with `bin` before System32 (probe-1 PATH dump). So reverting the fixture changes nothing there (probe 2: 113 passed). - A stock Git for Windows install puts only `Git\cmd` on PATH. Probe 3 ran the two files on exactly that PATH, where `which mktemp` is `None` and `which bash` is `C:\Windows\system32\bash.EXE`: - with the fixture: **113 passed**; - with it reverted: **59 failed, 15 passed, 39 errors**, for example `bash: line 199: mktemp: command not found` and `this machine has no mktemp, which the install needs`. - **`bash -s` runs still take their script through a text-mode pipe.** That pipe hands bash CRLF on Windows, but Git Bash's bash drops a carriage return before a newline: a CRLF script piped to it printed `crlf-ok` and exited 0 (probe 1). Sending bytes instead changed no result (mutation `text-stdin`, 113 passed), so that change was dropped. - **Python can make symlinks on the runner** (`os.symlink: ok`), so the three existing symlink tests run there unchanged. - **The macOS sentence names Git Bash without a line.** "No CI run, on Linux or in Git Bash on Windows, checks them against bash 3.2" is true with this job, and fits README's four lines. A clause naming the two suites would have needed a fifth line and a cap raise. - **Not claimed: that setup's own `yes` is asked in Git Bash.** Typed at a Git Bash prompt, `openRepoShape <Project>` reads its `yes` through native Windows Python. Whether that Python sees a console depends on Git Bash's pseudo-console support, and nobody has confirmed it on a Windows machine. - **Linux, on the head 52524fe** (fix round 7, `dirty=0`): - the two suites: **113 passed** in 41.23s; - `tests/test_install_sh_filter_repo.py` (#598's, unchanged): **18 passed** in 7.32s; - `tests/test_repo_hygiene.py`, `tests/test_readme_sections.py`, `tests/test_cli_reference.py`, `tests/test_setup_by_hand_commands.py` and `tests/test_readme_windows_quoting_sentence.py`: **171 passed** in 25.43s. - `git grep -l 'install.sh' tests/` finds `conftest.py`, `readme-cap.d/564.txt`, `test_install_sh.py`, `test_install_sh_filter_repo.py` and `test_repo_hygiene.py`. Every test file among them was in these runs. No skips. - The merge, before its commit: `test_repo_hygiene.py`, `test_readme_sections.py` and `test_readme_windows_quoting_sentence.py`, 144 passed. - **actionlint 1.7.12 on 52524fe**, on `tests.yml` and the two template workflows: exit 0, with shellcheck on PATH. `tests.yml` is the same blob as at 4c3b72a (5fc46de); #598 does not touch it. - **Linux, on the head 4c3b72a** (fix round 6, `dirty=0`): - the two suites: **113 passed** in 28.57s; - `tests/test_repo_hygiene.py`, `tests/test_readme_sections.py`, `tests/test_cli_reference.py`, `tests/test_setup_by_hand_commands.py` and `tests/test_readme_windows_quoting_sentence.py`: **171 passed** in 17.09s. - Those cover every `tests/test_install_sh*.py` (there is one) and every test file `git grep -l 'install.sh' tests/` finds (`test_install_sh.py`, `test_repo_hygiene.py`; the other two hits are `conftest.py` and `readme-cap.d/564.txt`). - 3080ac0's merge, before its commit: `test_repo_hygiene.py`, `test_readme_sections.py` and `test_readme_windows_quoting_sentence.py`, 144 passed. - **actionlint 1.7.12 on 4c3b72a**, on `tests.yml` and the two template workflows: exit 0, with shellcheck on PATH. The workflow keeps main's #593 `concurrency` block byte for byte (this PR's two `tests.yml` hunks are the header and the new job), the header's "The FULL suite runs on LINUX ONLY" with the exception sentence, and `tests-git-bash`. - **Linux, on the head 5f001de** (`dirty=0`): the two suites, `tests/test_repo_hygiene.py`, `tests/test_readme_sections.py`, `tests/test_cli_reference.py` and `tests/test_setup_by_hand_commands.py`: **281 passed** in 61.24s. - **Linux, fix round 4, before its commit:** - the two suites: 113 passed; - the two suites with no `git-filter-repo` on PATH: **113 passed**; - the four docs and hygiene files: 168 passed; - the merge alone, before its commit: `test_repo_hygiene.py` and `test_readme_sections.py`, 141 passed. - **actionlint 1.7.12**, fetched and checksum-checked as the ubuntu job does, on `tests.yml` and the two template workflows: exit 0, with shellcheck on PATH. - **The YAML parse.** Triggers: `push` to `main`, `pull_request`, `workflow_dispatch`. Jobs: `tests` (ubuntu-latest) and `tests-git-bash` (windows-latest, `timeout-minutes: 15`). Neither has an `if:`, so both run on every trigger. - **Linux, earlier rounds:** on 01e8c2c, the same five files without `test_setup_by_hand_commands.py`, 276 passed. Lane 5's re-review got the same 276 there. On 649b089, with `tests/test_setup_by_hand_commands.py`, `tests/test_agents_md_codes.py` and `tests/test_gh_guard.py` as well, 334 passed. - **Windows, `windows-latest`, through probe branches that ran only these suites:** - Probe 1 (run 38752845751): 109 passed, 2 failed, 0 skipped. The two failures were the newline arguments, fixed since. - Probe 2 (run 38754067543, tree ebba347): **113 passed, 0 skipped**, in 68.83s. - Probe 3 (run 38757083883, on 649b089): **113 passed** in 50.97s. On a stock Git for Windows PATH (only `Git\cmd`): 113 passed. - Probe 4 (`l5-585-probe-4`, on 01e8c2c): **113 passed** in 77.92s (run 38769166462). - **The same two files as they were at 5191f0e, with only `WINDOWS_SKIP` lifted** (probe 1): 88 failed, 23 passed. Two causes: the WSL launcher, and the real network through `Git\bin\bash.EXE`. - **Windows on 5f001de, the PR's own run of `tests-git-bash`:** **113 passed** in 50.28s, with the whole job taking 1 min 26 s (18:37:00Z to 18:38:26Z, check run 116601758252). - The guard step printed `C:\Program Files\Git\usr\bin\bash.exe`. - Its steps and env are probe 4's, less the git-filter-repo install, and with `-q -rA` and a 15-minute job timeout. Probe 2 ran these on **ebba347f**, the tree before the squash, which still had a `run_piped` helper; the `text-stdin` row is that helper. The squashed head 649b089 was then re-measured whole by probe 3 (113 passed), and fix round 2's head 01e8c2c by probe 4. Each row reverts one Windows hunk and runs the two files again. | Hunk reverted | Result | |---|---| | `BASH` is the bare word `bash` | 108 failed | | paths keep Windows' spelling | 25 failed | | scripts written with `write_text` (CRLF) | 1 failed: `test_a_release_whose_command_records_no_ref_exits_one`, whose launcher's first line became `#!/usr/bin/env bash\r` and was refused as not the command | | the fake prints its headers and tag in text mode | 22 failed | | mode 755 checked on Windows too | 2 failed | | symlinks on a test's own PATH | 28 failed | | newline arguments on the command line | 2 failed | | no `git_bash_session` | 113 passed on the runner's PATH; on a stock Git for Windows PATH (probe 3) 59 failed and 39 errors, against 113 passed with it | | `bash -s` script sent as bytes (`run_piped`) | 113 passed either way; this hunk was dropped | Fix round 2's anchors (probe 4, on 01e8c2c): | Run | Result | |---|---| | the fetched-setup.sh test, `native_path(ran_in)` reverted to `Path(ran_in)` in both asserts | exit 1: 1 failed. The new parent-directory anchor catches it. Since fix round 3 both asserts read one `ran_dir`, so this is the only way to revert it. | | the `gh` shim test, its PATH joined with `:` instead of `os.pathsep` | exit 0: 1 passed. The shim stays reachable, so the hunk is not load-bearing on this runner. The log shows only that outcome; MSYS's conversion of PATH is the likely reason. The test is still not vacuous: it asserts that the shim's own log holds exactly `auth status`, and only the shim writes that log, so an unreachable shim would fail it. | | both `ln -s` cases with `MSYS=winsymlinks:nativestrict` exported, head | exit 0: 2 passed | | the same, with the `MSYS` drop reverted | exit 1: 1 failed (`ln-default` made a link), 1 passed | #590 adds four cases to these two suites, written while they skipped on Windows: - `test_a_command_that_is_a_symlink_is_refused_before_any_request[live|dangling]`; - `test_install_refuses_a_target_that_is_a_symlink[live|dangling]`. Each compares a path bash printed with `str(path)`. Each also compares `os.readlink`'s answer with the target, and on Windows that answer typically starts `\\?\` (Python's `os.readlink` documentation, since 3.8; the probe's failures stop at the first assert, so no log shows it). Linux passes on the merged tree (117 passed), but under Git Bash those four cases fail, and `tests-git-bash` runs them. **Whichever of #596 and #590 lands second applies these hunks** to the merged tree. Probe 4 (run 38769166462) applied exactly these. ```diff --- a/tests/test_install_sh.py +++ b/tests/test_install_sh.py @@ -668,12 +668,14 @@ def test_a_command_that_is_a_symlink_is_refused_before_any_request( result = net.run() assert result.returncode == 2, result.stdout + result.stderr said = " ".join(result.stderr.split()) - assert f"{link} is a symbolic link, so nothing was installed" in said, said + assert f"{bash_path(link)} is a symbolic link, so nothing was installed" \ + in said, said assert "pipx upgrade openreposhape" in said assert "pipx uninstall openreposhape" in said assert net.calls() == [], net.calls() assert link.is_symlink() - assert link.readlink() == packaged + # Windows reads a link back with `\\?\` ahead of its target. + assert str(link.readlink()).removeprefix("\\\\?\\") == str(packaged) if not dangling: assert packaged.read_bytes() == original assert sorted(path.name for path in net.bin.iterdir()) == \ --- a/tests/test_openreposhape_command.py +++ b/tests/test_openreposhape_command.py @@ -407,17 +407,18 @@ def test_install_refuses_a_target_that_is_a_symlink(tmp_path, dangling): link = bin_dir / "openRepoShape" link.symlink_to(packaged) result = run_cmd("--install", home=tmp_path, - env={"OPENREPOSHAPE_BIN_DIR": str(bin_dir), + env={"OPENREPOSHAPE_BIN_DIR": bash_path(bin_dir), "OPENREPOSHAPE_REF": "v9.9.9", "OPENREPOSHAPE_RECORD_REF": "1"}) assert result.returncode == 2, result.stdout + result.stderr said = " ".join(result.stderr.split()) - assert f"{link} is a symbolic link, so --install will not copy this " \ - "command through it" in said, said + assert f"{bash_path(link)} is a symbolic link, so --install will not " \ + "copy this command through it" in said, said assert "pipx upgrade openreposhape" in said assert "pipx uninstall openreposhape" in said assert link.is_symlink() - assert os.readlink(link) == str(packaged) + # Windows reads a link back with `\\?\` ahead of its target. + assert os.readlink(link).removeprefix("\\\\?\\") == str(packaged) if not dangling: assert packaged.read_bytes() == original assert not packaged.with_name("openRepoShape.ref").exists() ``` Both files already import `bash_path` once #596 is in. On Linux, the merge of 649b089 with d090f0c, with these hunks: **117 passed**. Fix round 7, on the merge of 52524fe with #590's head 1c8a708, with fix round 6's resolved `tests.yml` (both inputs unchanged since; the result is blob 266c62c): the hunks apply (`git apply --check` exit 0). The two suites (117 by collection), `tests/test_install_sh_filter_repo.py` (18), `tests/test_openreposhape_package.py` and `tests/test_repo_hygiene.py` give **270 passed** in 48.69s. Fix round 6: on the merge of 4c3b72a with #590's current head 1c8a708, with `tests.yml` resolved as below, the hunks apply (`git apply --check` exit 0) and the two suites give **117 passed**; with `tests/test_openreposhape_package.py` and `tests/test_repo_hygiene.py`, 252 passed. Probe 4 runs the merge of 01e8c2c with `l3-563-pypi-package` on Windows: the four cases without the hunks, then both suites with them. In run 38769166462, on Windows: - **Without the hunks**: exit 1, `4 failed, 113 deselected`. These are exactly the four cases. The launcher pair failed on `REFUSED: D:\a\_temp\…\bin/openRepoShape is a symbol…`, which confirms rv585's inferred launcher failures; the install pair failed on `REFUSED: /d/a/_temp/…/.local/bin/openRepoShape …`. - **With the hunks**: they apply (exit 0), and both suites give **117 passed** in 75.13s. **`tests.yml` conflicts with #590 (fix round 6; unchanged in fix round 7, where #590's head is still 1c8a708 and this PR's `tests.yml` is the same blob).** #590 now targets `main` too: at fix round 6 its merge ref's first parent was main 77bb5f0. Its head 1c8a708 has merged #587's final head and main, so it holds #597, and it merges with main cleanly. Both PRs add to the same two places in `tests.yml`, so against 4c3b72a there are three regions: - **The header.** Keep this PR's "The FULL suite runs on LINUX ONLY" and its exception sentence, then #590's four lines about the `package` job. - **The end of the file, two regions.** git splits the two new jobs where they share the checkout and setup-python lines. Keep both jobs whole: `tests-git-bash`, then #590's `package`. - **Checked.** I built that file: this PR's `tests.yml` with #590's header paragraph and its `package` job added. Against #590's file it differs only in this PR's two hunks, and against this PR's file only in #590's two. `yaml.safe_load` gives the jobs `['tests', 'tests-git-bash', 'package']`, and actionlint 1.7.12 exits 0 on it. #590's `package` comment now says "the other jobs" and "on Linux like the rest of the suite since #597", which stay true beside `tests-git-bash`. Fix round 4's advice to drop `tests-windows` and `tests-macos` and to reword "three jobs" no longer applies: #590 has done both. The open pull requests are the 17 with a `refs/pull/<n>/merge` ref in `git ls-remote origin 'refs/pull/*'`: 583, 590, 594, 596, 599, 614, 617-623 and 625-628. Each head was merged with 52524fe and, separately, with main 396c0ff. The ones not in the table merge cleanly with this head and touch none of its six files: #583, #614, #617, #618, #619, #621, #622, #625, #626 and #628. | Against | Result | |---|---| | origin/main 396c0ff (#591, #572, #624, #598 3fb21d9, #570) | **clean**. The merge gives this head's own tree, 6d8ffea: the head holds all of main. | | #590 1c8a708 (`l3-563-pypi-package`) | **conflicts** in `.github/workflows/tests.yml`, 3 regions: the header and the end of the jobs. #590 against main alone is clean, so this is the two PRs' own overlap. It also touches `tests/test_install_sh.py` and `tests/test_openreposhape_command.py`, which merge as text. See the #590 section above for the resolution, the four test cases and fix round 7's 270 passed. | | #599 b0182e2 (lane 3, stacked on #590's old head d090f0c) | **conflicts** in `.github/workflows/tests.yml`, 2 regions. #599 also conflicts with main alone, in six files (`tests.yml`, README.md, the handbook, `install.sh`, `openRepoShape`, `tests/test_install_sh.py`). It is #597's conflict, through #599's stale base, not this job's. | | #627 fc0656b (`fix/606-readme-followups`, #606; new since fix round 6) | **conflicts** in docs/handbook.html only: 7 regions, each one § Starting marker line (this PR's badbb6b2 against #627's 51702a2b). Its README hunks (the second-run refusal in § Starting, and `CONTRACTS_DIR` and the `single-repository.yaml` question in § Adopting) merge as text with this PR's. #627 against main alone is clean. **Whoever lands second** sets the seven markers to the merged README's hash. On this head plus fc0656b that is `fe0d932b031e4a08fc885f93d6cd978df0d5a25c`. README is then 1456 lines: 1449 plus #627's own `readme-cap.d/606.txt` (+7). | | #594 be808a7, #623 1e30fa7 | **clean**. Both touch `tests/conftest.py`, which merges as text. | | #620 4c049a8 | **conflicts** in `adopt-project.py`, and so does #620 against main alone: its own conflict with main, not an overlap with this PR. | #598 landed as 3fb21d9 and is in the main row; fix round 7 is that merge. Every lane-5 PR and every other PR that has landed is in the main row. - **`tests/test_setup_sh.py`, and `test_repo_hygiene.py`'s `bash -n` test, still skip on Windows,** and since #597 nothing runs them on Windows at all. #585 names the launcher's and the installer's suites, and Brett Heap's word keeps a job for those two only. The same `BASH` would serve both. - **Any typed `yes`.** CI has no terminal. - **`tests/test_install_sh_filter_repo.py` in `tests-git-bash` (fix round 7).** #598's file is not added to the job; that is the coordinator's call. See checklist candidate 8. - **`docs/release-notes/v1.0.0.md` and `docs/release-notes/v1.0.1.md`** are records of those releases, and #597 left them unedited too. v1.0.1's notes say #585 runs the suites under Git Bash; that stays true. The v1.1.0 text is below. - **A required check.** This PR does not make `tests-git-bash` a required status check. That is a ruleset setting, and Brett Heap's call. No v1.1.0 notes file exists on main yet, so this PR writes none. Proposed text, a section of its own: > **Git Bash on Windows (#585).** Git Bash, the bash that Git for Windows installs, runs the one-line install and the `openRepoShape` command. CI's `tests-git-bash` job runs their suites under Git for Windows' own bash, with the PATH Git Bash makes, and fails if it cannot find that bash. That job is an exception to #597's Linux-only CI, made on Brett Heap's word. In PowerShell the way in is unchanged: `py setup-project.py`. Two things differ from macOS and Linux. `ln -s` in Git Bash makes a COPY unless `MSYS=winsymlinks:nativestrict` is set; a copy has no `openRepoShape.ref` beside it, so it follows main, and `openRepoShape --version` says so. Link it with that setting (Windows lets a non-administrator make a symlink only in Developer Mode), or put `~/.local/bin` on `PATH` instead. And `chmod` changes nothing on Git Bash's drives; a file that starts with `#!` runs as a command anyway. Not tested there: `setup.sh` run on its own (its suite still skips on Windows), and any typed `yes`, for which CI has no terminal. In Git Bash the preflight that the one-line install ends with cannot take a `yes` at all, so it installs nothing for you. 1. **The machine report's `python3` line on Windows.** `--install`'s `machine:` block probes only `python3`. Where Python came from python.org, Git Bash has `python` and no `python3`, or only the Microsoft Store stub. The block then says `python3 MISSING`, or "present" for a stub, while `--preflight` passes with `python`. 2. **A drive-letter `$OPENREPOSHAPE_REPO`.** `shape_clone_url` treats `C:/src/openRepoShape` as an owner/name and builds `https://github.com/C:/src/openRepoShape.git` from it. Git Bash and git itself both accept the drive-letter spelling. 3. **A Windows-spelled `$OPENREPOSHAPE_BIN_DIR`.** `--install` then prints `export PATH="D:\...:$PATH"`, which bash cannot use: the drive's colon splits it. 4. **A stale docstring.** `test_a_ref_the_doctor_would_check_out_is_validated` says the refusal arrives before any clone. `shape_checkout` clones first, then checks the ref. 5. **On Windows, the preflight a piped install ends with cannot take a typed `yes`.** - Under `curl … | bash`, install.sh:342 (:277 before #598) hands its children /dev/null for stdin. - setup-project.py's `open_terminal()` returns None when `os.name == "nt"`, so it has no Windows path to the person's terminal. Opening `CONIN$`, where POSIX opens `/dev/tty`, might be the analogue. - Found by rv585; it is the root of this PR's README wording. 6. **The handbook's step 3 has no Git Bash path.** Its install block and paragraph are `data-platform="linux mac wsl"` (docs/handbook.html:775-784 on 01e8c2c), so a reader on the Windows tab never sees the one-line install that the Windows panel says Git Bash runs. Adding it to the Windows tab is a docs decision. 7. **In Git Bash, the host-quoted lines are still quoted for PowerShell.** `openRepoShape --doctor`, a family `setup-project.py` run and `family.py`, started from Git Bash, run under native Windows Python, where `os.name` is `nt`. So `quote_arg`, `shell_quoted`, `shell_line` and `shell_word` (for example setup-project.py:2331, `if os.name == "nt" and not posix`) quote for PowerShell, even though the person is at a bash prompt. #588's sentence stays true ("on Windows"). But someone who follows "Git Bash runs that command" and pastes such a line back into Git Bash gets the failure that `tests/test_readme_windows_quoting_sentence.py`'s docstring names: `git -C C:\work\Orbit` reads as `C:workOrbit`. Choosing `sh`'s dialect under Git Bash (for example on `MSYSTEM`) would be a #532 decision. 8. **#598's installer cases do not run under Git Bash.** `tests-git-bash` runs `tests/test_install_sh.py`, and since fix round 7 that suite reaches #598's code in two ways: every run now prints `adoption:` and `done:`, and `no_pipx` expects uv's printed command. But the uv install, the readinesses' wording and the pasted next steps are tested in `tests/test_install_sh_filter_repo.py` only, on Linux. README says Git Bash runs step 3's one line, "tested in CI". - **What the file uses.** It inherits `test_install_sh.pytestmark` (`BASH_SKIP` and `git_bash_session`), and drives every run through `Net`: `fake` (LF bytes through `write_script`), `run` (`BASH`, `bash_path(INSTALLER)`), `serve`, `installed` and `calls`. Its own `uv` fake writes LF bytes, like `Net.fake`, and names the interpreter as `shlex.quote(sys.executable)`, as `Net.fake` does in the job today. No assert compares a path bash printed with a Python path; each checks a line's start or end. - **Under Git Bash it would likely pass.** Its programs see `PATH=net.path` alone, so the runner's own pipx or uv cannot leak in. The launcher's `--install` probes `python3` with `command -v` and never runs it, so the logging `python3`/`python` fakes stay silent. Its `paste()` sends a script through a text-mode pipe, as `Net.run(piped=True)` already does in the job. - **Not run on Windows.** On Linux: 18 passed in 7.32s. Adding it to the job is one more path on the job's pytest line. 9. **README still names `pip install git-filter-repo` for adopting.** README:768 (§ Adopting) says "`git filter-repo` is a **hard dependency** — `pip install git-filter-repo`, or the same name from apt or brew." #595 found that `pip install` is refused on an externally managed Python (Debian 12, Ubuntu 23.04, Arch; PEP 668), and #598's install.sh no longer prints it. Neither #598 nor this PR touches that line. Review: rv585 found 0 blockers, 2 should-fix and 8 nits on 649b089 (CHANGES NEEDED), fixed or answered in 01e8c2c; the re-review rv585b found 5 nits on 01e8c2c (APPROVE WITH NITS), fixed or answered in 3095a9c; rv585c found 4 nits on 5f001de (APPROVE WITH NITS), fixed or answered in fe58d66. CI (Brett Heap's ruling 27, Linux-only since #597): H0 is the head 52524fe, a hand-resolved merge of main 396c0ff, green on tests (Linux), tests-git-bash and SonarCloud; main since then (#625 8f453d0, #583 de428f5) touched none of this PR's files, and the local gate passed on the predicted squash over de428f5: the launcher's, the installer's and install.sh's filter-repo suites, 131 passed; and the hygiene, README-section, CLI-reference, setup-by-hand and Windows-quoting sets, 171 passed. The tests-git-bash job is on Brett Heap's word of 2026-10-15T18:20:35Z, "Option 1: keep a small Git Bash job for #596". Landed on Brett Heap's word of 2026-10-15T22:24:36Z, "Merge #596 when its turn comes". Closes #585 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ash sentences and #606's three sentences both stay in README and the handbook, so the page's seven § Starting markers read the merged section's hash (#606) Merges main bf7b19b (#583, #625 and #596 since the last merge, 396c0ff). Only #596 touches README.md and docs/handbook.html; #583 and #625 change code and tests, taken as main has them. README.md merged without a conflict. #596 rewords three places in README § Starting a project in a new organisation (the Windows and Git Bash paragraphs of its install section) and #606 adds a sentence to the part-way box in the same section, so that section's hash is neither branch's (#606: 51702a2b, main: badbb6b2) but a third one, fe0d932b. docs/handbook.html conflicted in seven hunks, all of them the one marker line of the seven page parts that read that section (masthead, start, partway, ci, legs, doublepin, park). The text around the markers merged without a conflict, so both sides' sentences are kept: #596's Git Bash words and #606's partway and adopt mirror sentences. Each of the seven markers now names fe0d932b031e4a08fc885f93d6cd978df0d5a25c, the merged README section's fingerprint, computed with the hygiene test's own readme_segments and readme_fingerprint. The § Adopting marker, ca4b6731, stays: #596 does not touch that section. README is 1456 lines against a cap of 1422 + 5 (550.txt) + 22 (564.txt) + 7 (606.txt) = 1456. #596 added no cap file (it rewords within the cap), so the cap is unchanged by the merge. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, a kept nested `.gitattributes` that outranks the root's line-ending rule for a pinned copy, and a root `.gitattributes` that `git add` would re-encode (#530) (#619) `adopt-project.py check` now reports two findings about the `.gitattributes` files of a source, and `execute` refuses the same plans with the same codes before any leg exists. Both go through `_entry_findings`, with every other finding of `check`'s (#168). - **`plan-nested-attributes`** (#530). A `.gitattributes` the plan keeps in the assembly root below the root, such as `.github/.gitattributes`, outranks the root's line-ending rule for a pinned copy when its rules make a checkout write that copy with CRLF. That happens on every platform, Linux and macOS included, and the root's first `make bootstrap` then reports `shape-copy-drift`, after `execute` said `adoption verified`. The finding names the kept file, the copies it reaches and the line to end the file with, or says to send it to a leg or `drop` it. A kept nested symlink counts, and so does a disk that ignores case. - **`plan-attributes-reencoded`** (lane 2's D5(a) line). The source's own root `.gitattributes`, which the split appends #471's block to, is found when this machine's `git add` would store other bytes than the source's followed by the block (CRLF under `text` or an `eol`, an expanded `$Id` under `ident`, a `working-tree-encoding`), or would refuse them. On main, `execute` made both legs and then reported the file `adopt-lost`. - Git is asked, never imitated: each question runs in a throwaway repository, and the nested one reads no attribute source of this machine, because a clone elsewhere has none. A source with no such file, or whose root file `git add` stores as written, adopts as before. On #457 this closes the nested `.gitattributes` line and the CRLF root `.gitattributes` line. Its two riders, the `.github -> ci` link spelling in #471's block and `working-tree-encoding=` or `filter=` over a pinned copy, stay open on #457. Tests: tests/test_adopt_nested_attributes.py adds 27 test functions and 79 cases, 56 failing on main. Each change reverted alone fails at least one. At ab7e8ef, 72 test files passed in both modes: 5,780 passed and 13 skipped with `git-filter-repo` on PATH, 5,460 passed and 333 skipped hidden, exit 0 in every chunk. The later commits change only comments in the new test file. Review: the adversarial review asked for one fix round (`checked_out_as` read `crlf` before `text`, as git does not, and the PR texts) and found it PR-READY at ab7e8ef, D5(a) included. The edge cases it found outside #530 are left for #457's checklist. The branch holds three merges of main, one of them by hand: cd46f10 (main de428f5, after #583), whose one conflict, the list `_entry_findings` returns, keeps both #530's two findings and #583's branch-name findings. The others were GitHub's update-branch, each a clean merge, and the branch was never rebased. The head 1a12fe4 is green on Linux and SonarCloud (ruling 27; CI is Linux only since #597). Closes #530 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-copy text is tested and pushed on the current main (#610) Merges f6376d7 (the squash of #614) and the main commits between this branch's base 396c0ff and it: #625 (family.py `bump --to`), #583 (adopt-project.py branch names), #596 (README, handbook and the tests-git-bash job) and #614 (adopt-project.py `git rm` bytes). No file this PR changes is among the files main changed since 40a85e1, and `git merge-tree --write-tree` was clean (tree 30b0fbf), so no hunk was resolved by hand. The pinned shape copies still move as one re-pin. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ote` reaches no repository is a finding `execute` refuses before any leg, so it no longer ends `adoption verified` over a recursive clone that fails (#418) (#618) `check` and `execute` now ask whether the url of each submodule a leg keeps answers, where git can find that out on this machine alone. On main, a kept submodule url on this disk where `git ls-remote` reaches no repository passed `check` and ended `adoption verified`, over an assembly whose recursive clone then failed. - Each url is taken as the leg resolves it (an absolute url as it stands, a relative one from the leg's remote, #190's `_resolved`) and expanded as git here expands it (`git ls-remote --get-url`, which contacts nothing). A url that then names a repository on this machine's disk, an absolute path or a `file://` url with no host, is asked with `git ls-remote` under `GIT_ALLOW_PROTOCOL=file`. A drive letter counts only on Windows, and a path that starts with two separators (`\\server\share`) never does, because Windows reaches it over the network. - One that answers nothing is `plan-submodule-url-answers-nothing` in `check`, and `execute` refuses it before any leg exists, through the one list both read (`_entry_findings`). Under `--local-remote-dir`, relative urls are asked from the bare repositories `execute` would create, and the refusal's first line says so. An absolute url on this disk is asked in every run, GitHub's too. - A url on a network is never asked by either command, because whether it answers can depend on whose credentials ask. `check` names each in one NOTE and still passes. No network call is added. - One cause, one finding: a relative url that names another repository from its leg keeps #190's `plan-submodule-relative-url` alone and is not asked. Tests: tests/test_adopt_submodule_url_answers.py adds 21 cases in 7 functions, all failing before the change. One existing test, `test_one_directory_through_a_symlink_is_one_repository`, now puts its dependency where the url resolves, since its old fixture was this defect. Each change reverted alone fails at least one test. At 63c139c, 71 test files passed in both modes: 5,722 passed and 13 skipped with `git-filter-repo` on PATH, 5,407 passed and 328 skipped hidden, exit 0. Review: the adversarial review asked for one fix round (a UNC url was asked over the network on Windows, and the PR texts) and found it PR-READY at 63c139c. The edge cases it found outside #418 are left for #457's checklist. Left open here: `_verify`'s closing line still does not say that the recursive clone was not tried. The branch holds five merges of main, one of them by hand: 33b46af (main de428f5, after #583), whose one conflict was a docstring sentence in `_entry_findings` listing the findings in order; it now places #418's after #190's, as the code returns them. The others were GitHub's update-branch, each a clean merge, and the branch was never rebased. It lands under the fast-refresh ruling of 2026-10-15T22:48:35Z. The earlier head 536d08a, after that hand merge, is green on Linux and SonarCloud, and every commit after it is a clean merge of main. The head 479879b is green on SonarCloud, and the full suite passed on its tree: 169 test files, 9,812 passed and 36 skipped. Closes #418 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #583 (#520), #596 (#585), #614 (#527), #619 (#530), #594 (#526), #617 (#525) and #618 (#418). Three files conflicted: - .github/workflows/tests.yml: main's file (with #596's tests-git-bash job) plus this branch's three additions, byte-identical to theirs: the header paragraph on the `package` job (#590), after main's Git Bash paragraph; #599's actionlint line for release.yml; and #590's `package` job, now after tests-git-bash. actionlint 1.7.12 finds nothing in it. - README.md, the native Windows block's closing paragraph: #596 says the two-liner IS the Windows way in from PowerShell, with no twin of the `openRepoShape` command, and that Git Bash runs that command and the one line, tested in CI but for a typed `yes`; this branch says the two-liner is the way in that installs nothing and pipx's `openRepoShape` runs the same flow. The merge keeps both facts in main's seven lines: the two-liner installs nothing, pipx's command runs the same flow, and Git Bash runs step 3's bash command and its one line, tested in CI but for a typed `yes`. - docs/handbook.html: the same two sentences in the masthead's second-way paragraph and the Windows panel, each combined the same way, and the seven `Starting a project in a new organisation` markers, which now read the merged README (62737be2). No other section's marker moves. README.md is 1477 lines, its cap (1449 on main, +28 for #563). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests, 57 of 82 milestone issues closed), with #583, #596, #614, #619, #594, #617 and #618 in their areas, #596's Git Bash facts for its placeholders, release.yml's step 1 whole, and every pull request cited beside its issue (RV630 SHOULD 3, 5, 6, nits 7, 8) - SHOULD 5: step 1 matches release.yml's header: "always on a `release/v<X.Y>` branch, which no push run tests", "A `release/v<X.Y>` branch serves while it is still at that commit", and that a tag's dispatch running at the tag's commit is GitHub's documented behaviour, not yet seen here. - SHOULD 6: #598 beside #595, #592 beside #593, #460 on #557's line, and #589's line says "where that clone is still on the machine and is no partial clone" again. The counts and the open-issue list are re-derived from `git log --first-parent bfed143..origin/main` (49) and the milestone (25 open, 57 closed: #342, #392, #393, #421, #423, #426, #432, #433, #434, #443, #444, #454, #455 and #629 joined it). New lines under adopt-project.py for #583 (#520, #420), #619 (#530), #618 (#418), #614 (#527), #594 (#526) and #617 (#525); #596 (#585) under the one-line install, CI, native Windows, the docs and the known limits, where the two placeholders for it were. - SHOULD 3: the known limit about "the installed doctor's temporary clone" is gone; the README, the handbook and AGENTS.md no longer say it. - SHOULD 1: the filter-repo line says install.sh's order, apt and dnf included. - nit 7: the release process's preconditions are said as the process this release follows; the workflow checks the environments, and nothing checks the rehearsal. - nit 8: the known limits are wrapped. - "the first on native Windows, where the one line cannot run" says PowerShell cannot run it, since Git Bash does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h, the `switch -c` remedy says to give it another name, or go back only if it holds work not merged yet, and the docs say `family/add-<Project>` (#607) (#637) ### Fix round 1 (rv607) Review rv607 of 6212c31 said CHANGES NEEDED: 0 blockers, 2 should, 7 nits. This round is one new commit, 66b1043, on top of 6212c31. No amend, and main is not merged in. Each point: - **S1, the sentence.** It sent a person back to the old branch first. That is right only where the branch holds work not merged yet. `BRANCH_LEFT` now puts another name first and going back second (the text is under "L2-46" below). It is word for word the same in `scripts/family.py`, in the pinned `validate-family.py` and in the pinned test constant, and the comments that described the old order are rewritten. Two new tests type the printed line in the states the review probed: `main` has moved since the earlier act, and a commit was made on a detached HEAD. - **S2, a guard for every `_switch_line` remedy.** A new test reads the validator's source with `ast` and fails any remedy that names `_switch_line` whose prose does not end with `BRANCH_LEFT`. On a merge of this head with #634 it fails at `_lockstep`, which is the point: whichever of #637 and #634 lands second adds the sentence there. #634's lines are not on this branch and are not touched. - **N1.** Under "Left out". - **N2.** The commit body of 6212c31 said neither tool can print the one line that works by asking first. That holds for the validator only. `family.py` could ask; it says the validator's words so the two tools keep one rule. Decision 1 says so, and the new commit's body corrects the old one. - **N3.** The merge table is refreshed: current origin/main, #620, #622, #626, #630 to #636, #638, #639 (lane 5's #611) and #634 (S2). - **N4.** Decision 1 no longer says the typed line works "in every shell". It says what a person types is "the printed line with one word changed (the branch name, or `-c` taken out)", so it keeps the printed quoting, and that the tests type both changes as printed. - **N5.** Decision 3 now says the case of the branch name "does not matter to `add`", and adds the note about case-insensitive disks. - **N6.** The `init` NEXT STEPS test read the lines with a regex whose `--family-root \S+` breaks on a root with a space in it (`C:\Users\Jane Doe`, or any `tmp_path` with a space). It now reads the words with `host_words`, as the NEXT STEPS test beside it does. Run with a space in the base temp directory, the old test fails with `AttributeError: 'NoneType' object has no attribute 'group'` and the new one passes. - **N7.** "Three existing tests take the new text" was wrong. It is two test functions plus `GITLINK_REMEDY`, a constant four cases share. Fixed under "Tests". The one new test that passes on BASE is disclosed there too. ### Fix round 2 (rv607b) The re-verify rv607b of 66b1043 said APPROVE WITH NITS: 0 blockers, 0 should, 5 nits. This round is one new commit, a277695, on top of 66b1043. No amend, and main is not merged in. It changes comments and docstrings only, so no behaviour moves and no mutation row changes. - **Nit 1.** Two comments called another name "right in every state". It is not: where the old branch holds work not merged yet, another name leaves that work behind. The comment above `BRANCH_LEFT` in `scripts/family.py` and the one in the pinned `validate-family.py` now say a new branch starts where the holder stands now, "which works in every state". The three `BRANCH_LEFT` texts are unchanged and still equal. - **Nit 3.** The guard's docstring said it finds each `_switch_line` call "however it is passed on". A switch line moved into a variable would defeat that. It now says "wherever the call stands inside the string expression that is passed on". - **Nit 4.** The N4 line above now quotes decision 1 as written. - **Nit 5, the docstring half.** The docstring of `test_another_name_works_where_main_has_moved_since` said going back to the old branch conflicts, but the test does not assert that. It now says the test shows the other name works, and that the conflict on the old branch is shown by rv607's probe and decision 2's table. No assertion is added. - **Nit 2, left as written.** The guard's control counts stay: each case fails loudly with a clear message. Checks on a277695: the diff to 66b1043 adds ten lines and removes seven, in three files, all comments and docstrings, and the `ast` of each file is equal once docstrings are blanked. The four files `test_family_switch_branch_exists.py`, `test_validate_family_branch_step.py`, `test_validate_family_unnamed_mounts.py` and `test_family_tracking_branch.py` give `96 passed in 51.34s`, and `test_repo_hygiene.py` gives `65 passed in 1.39s`. ### L2-46, the fix `family.py add`, `bump` and `remove` refuse the holder's tracking branch with `git -C <holder> switch -c family/<act>-<member>` (#487). The holder's `validate-family.py` names the same line before each `add` and `remove` it prints (#539, landed as 54041cc). If an earlier act left that branch, the line fails with `fatal: a branch named 'family/add-atlas' already exists` (exit 128), and neither tool said what to do next. Both tools now add one sentence, in the same words (`BRANCH_LEFT` in each file): > If `git switch -c` says that branch already exists, an earlier act left it: give the branch another name, which starts it where the holder stands, or, only if that branch holds work not merged yet, the same command with `switch` in place of `switch -c` goes back to it and keeps every commit on it. - `scripts/family.py`: the sentence comes on its own line, right under the `switch -c` line, in the `family-on-tracking-branch` and `family-holder-detached` refusals (`_checked_branch`), and in the dry run's `branch` lines (`_branch_said`). - `templates/family-root/scripts/validate-family.py`: the sentence ends the remedy text of `member-gitlink-absent`, `member-duplicate-id` and `member-gitlink-unnamed`, the three remedies that print `_switch_line`. The command lines under them are unchanged. ### L2-45, rides along AGENTS.md, the holder's README, its AGENTS-shape.md and `init`'s NEXT STEPS spelled the branch `family/add-<project>`, right beside `--member <org>/<Project>`. All four now say `family/add-<Project>`, the one placeholder the validator already uses for both (`PROJECT_PLACEHOLDER`, #539). **1. What the remedy prints when the branch exists: one sentence that names the way out, not a second command line.** - *One line that works in both states* (`git switch X 2>/dev/null || git switch -c X`): rejected. On Windows, `family.py` quotes its lines for PowerShell (`shell_line` uses `os.name`; README.md:155-158 says so). Windows PowerShell 5.1 has no `||`; `&&` and `||` arrived in PowerShell 7, and `family.py`'s own `_print_next_steps` docstring says 5.1 rejects `&&`. PowerShell also has no `/dev/null`. And `2>/dev/null` would hide the real reason a `switch` fails, such as a changed file it would overwrite. - *The tool checks and prints the one line that works*: rejected for the validator. Its report is read in CI: the holder's `validate.yml` runs `python3 scripts/validate-family.py` (and `--no-members`) in a runner's checkout, which holds none of the person's branches. `family.py` could check, but then the two tools would follow two rules, and one rule in the same words was the aim. So `family.py` says the validator's words; it is not that it cannot ask. - *Two printed lines, one per state*: rejected. In the validator, each indented line is a step to run in order, and going back to the branch replaces step one; it is not another step. In `family.py`, a new `git switch` line would also be quoted for PowerShell on Windows. README.md:155-158 lists exactly which `family.py` lines are quoted that way (`NEXT`, `switch -c`, `add`, `git rm -r --cached`). That sentence would become false, and fixing it would mean editing README § "Starting a project in a new organisation". Seven handbook parts record that section's hash (`grep -c` gives 7). - *Chosen*: a sentence that names both ways out. It prints no new command, so nothing new needs quoting, and the text is the same in both tools. What a person types is the printed line with one word changed (the branch name, or `-c` taken out), so it keeps the printed quoting. The tests type both changes as printed (tests below); none of them runs a shell, and Windows and macOS are not checked (see "Left out"). **2. Another name first, going back second (S1).** The reviewer's probes, and mine, in real local holders (a holder `Orbit`, members `Atlas` and `Beacon`, local bare remotes, the `gh` stub first on PATH): | State of the old branch `family/add-atlas` | `git switch family/add-atlas` (going back) | `git switch -c family/add-atlas-2` (another name) | |---|---|---| | No commit on it, `main` has not moved (rv607) | works | works | | Holds a commit of its own, not merged yet (mine: `probe-atlas.sh`, `probe-b.sh`) | works; `add` then commits on top of it, and the earlier commit is the parent | works, but the earlier commit stays only on the old branch (`git branch --contains` names `family/add-atlas` alone) | | `main` has moved: a later member (`Beacon`) was squash-merged (mine: `probe-d.sh`; test `test_another_name_works_where_main_has_moved_since`) | `add` exits 0, but `git merge-tree --write-tree main family/add-atlas` exits 1 (`CONFLICT (add/add)` in `.gitmodules`, `CONFLICT (content)` in `family.yaml`), and an untracked `members/Beacon` is left (`warning: unable to rmdir 'members/Beacon'`) | `add` exits 0, `git merge-tree --write-tree main family/add-atlas-2` exits 0, and the validator says `2 member(s)` | | Merged, then the member removed and that merged too; the old branch is still local (rv607's probe, and mine: `probe-c.sh`) | `add` says `member-already-present`, whose remedy points at `bump` and `remove`: a dead end | `add` exits 0, once the object store `remove` leaves behind is deleted, as its NOTE says (either choice needs that) | | Detached HEAD with a commit made on it (mine: `probe-detached.sh`; test `test_another_name_keeps_a_commit_made_on_a_detached_head`) | `Warning: you are leaving 1 commit behind, not connected to any of your branches`, and `git branch --contains` names no branch | the commit stays: `git branch --contains` names the new branch | So going back is right only where the branch holds work not merged yet, and another name is right in every state but that one. The sentence says so, in that order. **3. The person's earlier work on that branch is never discarded.** `git switch <branch>` keeps the branch's commits. It refuses to switch when that would overwrite a changed file. The act then commits on top of what the branch holds; the tests check that the earlier commit is the parent of the act's commit. "Give the branch another name" leaves the old branch alone. No printed line names `switch -C` (which resets a branch), `branch -D`, `reset` or `--discard-changes`; a test checks that too. **4. The placeholder: `<Project>` everywhere.** - The validator already said `family/add-<Project>` beside `--member <org>/<Project>` (#539's fix round, pinned by `tests/test_validate_family_unnamed_mounts.py`). It is the only spelling that is one placeholder for both values. - The other option, a sentence saying the branch word is the name in lowercase, does not fit: AGENTS.md is at its cap, 492 of 492 lines (`tests/test_repo_hygiene.py:633`). This change keeps it at 492. - The case of the branch name does not matter to `add`. `_checked_branch` takes any branch but the tracking one, so `family/add-IRQS`, the doc line filled in as written, is a branch `add` commits on (test). Where the tools know the name, they still print it lowercased (`_suggested_branch`). On a case-insensitive disk (macOS, Windows) `family/add-Atlas` and `family/add-atlas` can be one loose ref there. That is untested: CI is Linux only. **5. Pinned copies.** This changes two pinned holder files: `templates/family-root/scripts/validate-family.py` and `templates/family-root/AGENTS-shape.md` (`scripts/shape_materialize.py` lists the pinned set). Each holder takes them as two `upstream-changed` rows on its next `update-shape.py` run. The holder's README is rendered once and is not pinned. No test pins those files' bytes; the tests that compare them read the template at run time. Fix round 1 changes only `validate-family.py` of the two. New file `tests/test_family_switch_branch_exists.py`, 25 tests (21 before the fix round). Four are new in the fix round: - `test_another_name_works_where_main_has_moved_since` (S1, state (d)): `family/add-irqs` is left at the `main` of its day; a later member (`IRXS`, scaffolded in the test) is added on a branch and squash-merged onto `main`, so `main` has one new commit; the refusal's `switch -c`, typed as printed, fails with `already exists` (exit 128); the same line with another name works; `add` exits 0, `git merge-tree --write-tree main <branch>` exits 0, and the validator passes. - `test_another_name_keeps_a_commit_made_on_a_detached_head` (S1): the refusal is `family-holder-detached`; after the printed line with another name, `git branch --contains <the commit>` names the new branch and no other. - `test_every_remedy_that_names_a_switch_line_ends_with_the_sentence` (S2, the guard) and `test_the_guard_sees_a_remedy_left_without_the_sentence` (its control: the source with the sentence taken out of any one remedy fails it, and so does a fourth remedy built by a helper as `_lockstep` builds its, until the sentence ends it). The other 21: - `test_the_refusal_typed_as_printed_puts_the_act_on_its_branch` [add, bump, remove] x [no-branch, branch-left]: the refusal, then its line typed as printed. With no branch, the `switch -c` makes one. With a branch an earlier act left, holding a commit of its own, the `switch -c` fails with `fatal: a branch named '<branch>' already exists` (exit 128) and changes nothing, and the same command with `switch` in place of `switch -c` goes back to that branch. The act then commits on top of the earlier commit, and NEXT pushes that branch. - `test_a_dry_run_on_the_tracking_branch_says_it_too` [add, bump, remove]. - `test_a_missing_gitlinks_remedy_typed_as_printed`, `test_a_duplicate_ids_remedy_typed_as_printed` (with the `remove` run as printed) and `test_an_unnamed_gitlinks_remedy_typed_as_printed`, each in both states. - `test_the_two_tools_say_the_same_words`. - `test_each_doc_spells_the_branch_as_its_member` [AGENTS.md, AGENTS-shape.md, holder README], `test_init_next_steps_spell_the_branch_as_its_member` (read with `host_words` now, N6), and `test_the_branch_filled_in_as_written_is_one_add_commits_on`. Two existing test functions take the new text: `test_init_next_steps_make_the_branch_before_the_add` (`<Project>`) and `test_a_missing_gitlink_remedy_makes_the_branch_and_names_the_holder` (the remedy line). `GITLINK_REMEDY` in `test_validate_family_unnamed_mounts.py`, a constant four cases share (`test_a_member_mounted_by_hand_is_two_findings` x 3 and `test_a_removed_row_is_named_by_its_gitlink_and_its_section`), ends with the sentence. The sentence is pinned once, as text, in `tests/test_validate_family_branch_step.py`. Before and after, with the four changed test files copied in (`run9.sh`, the `gh` stub first on PATH): - On BASE 396c0ff: `30 failed, 66 passed` in the four files. 24 of the 25 new-module tests fail. The one that passes, `test_the_branch_filled_in_as_written_is_one_add_commits_on`, is the evidence for decision 4: it shows behaviour this PR keeps. - On 6212c31: `23 failed, 56 passed` in the three files that pin the sentence. Both new S1 tests fail there, where they assert the new wording. The two guard tests pass there: they check where the sentence stands, not its words. - On the head 66b1043: the new module alone, `25 passed`. ### Targeted runs on the head 66b1043, BASE 396c0ff - Set A, 29 files: every `tests/test_family*.py` and `tests/test_validate_family*.py`, plus `test_bump_leg.py`, `test_bump_leg_dry_run_tracking.py`, `test_bump_leg_dry_run_truth.py`, `test_bump_leg_paste_lines.py` and `test_update_shape_branch_any_directory.py`. Result: `1016 passed, 20 skipped in 511.01s`. - From set B: `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_readme_sections.py` and `test_cli_reference.py`. Result: `210 passed in 15.73s`. The fix round changes no template other than `validate-family.py`'s text, so the rest of set B (the 27 files of the first round) is not re-run. - Every skip is a platform skip that was there before this change (a case-ignoring disk, a Windows default, powershell.exe), or a template-quoting skip in `test_family_init_values.py`. A real holder `Orbit`, made with `family.py init`, and a member `Atlas`, scaffolded with `scaffold-project.py --local-remote-dir`. - **On BASE 396c0ff** (first round): `add` refuses with `switch -c family/add-atlas`. Typed as printed, that gives `fatal: a branch named 'family/add-atlas' already exists`, exit 128, and the remedy says nothing more. The validator's `member-gitlink-absent` remedy prints the same line, with the same result. - **On the head, an unmerged act** (`probe-atlas.sh`, a branch holding a commit of its own): the refusal prints the new sentence. The same command with `switch` for `switch -c` gives `Switched to branch 'family/add-atlas'`, exit 0, at the earlier commit. `add` then exits 0, its NEXT pushes `family/add-atlas`, and its commit's parent is the earlier commit. The validator carries the same sentence. - **On the head, `main` moved** (`probe-d.sh`): the printed line fails (128). With another name: `add` exits 0, `git merge-tree --write-tree main family/add-atlas-2` exits 0, `family ok: Orbit (orbit), 2 member(s)`. Going back instead: `add` exits 0, and `git merge-tree --write-tree main family/add-atlas` exits 1 with the two conflicts in decision 2. - **On the head, a detached HEAD with a commit** (`probe-detached.sh`): the printed line fails (128). With another name, `git branch --contains` names `family/add-atlas-2`. Going back with `switch`: git warns it is leaving 1 commit behind, and no branch contains it. No row needs real GitHub. Each change below was made alone in a scratch clone of the head, then the new module was run (and, for the validator changes, the two other files that pin the sentence), and the file was put back. Every change that alters behaviour makes at least one new test fail: | Change | Result | |---|---| | `family.py` `BRANCH_LEFT` back to the first round's words | 12 failed: the six refusal cases, the three dry-run cases, both new S1 tests, `test_the_two_tools_say_the_same_words` | | `family.py` `BRANCH_LEFT` one clause dropped (`only if`) | 12 failed, the same tests | | `_checked_branch` remedy with no sentence | 8 failed: the six refusal cases and both new S1 tests | | sentence printed above the `switch -c` line, not under it | 8 failed: the same eight | | `_branch_said` (dry run) with no sentence | 3 failed: `test_a_dry_run_on_the_tracking_branch_says_it_too` [add, bump, remove] | | `_switch_command` prints `switch -C` | 11 failed, both new S1 tests among them: the printed line no longer fails `already exists` | | validator `BRANCH_LEFT` back to the first round's words | 12 failed in the three files, `test_the_two_tools_say_the_same_words` among them | | validator `member-gitlink-absent` with no sentence | 5 failed: its two typed-as-printed cases, the remedy test, the guard and its control | | validator `member-duplicate-id` with no sentence | 4 failed: its two typed-as-printed cases, the guard and its control | | validator `member-gitlink-unnamed` with no sentence | 8 failed: its two typed-as-printed cases, the four `GITLINK_REMEDY` cases, the guard and its control | | **S2:** a fourth remedy added that names `_switch_line` and does not end with the sentence (built by a helper, as #634's `_lockstep` does) | 2 failed: the guard and its control, and nothing else in the three files | | **S2:** the same fourth remedy ending with `{BRANCH_LEFT}` | 79 passed | | `init` NEXT STEPS branch back to `<project>`; its `--member` lowercased | 1 failed each: `test_init_next_steps_spell_the_branch_as_its_member` | | AGENTS.md, AGENTS-shape.md, holder README: branch back to `<project>` (and AGENTS.md's `--member`) | 1 failed each: `test_each_doc_spells_the_branch_as_its_member` for that doc | The other hunks change only docstrings and comments (`_checked_branch`, `_branch_said`, `_print_next_steps`, `_switch_line`, and the comments above the two constants). They change no behaviour, so no test can catch reverting them. | Ref | Result | |---|---| | origin/main f6376d7 (#614 landed; main has moved since BASE through #625, #583, #596) | clean, tree eb91b600. A committed merge of the head with it, e26dca8c, ran set A and #625's `test_family_bump_to_local_only.py`: `1024 passed, 20 skipped in 500.52s` | | #620 a2b4f89 | clean (it conflicted in `adopt-project.py` in the first round; it no longer does) | | #622 227bc3b (lane 2, #609, `scripts/family.py`) | clean | | #626 207d4e1 (lane 4, `scripts/family.py`) | clean | | #630 82f9403, #631 9733547, #632 86c6b9c, #633 f7f5de5, #635 2c0b157, #636 4aade9f | clean | | #638 64e6621 | clean | | #634 40a85e1 (lane 2, #610: `_lockstep` prints a fourth `switch -c`) | clean, and S2 fires on it. A committed merge, 4d1a7161: the new module, `test_validate_family_branch_step.py` and #634's `test_pinned_copy_text.py` give `2 failed, 86 passed`. The two failures are the guard, `assert [124] == []` (line 124 of the merged file is `_lockstep`'s return), and its control. With one line added to `_lockstep`, `f"moving one alone is what this refuses. {BRANCH_LEFT}"` in place of the plain string, the same merge gives `306 passed` over the nine files that read the validator or the lockstep rule (`test_validate_family*.py`, `test_family.py`, `test_family_tracking_branch.py`, `test_pinned_copy_text.py` and the new module). So the second PR to land needs that one line and nothing else. | | #639 6f938ed (lane 5, #611) | conflicts in the same one sentence, in the same two places: the end of the `member-gitlink-unnamed` remedy text in `validate-family.py` (`_check_gitlinks_named`), and the `GITLINK_REMEDY` constant in `tests/test_validate_family_unnamed_mounts.py`. #611 adds "and commit that removal, since `family.py add` refuses ..." and this PR appends `BRANCH_LEFT`. Resolved as in the first round, keeping #611's clause and appending ` {BRANCH_LEFT}` in the validator and `+ BRANCH_LEFT` in the test constant, in a committed merge (92602308): `scripts/family.py` merged cleanly, and the six test files that either PR changes or pins gave `157 passed`. Neither branch was resolved. | - No compound `||` line and no state check: see decision 1. - No README.md edit: no README sentence becomes false, because no new kind of line is printed. - The docs do not mention the exists case. AGENTS.md is at its cap, and the tools print the sentence where the failure happens. - The `switch -c` in `init`'s NEXT STEPS gets no sentence: a holder `init` just made has no branch an earlier act could have left. - **A branch checked out in another worktree (N1).** Where `family/add-atlas` is checked out in another worktree of the holder, `git switch family/add-atlas` gives `fatal: 'family/add-atlas' is already used by worktree at '<path>'` (exit 128), so the going-back half of the sentence does not work there. Git names the worktree, and another name works (`probe-n1.sh`: `switch -c family/add-atlas-2` exits 0). The sentence is not made longer for it. - I could not run Windows PowerShell or macOS here. The tests read `family.py`'s lines with `host_words`, which parses PowerShell's quoting on Windows. They make the earlier branch with `commit-tree` and `update-ref`, move `main` with the same two, write no file with `write_text` and compare nothing CRLF-sensitive. `test_another_name_works_where_main_has_moved_since` runs `git merge-tree --write-tree`, which needs git 2.38 or newer. No CI job checks Windows or macOS. - **bump-leg.py: the `git switch -c bump/<role>-<sha12>` remedy fails `already exists` when an earlier bump left that branch.** `_switch_command` (scripts/bump-leg.py:1124) prints the line for the two refusals and the dry run's closing hint. Like #607, it says nothing about a branch that is already there. - **setup-project.py: the family `git -C <holder> switch -c family/add-<project>` line has the same gap.** The NEXT lines a scaffold into a family folder ends with (setup-project.py:2358) print it with no word on an existing branch. It matters only when an earlier attempt left that branch. - L2-46 ([#459 comment](#459 (comment))) - L2-45 ([#459 comment](#459 (comment))) Review: rv607 found 0 blockers, 2 should-fix and 7 nits on 6212c31 (CHANGES NEEDED), fixed in 66b1043; the re-verify rv607b found 5 nits on 66b1043 (APPROVE WITH NITS), of which nits 1, 3 and 4 and the docstring half of nit 5 are fixed in a277695, and nit 2 is left. CI (Brett Heap's ruling 27, with his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a277695 passed tests (Linux), tests-git-bash and SonarCloud; the head 62263d2 is a clean update-branch merge of main a524542 (over e041867, a clean merge of 5b29ee1), with SonarCloud, tests-git-bash and package green on it, and the local gate passed on the head itself: the family and validator set, 1016 passed (20 skipped), and the hygiene, AGENTS.md-code, README-section and CLI-reference sets, 222 passed. Landed on Brett Heap's word of 2026-10-16T11:21:49Z, "Merge #637 when its turn comes". Closes #607 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s not UTF-8, so a source whose HEAD or `origin/HEAD` is on `caf<e9>` is refused or adopted past it, and no longer ends `plan`, `check` and `execute` in a traceback (#425) (#717) `adopt-project.py`'s `_default_branch` read the branch the source is on (`git symbolic-ref --short refs/remotes/origin/HEAD`, then `HEAD`) through `git_out`, which decodes strictly. git keeps a branch name as the bytes it was given, so a source whose HEAD or `origin/HEAD` names a branch such as `caf<e9>` ended `plan`, `check` and `execute` in a `UnicodeDecodeError` traceback, exit 1, with no plan, no leg and no word about the branch. Now the name is read as bytes and decoded with `surrogateescape`, as `_elector` (#292) and the other git name reads already do. The byte is kept as a lone surrogate (`caf\udce9`), and the checks that already existed answer: - `plan` with no `--tracking-branch` refuses it as `unsafe-value` for the tracking branch it would take (#479), exit 2, before any plan is written. - `plan --tracking-branch main` reads the source at `caf<e9>`, and `check` and `execute` adopt it. - `check` and `execute` of a plan whose tracking branch the source no longer has say `plan-tracking-branch-missing` (#479, #583), and `execute` refuses it before any leg exists. One line of code, plus its docstring. Not in this PR: the same strict decode in `scripts/family.py`'s `_member_head`, and a UTF-8 name such as `café` under the C locale, which #653 takes. Tests: tests/test_adopt_git_reads.py adds 8 cases, all failing on the base, and each change reverted alone fails at least one of them. Review: the adversarial review found it PR-READY with text corrections only. It followed the surrogate everywhere the branch name flows, and confirmed the writer's audit of all 25 `git_out` calls. Its edge cases outside #425 go to #457's checklist. GitHub's update-branch merged main into the branch once, a clean merge, and the branch was never rebased. It lands under the fast-refresh ruling of 2026-10-15T22:48:35Z. The earlier head 44e9711 is green on Linux, Git Bash and SonarCloud, and every commit after it is a clean merge of main. The head 58bec93 is green on SonarCloud and Git Bash, and the full suite passed on its tree (lane 1's gate, run on that tree predicted before main reached it): 192 test files, 11,206 passed and 36 skipped. Closes #425 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-1 (openRepoShape-1)
Claim: #520 (comment)
Claim: #420 (comment)
What
adopt-project.py checknow finds the branch namesexecuteused to fail on after both legs existed, andexecuterefuses each one before it creates a leg:check: refuse the branch namesexecutefails on after both legs exist (case,hideRefs, git's own rules) #520). A branch that is in git's way of the split branch only once case is ignored isplan-split-branch-taken. Examples areAdopt,ADOPT/THREE-REPO-SHAPEandAdopt/Three-Repo-Shape/oldbesideadopt/three-repo-shape. This holds in the source and on GitHub, on every system, as ruled on 2026-10-12, and the finding says that it holds once case is ignored.check: refuse the branch namesexecutefails on after both legs exist (case,hideRefs, git's own rules) #520). A local repository's branches are read from its own refs withgit for-each-ref, so a branch the source hides withuploadpack.hideRefsortransfer.hideRefsis seen. A clone of the source never gets such a branch, so the finding about it is about the push into the source (execute --local-remote-dir) alone.--tracking-branchand the plan'stracking_branch:andadopt_branch:are held only tochecked_value's alphabet and never asked of git, so a name git refuses (~,a..b,x.lock, a trailing/) passesplanandcheckandexecutedies at the first git command that takes it, after a leg repository exists (git init --bare -b,git checkout -B, or a push to GitHub after bothgh repo createcalls), and the corrected plan is then refusedleg-remote-exists#420.checkasks git (git check-ref-format --branch, through scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283'srefuse_bad_branch) about the plan'sadopt_branch:andtracking_branch:. The findings areplan-bad-adopt-branchandplan-bad-tracking-branch.plan --tracking-branchrefuses a name git will not create asadopt-bad-tracking-branch, and writes no plan.checkholds both names tochecked_value's alphabet, asexecutedoes. The finding isunsafe-value, inexecute's own words.Why (repro on main)
Measured on main at bfed143 with git 2.43.0, git-filter-repo a40bce548d2c and Python 3.12.3. Every remote is local, and
ghis a stub that refuses every call. The source is three files (src/app.py,docs/spec.md,README.md) in one commit, and<D>is the scratch directory.A source branch
adopthidden by the source's ownuploadpack.hideRefs = refs/heads/adoptwent the same way.checksaidplan ok, andexecute --local-remote-dirmade and pushed both legs before the push of the split failed:transfer.hideRefsfailed at the same push, after both legs were made and pushed, but not on the ref lock: the pattern hidesrefs/heads/adopt/three-repo-shapefrom the push as well, and git refused it with! [remote rejected] HEAD -> adopt/three-repo-shape (deny updating a hidden ref).plancheckexecute --local-remote-diradopt_branch:edited toa..b,x.lock,~ora/plan okgit checkout -BREFUSED leg-remote-existsadopt_branch:ortracking_branch:edited toa=b, oradopt_branch:toa,b(L2-1; the source holds a brancha=b)plan okREFUSED unsafe-valueadopthidden byuploadpack.hideRefsortransfer.hideRefsplan okAdopt,ADOPT/THREE-REPO-SHAPEorAdopt/Three-Repo-Shape/old(A4)plan okadoption verified. #457 reports macOS and Windows making both legs before git refused#420's table, measured again row by row on bfed143. #283, #479 and #181 changed this area after #420 was filed.
--tracking-brancha..b,~,x.lock,a/,a//b,.aplanexit 0 with a NOTE that the source has no such branch.checkexit 1,plan-tracking-branch-missing.executerefused before any leg, nothing leftplan-tracking-branch-missing.planstill wrote the plan, and nothing named git's rule: fixed here.tracking_branch:edited to"a..b","x.lock","~"or"a/"checkexit 1,plan-tracking-branch-missing.executerefused before any leg, nothing leftplan-bad-tracking-branch, in git's words, and not alsoplan-tracking-branch-missing.adopt_branch:edited to"a..b","x.lock","~"or"a/"checkplan ok.executeexit 2 atgit checkout -B, both leg repositories left. Since #420 was filed the refusal names the failed command, where #420 saw nothing on stderrmainandadopt/ok(controls)--local-remote-dirREFUSED leg-remote-existsafter anadopt_branch:row. After atracking_branch:row there is nothing to meetadopt_branch:, by #479 fortracking_branch:.--tracking-branch a..b, GitHub mode, stubghplanexit 0.executerefusedplan-tracking-branch-missingbefore anyghcall (0 calls)plannow refuses it.#283 (6c2a371, PR #355) put
branch_problemandrefuse_bad_branchintoscripts/shape_materialize.py, and asked them from the scaffold and the family holder only. #181's commits (3ef059c, PR #314, and b8b7062, PR #518) ask git about a tracking branch only when the project's loader misreads it (_a_branch_git_creates), and none of these rows is misread.How
--tracking-branchand the plan'stracking_branch:andadopt_branch:are held only tochecked_value's alphabet and never asked of git, so a name git refuses (~,a..b,x.lock, a trailing/) passesplanandcheckandexecutedies at the first git command that takes it, after a leg repository exists (git init --bare -b,git checkout -B, or a push to GitHub after bothgh repo createcalls), and the corrected plan is then refusedleg-remote-exists#420, L2-1)._branch_name_refusal(plan, key, default, code)reads each name asexecutereads it in_checked_plan_values: the constantADOPT_BRANCHormainwhere the key is absent, andstrof what the file holds otherwise. It holds the name tochecked_valuefirst, which givesunsafe-valueinexecute's own words, and otherwise asks git withrefuse_bad_branchunder the key's own code._branch_name_findingsasks the split branch first, then the tracking branch, the order_checked_plan_valuesasks them. Their remediation is said once. It ischecked_value's for the alphabet; for git's verdict it is a sentence about the plan's key, in place of the helper's "pass a branch name", which is worded for a flag._entry_findings. The two name findings come after the project's validator (adopt-project.py and scaffold-project.py accept values the new project's own validate-manifest.py refuses (a blank--elected-byor--reference,--elected-on 2026-W41-6or20261011on Python 3.11+,--org test+org, a plan'svisibility: secret), soadoption verifiedor a scaffold exit 0 is followed by a failed first bootstrap; andplanadmits a 251-255-character--projectthatcheckrefuses #261) and right beforeplan-tracking-branch-missing(adopt-project.py: a source whose default branch is notmainis adopted with both legs pushed, then its pull request is refused (--base main), and the printed way out repeats--base main#479). So a value's name is judged before its existence, in the orderexecuteasks the two names.plan-tracking-branch-missing._check_split_branchasks nothing about a split branch with a name finding, soadopt_branch: "a/"beside a source branchais one finding, not two.executestill refusesunsafe-valuefirst, in_checked_plan_values, now in the same words ascheck's finding. It refuses git's verdict amongcheck's other findings, in_refuse_what_check_finds. Both happen before any leg exists.planasksrefuse_bad_branch("adopt-bad-tracking-branch", ...)right afterchecked_valueon--tracking-branch, or on the source's default branch when the flag is not given. Its--helpsays so, anddocs/cli.mdis regenerated._in_the_waycompares throughpath_overlap, as a leg's mount path is compared. It returns(name, commit, aside), withCASE_ASIDEfor a name that is in the way only once case is ignored. Such a finding does not quote git's ref-lock words, which are not measured on a disk that ignores case. It says instead that git keeps each branch as a file named after it, that such a disk keeps two paths that differ only by case as one, and thatcheckandexecuterefuse the pair on every system.SPLIT_BRANCH_TAKEN_REMEDIATIONnames the case rule too._branches_atreads a local path's refs withgit for-each-ref(what a push into it meets), and any url withgit ls-remote --heads, unchanged._branches_a_clone_getsis thels-remotelisting, which is what_extract_leg'sgit clonegets._places_in_the_sourceis the source ascheckandexecute --local-remote-dirask it. Branches a clone gets are about either run; branches the source hides are a second place, about the push alone.--local-remote-dir,executeasks the extraction about the branches a clone gets. On main it read the source's own refs there, and refused a hidden branch in words about agit filter-repothat never meets it._refuse_a_split_branch_left_behindreads the source's own refs under--local-remote-dir, so that an earlier split branch the source hides, whichchecknow finds, is refused too._tip_atkeeps the oldgit ls-remote <url> <ref>for any other url._pushed_in_every_modekeeps askinggit ls-remote, asexecute --local-remote-dirasks the tracking branch (unchanged behaviour).After this change, on the same rows:
plan --tracking-branch a..b(and~,x.lock,a/,a//b,.a) exits 2 withadopt-bad-tracking-branchand writes nothing. A hand-edited name is onecheckfinding, andexecuteexits 2 with that finding and makes no leg. The correctedadopt_branch:plan adopts into the same--local-remote-dir.Adopt, a hiddenadopt(either knob) anda=bare each found bycheckand refused byexecutebefore any leg. The controls still adopt.Tests
All new tests live in
tests/test_adopt_branch_names.py(new, 22 tests) andtests/test_adopt_split_branch.py(43 to 53 tests together withtests/test_adopt_tracking_branch.py). That is 32 new test ids. Two existing tests gained a parameter and are renamed:test_a_branch_in_gits_way_on_github_is_refused_in_the_pushs_words[in-the-way]andtest_a_source_branch_in_the_way_is_refused_and_runs_once_it_is_gone[seen].test_adopt_branch_names.py:planrefuses each of adopt-project.py:--tracking-branchand the plan'stracking_branch:andadopt_branch:are held only tochecked_value's alphabet and never asked of git, so a name git refuses (~,a..b,x.lock, a trailing/) passesplanandcheckandexecutedies at the first git command that takes it, after a leg repository exists (git init --bare -b,git checkout -B, or a push to GitHub after bothgh repo createcalls), and the corrected plan is then refusedleg-remote-exists#420's six names.checkfinds each hand-edited name once, for both keys.checkholds both keys to the alphabet, with a name git creates and a deleted key as controls. Two bad names are found in one run, with the remediation said once.executerefuses before any leg, word for word. End to end,adopt_branch: "a..b"makes no leg and the corrected plan adopts into the same--local-remote-dir.test_adopt_split_branch.py: the three A4 names in the source, andAdopton GitHub. A branch hidden byuploadpack.hideRefsortransfer.hideRefsis found and refused under--local-remote-dir, and does not stop a GitHub run. An earlier split branch the source hides isadopt-branch-existsincheckand inexecute. End to end, a hiddenadoptis refused and the run adopts once the branch is deleted.Two existing tests pinned the behaviour this changes, and now pin the new one:
tests/test_adopt_plan_yaml_quoting.py:test_plan_records_a_branch_git_will_not_create_as_on_mainis nowtest_plan_refuses_a_branch_git_will_not_create_in_gits_words.plan --tracking-branch ~exits 2 withadopt-bad-tracking-branchand writes no plan. It is still never refused asadopt-manifest-misread, which was that test's point (rv181f's H1).tests/test_adopt_template_values.py: inPRINTED, the rowadopt_branch-surrogate(adopt_branch: "adopt/x\ud800") now expectscheckto exit 1 withFINDING unsafe-value: adopt_branch iswhere it expectedplan ok. That row was L2-1 itself.executestill refuses it asunsafe-value, andcheckstill prints the value spelled underWHAT WILL HAPPEN.Revert table. Each part was reverted alone in a scratch clone, and the three branch files were run there. Unreverted, 75 of 75 pass. The two updated tests were run under the first two reverts as well, and pass unreverted.
plan'srefuse_bad_branchtest_plan_refuses_a_tracking_branch_git_will_not_create[*](6), and the updatedtest_plan_refuses_a_branch_git_will_not_create_in_gits_words_entry_findingstest_adopt_branch_names.pytest but the sixplanones (16), and the updatedtest_check_prints_a_hand_edited_value_spelled[adopt_branch-surrogate]plan-tracking-branch-missingbeside a name findingtest_check_finds_a_name_git_will_not_create_once[*-tracking-branch](4),test_check_holds_both_names_to_the_alphabet_execute_holds_them_to[tracking_branch]_check_split_branchskipping a bad nametest_check_finds_a_name_git_will_not_create_once[a/-adopt-branch]_in_the_waytest_a_branch_in_gits_way_of_the_split_branch_is_taken[Adopt],[ADOPT/THREE-REPO-SHAPE],[Adopt/Three-Repo-Shape/old],test_a_branch_in_gits_way_on_github_is_refused_in_the_pushs_words[once-case-is-ignored]for-each-reffor a local pathtest_a_branch_the_source_hides_is_taken_where_the_split_is_pushed[uploadpack],[transfer],test_an_earlier_split_branch_the_source_hides_is_found_and_refused,test_a_source_branch_in_the_way_is_refused_and_runs_once_it_is_gone[hidden]test_a_branch_the_source_hides_is_taken_where_the_split_is_pushed[*]test_a_github_run_is_not_refused_for_a_branch_the_source_hides[*]_refuse_a_split_branch_left_behindreading the source's own refstest_an_earlier_split_branch_the_source_hides_is_found_and_refusedEvery new test fails under at least one revert. Before the change, all 32 new ids failed on bfed143.
Both modes, on head aee12f2, dirty 0. The set is 71 test files: the 70 that run adopt-project.py (the brief's
grep -l -E 'adopt-project|adopt_project' tests/*.py, plus the 18 files that run it through conftest'sADOPT;tests/test_shape_doctor.pyandtests/test_scaffold_leg_path_collisions.pyare among them), andtests/test_cli_reference.py, added becausedocs/cli.mdchanged.git-filter-repoon PATH: 5744 passed, 13 skipped, in three chunks, each EXIT=0PATH=<stub gh>:/usr/local/bin:/usr/bin:/bin): 5429 passed, 328 skipped, in three chunks, each EXIT=0A first run on the commit before
aee12f2failed only the two existing tests above, which pinned the old behaviour; they are updated in this commit.Main has moved since: v1.0.0 is tagged, and main was 2247dbf when this was measured, which also changes
tests/conftest.pyandtests/test_adopt_plan_yaml_quoting.py.git merge-treeof this head with 2247dbf is clean, tree 45b18ba3. On that merge, the four test files this PR changes andtests/test_adopt_tracking_branch.py, plustests/test_agents_md_codes.py,tests/test_repo_hygiene.py,tests/test_readme_sections.py,tests/test_cli_reference.pyandtests/test_clone_just_pushed.py, withgit-filter-repoon PATH: 882 passed, EXIT=0. Main is now bbcfd34.git merge-treeof this head with it is clean too; the only file both sides change isdocs/cli.md, in different hunks, and CI'stest_cli_reference.pychecks the merged file.Windows
Path.write_textand text-modesubprocessstdin write\r\n. Its header showsPath.write_text('a\nb\n')wroteb'a\r\nb\r\n'and stdin'x\ny\n'arrived asb'x\r\ny\r\n'.tests/test_adopt_branch_names.pyandtests/test_adopt_split_branch.pygave 69 passed, the end-to-end tests included.planwrote it. ThehideRefslines are appended to the source's.git/configwithnewline="\n".Adopt,ADOPT/THREE-REPO-SHAPE,Adopt/Three-Repo-Shape/old,adopt,adopt/three-repo-shape/old,lane-3/shape+fix@eagle,a=banda, and all are legal on NTFS. Each is created in a source that holds no case twin of it. The names git refuses (a..b,~,x.lock,a/,a//b,.a,a:b) are only ever plan values.for-each-refandls-remotelist a case twin under that name._refuse_a_split_branch_left_behindasks the exact name, so a twin is refused asplan-split-branch-takenthere, as on Linux. Nothing was run on macOS or Windows here;tests-macosandtests-windowsrun these tests.Not in this PR
_refuse_a_tracking_branch_the_remote_lacksstill asksgit ls-remoteunder--local-remote-dir. Soexecuterefuses, before any leg, a tracking branch the source hides withuploadpack.hideRefs, whichcheckpasses. This was measured after the change, with--tracking-branch developanddevelophidden. It is a refusal in the safe direction, and it is listed for Tracking: adopt-project.py, converting an existing repository in place #457.transfer.hideRefsorreceive.hideRefshides the split branch's own name (refs/heads/adopt, with no branch in the way) passescheck.execute --local-remote-dirthen makes both legs, and the push of the split is denied as a hidden ref. This was measured after the change, and it is listed for Tracking: adopt-project.py, converting an existing repository in place #457. A pattern that hides a branch in the way hides the split branch with it (transfer.hideRefs = refs/heads/adoptbeside a branchadopt):checknow namesadoptin the words of git's ref lock, where git's push would saydeny updating a hidden ref, and onceadoptis deleted the run meets the case above.adopt_branch: trueoradopt_branch: 007checks ok and adopts onto a split branchTrueor7without a word (measured;~andnullfall back to the default). This is the YAML-scalar question scaffold-project.py writes--idand--tracking-branchinto project.yaml unquoted, so--id nullor--id truescaffolds a project whose first bootstrap failsmanifest-id, and--tracking-branch true,007ornullputs the clone's legs onTrue,7ormainwith no upstream: #181's defect on the scaffold side, beside #225 #258 asks of the manifest's values, and it is listed for Tracking: adopt-project.py, converting an existing repository in place #457.scaffold-project.pyandfamily.pyare scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283's (PR scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355) and are untouched.Known limits
checknow runsgit check-ref-format --branchthree more times (twice in_entry_findings, once in_check_split_branch), andexecuteruns it twice more. Each run is asked of no repository, from an empty temporary directory, as scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283's helper does.for-each-refandls-remote. A local path is told from a url byremote_is_a_path, as elsewhere in this file.Closes #520
Closes #420
🤖 Generated with Claude Code
Summary by Sourcery
Prevent adoption runs from partially progressing by validating branch names and detecting hidden or case-insensitive branch conflicts before any leg is created.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: