Skip to content

setup.sh, install.sh and openRepoShape: a ref git refuses part by part (a.lock/b, a/.b, a//b, a/b/, a.) is refused by each of them before the clone, the fetch or the record, and each refusal states git's rule - #875

Merged
brettheap merged 32 commits into
mainfrom
fix/874-ref-parts
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #874 (comment)

A v1.1.1 DRAFT. Branch fix/874-ref-parts, from origin/main d6071c7d.

Stacked on

Both merges were clean, and both heads were unmoved when the branch was pushed. Review the commits after a1fa6768, the last merge: one, the fix round's 8d6c5bdb, on rv874's review (see the last section). 456589b3 (the change and its tests) and 17185b67 (the tests only, putting the network stubs under two of the table tests) are what rv874 reviewed at a1fa6768.

#867 has landed as 5c54af2. The branch then merged #867's final head 0ccda48b (8d9cd4d9), and main's squash 5c54af2 with -s ours (a1fa6768), because the squash's tree a4b0a79d is the final head's. So the diff from main is #872's and this PR's alone: git diff 5c54af2 a1fa6768 equals git diff 89be75bf 17185b67 byte for byte, apart from the index lines. #872 is still open; when it lands, this branch takes its final head and its squash the same way.

#872 has landed as c5d0bde. The branch merged #872's final head a1cad963 (5481abe7), and main's squash c5d0bde with -s ours (34d2645f), because the squash's tree 4156a29a is the final head's. Both parents have now landed, so the diff from main is this PR's alone. The head's tree 07b98c23 is main c5d0bde with the writer's two rounds applied (145a5ee3..17185b67 and a1fa6768..8d6c5bdb), measured with a scratch index.

This PR lands after #873 (lane 5's, fix/857-setup-dry-run-offers, which gives setup-project.py git's per-part rule as why_not_a_ref) and does not merge it: #873 edits setup-project.py only. #873's head is now ac570069, not the 79a33f11 the issue names: one commit on top, test-only, and setup-project.py is the same bytes. setup.sh's comment names why_not_a_ref, which exists once #873 lands.

Main 5c54af2 is merged, as above.

Decisions

  1. What each copy gates, measured on the stacked base with stub gh and curl that log and fail, a local repository, and GH_TOKEN=bogus. On a.lock/b, a/.b, a//b, a/b/ and a.:

    • setup.sh printed (0) self-bootstrap, ran git clone, and ended error: pathspec '<ref>' did not match any file(s) known to git, exit 1. Its check runs only on the self-bootstrap path (is_shape_checkout execs setup-project.py before it), so it gates the clone.
    • install.sh asked curl for https://raw.githubusercontent.com/opensoft/openRepoShape/<ref>/openRepoShape (one request). The version is the URL's path, so the check gates the fetch.
    • openRepoShape --install with OPENREPOSHAPE_RECORD_REF=1 exited 0 and wrote the ref to openRepoShape.ref. ref_fault's shape line gates the record an install writes and every run reads (read_recorded_ref refuses a bad one before any request), and the doctor's git checkout.
  2. Every copy holds the rule itself. None of the three only gates a later setup-project.py run: between each check and that run there is a clone, a fetch or a record, so the issue's second option ("say so and leave it to that run") fits none.

  3. The rule, one pattern list in all three, plain case patterns that bash 3.2 reads, inside each copy's character set, which is neither widened nor narrowed:

    [!A-Za-z0-9]* | *..* | */.* | *.lock | *.lock/* | *//* | */ | *. | *[!A-Za-z0-9._/-]*
    

    The first, second, fourth and last are today's. New: */.* (a part after a / starts with .), *.lock/* (a part before a / ends in .lock), *//* (an empty part), */ (a / at the end), *. (the name ends in .). That is why_not_a_ref's five facts and no more. A leading / and a leading . stay the first pattern's. The line is written out in each file, not shared: install.sh and setup.sh arrive alone down a pipe.

  4. Checked against git, not argued. Every name of up to five parts from a, 0, ., /, - and .lock that the character set takes is 3110 names. Each was fed to the pattern list, read out of each of the three files, and to git check-ref-format --branch (2.43.0). The old line disagrees with git on 1122 of them (setup-project.py: a passed-through --dry-run makes none of the preflight's offers and --shape-ref is held to git's rule part by part, so a rehearsal no longer writes git's global config and a.lock/b is refused before the clone (#857) #873's number for the Python rule) and the new one on none, in each file.

  5. The words: one sentence per refusal, the same in all three, which states the whole rule. The lead is as it was; the first half says an ASCII letter or digit and ASCII letters, as setup-project.py: a passed-through --dry-run makes none of the preflight's offers and --shape-ref is held to git's rule part by part, so a rehearsal no longer writes git's global config and a.lock/b is refused before the clone (#857) #873's REF_RE words do (fix round 1, NIT 7); the end, has no '..' and does not end in '.lock'., becomes the rule in setup-project.py's phrases where they fit (an empty part between slashes, the name git gives the lock file it writes beside a ref, a revision range). A sentence per failed part, as why_not_a_ref has, was weighed and left: three more die lines in each file, and nothing asks the shell copies to say which part failed.

  6. setup.sh's comment said it mirrors REF_RE; it now says it mirrors checked_ref, which asks REF_RE and then why_not_a_ref, lists the rule, and says the check gates the clone.

  7. No test pinned the old words (git grep -n "does not end in\|starts with a letter or digit" -- tests found none), so no existing test was changed; REF_RULE_WORDS pins the new ones, in all three.

What changed

The refusal of each copy for a.lock/b, measured by a probe. Before (the topic.lock refusal, since the old line passed a.lock/b):

REFUSED: --shape-ref is 'topic.lock', which is not a branch, tag or commit this tool will pass to `git checkout`: a ref starts with a letter or digit, uses only letters, digits, '.', '_', '/', '-', has no '..' and does not end in '.lock'.
REFUSED: the version is 'topic.lock', which is not a release tag, branch or commit: a ref starts with a letter or digit, uses only letters, digits, '.', '_', '/', '-', has no '..' and does not end in '.lock'.
REFUSED: $OPENREPOSHAPE_REF is 'topic.lock', which is not a branch, tag or commit this tool will pass to `git checkout`: a ref starts with a letter or digit, uses only letters, digits, '.', '_', '/', '-', has no '..' and does not end in '.lock'.

After, setup.sh, install.sh and openRepoShape, for a.lock/b and for topic.lock alike:

REFUSED: --shape-ref is 'a.lock/b', which is not a branch, tag or commit this tool will pass to `git checkout`: a ref starts with an ASCII letter or digit, uses only ASCII letters, digits, '.', '_', '/', '-', has no '..' (git reads it as a revision range), no empty part between slashes ('//', or a '/' at the end), no part that starts with '.' or ends in '.lock' (the name git gives the lock file it writes beside a ref), and does not end in '.'.
REFUSED: the version is 'a.lock/b', which is not a release tag, branch or commit: a ref starts with an ASCII letter or digit, uses only ASCII letters, digits, '.', '_', '/', '-', has no '..' (git reads it as a revision range), no empty part between slashes ('//', or a '/' at the end), no part that starts with '.' or ends in '.lock' (the name git gives the lock file it writes beside a ref), and does not end in '.'.
REFUSED: $OPENREPOSHAPE_REF is 'a.lock/b', which is not a branch, tag or commit this tool will pass to `git checkout`: a ref starts with an ASCII letter or digit, uses only ASCII letters, digits, '.', '_', '/', '-', has no '..' (git reads it as a revision range), no empty part between slashes ('//', or a '/' at the end), no part that starts with '.' or ends in '.lock' (the name git gives the lock file it writes beside a ref), and does not end in '.'.

What each does with the five names, before and after (exit codes; git clone lines counted on stdout; requests counted in the stub's log):

copy before after
setup.sh --shape-ref <ref> prints (0) self-bootstrap, 1 git clone, exit 1 (pathspec ... did not match) exit 2, 0 git clone lines, nothing in $TMPDIR
install.sh <ref> 1 curl request, exit 2 (could not fetch) exit 2, 0 requests
openRepoShape --install (opt-in) exit 0, <ref> written to openRepoShape.ref exit 2, nothing placed

topic.lock was refused by all three before and is after, and main is taken by all three.

Tests

One table, REF_PARTS in tests/conftest.py, read by three files (so Sonar's duplication gate sees one copy): 36 names, with whether git takes each.

  • The issue's: a.lock/b, x/y.lock/z, release.lock/v1, a/.b, a/.b/c, a//b, a/b/, a.lock/, a., and topic.locked, v1.0.0.
  • rv873's: a/b/.c/d, x/.y/z/w.
  • Of the issue's kind, and taken by the old line too: a/b., a/..
  • Still refused by the whole-name rules: topic.lock, a..b, a/.., a/b.lock, .lock, /a.
  • Still passing: main, v1.1.0, feature/x-y_z, a 40-hex sha, workBenches' pinned-by-workBenches-no-fetch, and a.lockb, a.lock-x/b, a/b.lockb/c, a./b, a/lock/b, refs/tags/v1.0, v1.0.0-rc.1, and, with a part that starts upper-case, _ or -, Feature/ABC-123, fix/_tmp, fix/-wip (fix round 1).

git_takes_ref asks the git on PATH (git check-ref-format --branch) and never copies its answer. assert_a_copy_answers_as_git_does holds git to the table's row first, then the copy to git, so a stale row is named as the row. assert_the_refusal_states_the_ref_rule pins the shared sentence, REF_RULE_WORDS, in all three refusals.

file test (36 rows each) what it pins
tests/test_setup_sh.py test_the_shim_takes_a_shape_ref_as_git_does_before_any_clone a copy of setup.sh outside any checkout, OPENREPOSHAPE_REPO a path nothing is at: a ref it takes prints (0) self-bootstrap and fails at the clone, offline; a ref it refuses exits 2 with the rule, prints no git clone line, leaves $TMPDIR empty
tests/test_install_sh.py test_the_installer_takes_a_version_as_git_does_before_any_request through Net.run(<ref>) and its fakes: a version it takes is asked of curl (a call is logged); one it refuses logs no call, exits 2 with the rule, places nothing
tests/test_openreposhape_command.py test_an_install_records_a_ref_as_git_takes_it_and_refuses_the_rest through install_at with the opt-in (--version asks only the characters since #872): a ref taken is recorded, exit 0; one refused exits 2 with the rule and places nothing

Every run keeps the network out of reach, with a gh and a curl that log and fail, and local paths only. install.sh's test runs under that file's own fakes and asserts a refused version logs no call. The other two put fake_github(tmp_path, (), log=...) first on $PATH (17185b67): the setup.sh test asserts the log was never written; the launcher test asserts no curl ran and no gh but the gh auth status an install that placed the command asks (the machine report), and that a refused install asked for nothing. install_at takes an env_also for it.

Runs, /usr/bin/git 2.43.0, /usr/bin/python3, one pytest process at a time, in two batches. Batch 1: test_setup_sh, test_install_sh, test_install_sh_filter_repo, test_repo_hygiene, test_cli_reference, test_agents_md_codes. Batch 2: test_openreposhape_command, test_openreposhape_package, test_setup_project_py. Those are the issue's four files, every file that names a word changed (git grep -l), and the common three.

batch 1 batch 2 total
stacked base 145a5ee3, before the change (batch 1 without test_install_sh_filter_repo) 471 passed 699 passed 1170 passed
17185b67, after (same batch 1) 537 passed 732 passed 1269 passed
a1fa6768, the restacked head rv874 reviewed, before the fix round 555 passed 732 passed 1287 passed
8d6c5bdb, after the fix round 561 passed 735 passed 1296 passed

The 99 more at 17185b67 are the three new tests at 33 rows each; the 9 more at 8d6c5bdb are the three rows the fix round adds to each. Nothing was red before. The whole suite is the coordinator's gate.

On the base: the three new tests against the base copies of the three files (145a5ee3): 57 failed, 51 passed (the 17 passing names, three times), measured again with the fix round's 36 rows. Per copy, 13 names the old line takes and git refuses fail, and the 6 refused names it already refused fail on the words.

Mutants, each applied to the case line, the copy's own table test run, the file put back at once (git status clean after). All 33 (11 per copy) are caught:

mutant rows that fail, in each copy's test
the old line back, *.lock the only .lock check 13: a.lock/b, x/y.lock/z, release.lock/v1, a/.b, a/.b/c, a//b, a/b/, a.lock/, a., a/b/.c/d, x/.y/z/w, a/b., a/.
drop */.* a/.b, a/.b/c, a/b/.c/d, x/.y/z/w
drop *.lock/* a.lock/b, x/y.lock/z, release.lock/v1
drop *//* a//b
drop */ a/b/
drop *. a., a/b.
drop *.lock topic.lock, a/b.lock
add *./* (refuses a part that ends in . but not the name) a./b
the words no empty part between slashes cut to no empty part the 19 refused rows (the words assertion)
the lead's an ASCII letter or digit cut back to a letter or digit the 19 refused rows (the words assertion)
setup.sh: add */[-_]* (refuses a part that starts with - or _) fix/_tmp, fix/-wip
install.sh: add */[A-Z]* (refuses a part that starts upper-case) Feature/ABC-123
openRepoShape: [!A-Za-z0-9]* becomes [!a-z0-9]* Feature/ABC-123

The last three mutants survive the table without the fix round's three rows (deselected: 33 passed in each copy, as rv874 measured) and are killed with them.

git: /usr/bin/git 2.43.0 only. The issue also names 2.47.1: it was not run here, and CI runs a newer git. The tests ask whichever git runs them, so CI holds the copies to that git's answer. No Windows or macOS run: CI is Linux only (#597).

Not changed

  • The bin directory's byte check, misdrawn_characters, locale_reading, refuse_bad_values, checked_repo and ref_character_fault, and the length and control-character checks of all three.
  • A fourth copy, in the Python package: src/openreposhape/standard.py:215 keeps the old rule (not REF_RE.fullmatch(ref) or ".." in ref or ref.endswith(".lock")) and the old sentence. target() checks a ref "before anything reaches git", so it gates the package's clone of the standard. rv874 confirmed it with a probe on a detached copy of the package: 1 git clone into the cache for each of the five names, then REFUSED: cloned ... but could not check out. The issue names three bash copies and this PR touches no Python, so it is left; it wants its own issue. Its docstring at :198-201 says it is "the launcher's ref_fault, in its order and its words", which is no longer true of the words once this lands.
  • openRepoShape:829 shape_checkout (--doctor from a download) runs git clone (:834) before checked_ref (:839). test_a_ref_the_doctor_would_check_out_is_validated pins that order in its docstring ("clones this checkout first, then checks the ref, and refuses before git checkout"), the old rule had it for a..b too, and this PR changes the rule and its words only. Moving checked_ref above the clone is a one-line change for a later issue. rv874 confirmed it with a probe on an installed copy: --doctor with OPENREPOSHAPE_REF set to each of the five names shows 1 git clone and 0 checkouts, then the new refusal. Both go to Brett Heap as candidates; nothing is filed without his word.
  • A scaffold run's $OPENREPOSHAPE_REF, only set for the run and not recorded, is asked its characters before the fetch (refuse_bad_values, openRepoShape: --version and two fetch refusals print $OPENREPOSHAPE_REPO and $OPENREPOSHAPE_REF with no check, so an escape or a bidi override in either reaches the terminal as typed, and a scaffold run sends its fetch with them #866, by design). A ref this rule refuses never reaches setup.sh's check: the fetch of setup.sh at such a ref fails first (2 requests, gh api .../contents/setup.sh?ref=a.lock/b and then curl, at the base and at the head alike), and the run ends could not fetch setup.sh, with nothing cloned. That is openRepoShape: --version and two fetch refusals print $OPENREPOSHAPE_REPO and $OPENREPOSHAPE_REF with no check, so an escape or a bidi override in either reaches the terminal as typed, and a scaffold run sends its fetch with them #866's design, and this PR does not move it.
  • No documentation states the rule (docs/cli.md's --shape-ref help, the README, the handbook and the release notes were grepped for it), so none is touched, and no --help text changed.

Fix round 1, on rv874's review (READY, 2 SHOULDs, 6 NITs)

rv874's review of a1fa6768: #875 issuecomment-6246173536. No MUST. The fix round is 8d6c5bdb, on top of the restack; nothing was rebased, amended or force-pushed.

# finding what was done
SHOULD 1 REF_PARTS has no part that starts upper-case, _ or -, so three over-refusing mutants pass all 33 rows Feature/ABC-123, fix/_tmp and fix/-wip join the "still pass" group (tests/conftest.py). With the three rows deselected the mutants pass 33 rows in each copy; with them: setup.sh + */[-_]* fails fix/_tmp and fix/-wip; install.sh + */[A-Z]* fails Feature/ABC-123; openRepoShape with [!a-z0-9]* fails Feature/ABC-123. All three tables pass: 108 rows
SHOULD 2 src/openreposhape/standard.py:198-219 keeps the old rule and sentence, and its docstring says it has the launcher's words Not changed: a follow-up for Brett Heap. rv874's probe is in Not changed
NIT 3 a/b. and a/. sat under "Refused before #874 as well", but the old line took both Both moved up with the issue's names (the 13 rows the old line takes now come first, as the docstring says). The body's lists are fixed above
NIT 4 setup.sh's comment: why_not_a_ref, #874 reads as #874's, and "one pattern for each" is not exact Now "(#857's why_not_a_ref, held here since #874)" and "one or two patterns for each"
NIT 5 install.sh:161 says "before any request", untrue with no version named (releases/latest is asked first) Now "before the launcher is fetched"
NIT 6 this body said setup.sh's check follows the fetch, which is untrue for a ref the rule refuses The body only: the "Not changed" bullet now says the fetch fails first (2 requests, could not fetch setup.sh), and that it is #866's design
NIT 7 the sentence says "letters" where #873's REF_RE words say "ASCII letters" The sentence is now a ref starts with an ASCII letter or digit, uses only ASCII letters, digits, ... in setup.sh, install.sh, openRepoShape and REF_RULE_WORDS, byte for byte the same (REF_RULE_WORDS is found once in each file). #873's phrase is "an ASCII letter or digit first, then ASCII letters, digits". setup.sh's comment says it too. Mutant: the lead cut back to a letter or digit fails the 19 refused rows in each copy. No other test pinned the words
NIT 8 openRepoShape:834 clones before :839 checked_ref Not changed: a follow-up for Brett Heap. rv874's probe is in Not changed

Runs after the fix round, /usr/bin/git 2.43.0: batch 1 561 passed, batch 2 735 passed (1296; 1287 before, at a1fa6768). All 33 mutants are caught again (11 per copy, listed in Tests).

Closes #874

🤖 Generated with Claude Code

Summary by Sourcery

Reject Git-invalid ref parts consistently across the shell entry points before any clone, fetch, or record operation.

Bug Fixes:

  • Align ref validation in setup.sh, install.sh, and openRepoShape with Git’s part-by-part branch-name rules, preventing invalid refs from reaching cloning, fetching, or recording.

Enhancements:

  • Update rejection messages and setup comments to explain the complete Git-compatible ref rule.
  • Add shared table-driven coverage that compares all three shell copies against the Git version available in the test environment, including checks for side effects and rejection wording.

Tests:

  • Add comprehensive tests for valid and invalid ref parts across setup.sh, install.sh, and openRepoShape, including network, clone, and recording guards.

Brett Heap and others added 27 commits October 16, 2026 14:07
…IR` is respelled with `cygpath -u` or refused before install.sh's fetch, and the PATH note single-quotes a directory bash would still read, and `--install` refuses one holding `:` or a newline, so install.sh names `/d/tools/bin` as the command does and the `export PATH` line does what it says (#667)

#649's two leftovers, both from #656's review.

install.sh read `$OPENREPOSHAPE_BIN_DIR` as typed, so with `D:\tools\bin`
its `preflight:` and `done:` lines (and `install:` for an old release) said
`D:\tools\bin/openRepoShape` while the command's own lines said
`/d/tools/bin/openRepoShape`, and where no `cygpath` answers the command's
`--install` refused (exit 2) only after install.sh had fetched it. install.sh
now carries a copy of the command's rule where it reads the variable, before
the symlink check and the fetch: a value that starts `[A-Za-z]:/` or
`[A-Za-z]:\` is spelled with `cygpath -u --`, and is refused with exit 2 where
no `cygpath` answers, fails, or answers a drive spelling again. The refusal
does not echo the value.

The command's PATH note printed `export PATH="$dir:$PATH"` with the directory
between double quotes as it was, so `a"b$HOME c` printed a line `eval` could
not parse, and a `$`, a backquote or a `\` in a name was read, not kept.
`export_path_line` keeps today's line byte for byte for a directory that
double quotes hold (a space and an apostrophe included) and, for one holding
a `"`, `$`, backquote, `\` or `!` (bash reads that at a prompt as history),
prints it between single quotes with `'\''` for an apostrophe. `--install`
also refuses a directory holding a `:` or a newline, which no `$PATH` line can
name, before anything is placed; the newline's refusal does not echo it.

Tests feed one value through install.sh and through the command, in each
spelling and each state of `cygpath`, and pin every `install:`, `preflight:`
and `done:` line; the PATH note is checked as printed, by `eval` and by
`bash -i`.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…holding a control character before they name it, and install.sh refuses a `:` before its fetch, so a carriage return or an escape is no longer echoed as typed (#760)

#667's two checklist candidates, with rv667's NIT 2.

`--install` refused a newline in `$OPENREPOSHAPE_BIN_DIR` and nothing else
that is a control character: a carriage return, an escape, a tab or DEL
placed the command and was printed as typed (`/tmp/x^MFORGED is not on
$PATH`; an escape sequence reaches the terminal the same way). The newline
arm is now `*[[:cntrl:]]*`, first in `install_commands`, ahead of the
Windows spelling and the colon, and its refusal still says nothing of the
value. It is `[[:cntrl:]]` and not `[![:print:]]`, so a directory with a
letter the locale does not print (`café` under LC_ALL=C) is placed. Which
C1 controls match is the locale's, measured with bash 5.2.21: C.UTF-8
matches U+0080 to U+009F (and U+2028, U+2029), C and POSIX match none, and
the ASCII controls and DEL match in all three.

install.sh learnt of a `:` or a newline only after its fetch: it fetched the
launcher, ran `--install`, and said only that `--install` refused (exit 2); a
release older than #667 placed the command under the directory. It now
carries both arms of the command's `case`, copied as #667 copied the
drive-letter one: the control-character arm first, before any line names the
directory, and the colon arm after the Windows respelling, both before the
symlink check and any request. Where the control-character refusal says
nothing of the value, the colon refusal names the directory, as the
command's does.

Tests feed each value through install.sh and through the command (a colon,
two directories, a drive-relative name, a newline, a carriage return, an
escape, a tab, DEL, a drive spelling with a newline or an escape, a colon
with an escape): exit 2, no request, nothing placed, neither stream holds a
forged tail. A link in a directory holding a carriage return, a drive
spelling, a colon with an escape, a letter under LC_ALL=C and LC_ALL=C.UTF-8,
a C1 control under a UTF-8 locale, and the `$HOME` default pin the order and
the width of the arms.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…`LC_ALL=C` and names a C1 control's two bytes, so U+0080 to U+009F is refused in every locale and whatever else the value holds, where a UTF-8 locale let one through beside a byte that is not UTF-8 (#760)

Lane 1's review of #804 (finding 1): `*[[:cntrl:]]*` matches a C1 control only
in a value that is valid UTF-8. One byte that is not (a lone 0xE9) makes bash
match byte by byte, and then U+009B's two bytes (0xC2 0x9B) are not controls,
as under `C`. Measured on 7569ffa with bash 5.2.21: under `LC_ALL=C.UTF-8`
and `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/caf`, 0xE9, U+009B, `31mFORGED/bin`,
`openRepoShape --install` exited 0, placed the command and printed the value
raw in four lines; install.sh, given the same value after a `:`, exited 2 with
the colon refusal naming it, the C1 control in the line.

Both files now run the check as `(LC_ALL=C; case ... *[[:cntrl:]]* |
*$'\302'[$'\200'-$'\237']* ...)`: the ASCII controls and DEL byte by byte, and
the two bytes of each C1 control named, so the answer is the same in every
locale. Every value measured on 7569ffa and refused only under C.UTF-8, or
not at all, is refused with exit 2, nothing placed, nothing echoed, and
install.sh makes no request. `café` and U+00A0 are still placed. U+2028 and
U+2029, which only a UTF-8 locale's `[[:cntrl:]]` matched, are now placed in
every locale, and a test says so. Both comments say what the check does, and
no longer say the C1 controls are the locale's.

Tests: the C1 case, which skipped where no UTF-8 locale read U+009B as a
control, is now five values (U+009B, the first and the last of the range, a
0xE9 byte before U+009B, and a colon beside that) in each of `C` and
`C.UTF-8`, through the command and through install.sh. The placed-letter test
gains U+00A0 (the byte after the range's last), U+2028 and U+2029. 18 of the
36 fail on 7569ffa's code and the other 18 are guards; 18 mutants of the two
code hunks are caught.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	install.sh
#	openRepoShape
#	tests/test_install_sh.py
#	tests/test_openreposhape_command.py
…holding a bidi control, a character of no width, a soft hyphen or U+2028 before any request and name it by its code point, so no line about `$PATH` shows the directory as another (#837)

#804's check refuses the C0 and C1 controls and DEL, and passed every
character a terminal draws as something other than itself, under `C` and
`C.UTF-8` alike: the command placed itself under `<dir>/a` U+202E
`forged/bin` and printed the value in four lines, and install.sh made its
request. Laid out as a terminal lays out a line (fribidi), `~/` U+202E
`.local/bin` reads `~/nib/lacol.`, `/opt/tools-1` U+200F `/2/bin` reads
`/opt/tools-12//bin`, and `~/.local` U+200B `/bin` reads `~/.local/bin`.

Both files now refuse, right after the control check, before the drive
spelling, the colon, the symlink check and any request, a directory
holding one of 29 code points, `MISDRAWN_CHARACTERS` in the launcher:
U+00AD; the twelve bidirectional controls (U+061C, U+200E, U+200F,
U+202A to U+202E, U+2066 to U+2069), as `repo_shape.spelled` escapes
them (#391); U+2028 and U+2029, which end a line for every reader that
keeps Unicode's line breaks, and which #804 left to a ruling; and U+200B,
U+FEFF and U+2060 to U+206F, of no width, the whole block
default-ignorable. U+200C and U+200D stay allowed: Persian, the Indic
scripts and emoji sequences need them. So does every space.

`misdrawn_characters` builds each one's UTF-8 from its code point with
printf's octal escapes and looks for the bytes under `LC_ALL=C`, in the
subshell #804's check uses, so one list says what is refused and what
the refusal names, and no byte is read back as a number. The refusal
says "holds a character a terminal draws as something other than itself
(U+202E): one that is invisible, ends a line or reorders the text around
it, so no line about $PATH can print the name as it reads", names every
one found, and nothing else of the value. install.sh carries the list and
the function as the launcher has them, and a test holds the two to one
text.

Tests: #804's C1 locale test in each file now runs `MISDRAWN_BIN_DIRS`
too (each code point, two at once, after a byte that is not UTF-8, beside
a colon, in a drive spelling), under `C` and `C.UTF-8`, through the
command and install.sh, with no request; the link test takes U+202E; the
lifted function names exactly its 29 over every code point that shares a
first byte with them (896, on stdin, so it runs under Git Bash);
`NOT_CONTROLS` drops U+2028 and U+2029 and gains a Persian word with
U+200C, an emoji sequence with U+200D, U+2026 and U+FF21.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion and the redirect's location are refused unechoed for a misdrawn character too, the set takes in U+FFF9 to U+FFFB, U+1D173 to U+1D17A and U+E0001, and the refusal says a character may be drawn as another (#837)

Fix round 1 on rv841's review of #841.

The set (SHOULD 1). `MISDRAWN_CHARACTERS` gains FFF9-FFFB (the
interlinear annotation characters), 1D173-1D17A (the musical beam, tie,
slur and phrase controls) and E0001 (the deprecated language tag): format
characters of no width (glibc `wcwidth` 0) that nothing needs, which the
command placed and printed in four lines. The encoder builds the
four-byte form too, with bash 3.2's arithmetic and `%b`'s `\0NNN`. Every
other format character is left out for a reason the comment now gives:
U+200C, U+200D and U+180E, which Persian, the Indic scripts, emoji and
Mongolian need; the Egyptian hieroglyph and shorthand format controls,
with which those scripts build their signs; the tags, the variation
selectors, U+034F and U+1160, drawn as part of the character before them;
the signs drawn over the digits after them, which take a cell; and every
space and the blank Hangul fillers.

The repository and the ref (SHOULD 2). #837's line named the
`[![:print:]]` checks of both files: a UTF-8 locale's U+202E, U+200B,
U+FEFF or U+00AD passes them, and the refusal after them echoed the value.
Each now asks `misdrawn_characters` first, and refuses by the code point,
saying nothing else of the value: `checked_repo`, `checked_version` (the
version named and the newest release's tag), the option check and
`latest_from_redirect`'s location in install.sh; `shape_checkout` and
`ref_fault` (`$OPENREPOSHAPE_REF` and the record) in the command. First,
because `[![:print:]]` is the locale's, so the words do not change with it.

The words (NIT 3): "one that is invisible, is drawn as another character,
ends a line or reorders the text around it", because U+00AD is drawn as a
hyphen. The order (NIT 5): a tab before U+202E is refused as a control
character, and a row holds it.

Tests: `MISDRAWN` and the locale table take the twelve new code points and
the tab row; the lifted sweep takes U+1D100 to U+1D1FF, U+E0000 to U+E007F
and a sample of the planes past U+FFFF; `NOT_CONTROLS` takes a subdivision
flag (tags U+E0020 to U+E007F); a test of each file refuses a value holding
U+202E at every new site under `C` and `C.UTF-8`, through both entry
points, with no request, nothing placed and neither stream holding it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fuse is read again in the locale's own charset, so a GB18030, GBK or Shift_JIS name that holds no control character and nothing drawn as another is placed, and where nothing can read it the bytes decide as before (#858)

#804's control check and #837's `misdrawn_characters` look for UTF-8's
bytes under `LC_ALL=C`, and a multibyte charset that is not UTF-8 reads
some of those bytes as text: GBK, GB18030 and CP936 read `0xC2 0x80` to
`0xC2 0x9F` as 32 hanzi; GBK, Big5, EUC-JP and EUC-KR read U+00AD's two
bytes as one character; and in Shift_JIS a kanji whose second byte is
`0xC2` before a common kanji puts a C1 control's two bytes side by side
(390 in 100,000 random pairs of common kanji, measured with Python's
codecs). Such a directory, the default `$HOME/.local/bin` under a user
name among them, was refused as holding a control character or a
character drawn as another, in a locale where it holds neither.

Both files now carry `locale_reading` (install.sh as a copy, held to one
text by a test). Where the bytes refuse a directory, it asks `locale
charmap` under the person's own locale, and where the answer is neither
empty, UTF-8 nor ASCII, `iconv` reads the whole value from that charset
into UTF-8; the reading is used only where it has fewer characters than
the value has bytes, so a single-byte charset keeps the byte rule, as the
issue's Direction says. The C1 bytes and `misdrawn_characters` are then
asked of the reading, and `[[:cntrl:]]` of the bytes as typed. Only a
value the bytes refuse is read again, so no name placed before is
refused; with no `locale` or `iconv`, a charset either does not know, or
a value the charset cannot read, the bytes decide, in #804's and #837's
words.

A lone `0x9B` stays placed, the limit #804 states: a terminal that reads
UTF-8 draws it as a character it cannot read, only one set to an 8-bit
charset acts on it, and a rule needs a UTF-8 validity step bash does not
have. The launcher's comment says so.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…above)

# Conflicts:
#	install.sh
#	openRepoShape
#	tests/test_install_sh.py
#	tests/test_openreposhape_command.py
…cked once, before `--version` answers and before any fetch, so a control character or a character a terminal draws as another in either is refused, never printed or sent (#866)

`--version` printed both values as typed and exited 0. A scaffold run sent
its fetch with them, and so did `--install` from stdin, and each named
them three times in its refusal. So an escape, a C1 control, U+202E or
U+200B in either value reached the terminal as it was typed.

One function, `refuse_bad_values`, now runs once the arguments are read,
and inside the `--version` case, which answers within the argument loop. It
refuses the broken record first, as before. Then it checks
`$OPENREPOSHAPE_REPO` with `checked_repo`, `shape_checkout`'s guard moved
whole: #841's misdrawn characters, then a leading `-` or `[![:print:]]` on
the clone URL. Then it checks `$OPENREPOSHAPE_REF` with
`ref_character_fault`, `ref_fault`'s misdrawn and control-character checks,
moved whole. `ref_fault` calls them in the same place. Every refusal keeps
the words it had.

The ref's length and shape stay where a ref is used: `checked_ref` when
`--install` records, `shape_checkout` before `git checkout`, and setup.sh's
`--shape-ref` checks. So `--version` still prints any printable ref in
force, and an install that records no ref still takes one that names
nothing, as workBenches sets it.

`--version` refuses with exit 2, as a broken record already makes it do,
and does not respell the value. install.sh's `shown=` parse is unchanged
for every value it can hand the command: an owner/name that its own
`checked_repo` passed, and the tag that its `checked_version` passed. Both
pass this check, and `--version` prints them byte for byte.

`--help` still answers first. It names the variables, never their values.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_BIN_DIR` must write back into the value byte for byte, and tests hold that the reading is itself checked, that `locale charmap` is asked under the person's own locale and that a name the bytes pass is never read again (#858)

Fix round 1 on rv858's review of #867 (READY, 2 SHOULDs, 5 NITs).

SHOULD 1: no test had a multibyte charset read a name whose reading still
holds a C1 control or a character drawn as another, so `locale_reading`
printing anything at all (rv858's R1) shipped green. Two rows now do,
through both files: a GB18030 hanzi of the C1 bytes beside U+009B in
GB18030's own four-byte form (`0x81 0x30 0x83 0x37`), refused as a control
character, and beside U+202E (`0x81 0x36 0xA7 0x31`), refused naming
U+202E, install.sh before any request.

SHOULD 2: the stub `locale` answered one charmap whatever it was asked
under, so `LC_ALL=C locale charmap` (R2) shipped green. `put_a_locale`'s
stub now answers as glibc's does, from the first of `$LC_ALL`,
`$LC_CTYPE` and `$LANG` set: a marker `xx_XX.<charmap>` gives that
charmap, `C.UTF-8` gives UTF-8, anything else `ANSI_X3.4-1968`; the cases
run with `LANG` the marker and `LC_ALL` and `LC_CTYPE` empty, so bash runs
in `C` (it says nothing of a `LANG` that is not installed). `C` and
`C.UTF-8` stay where they mean something: as the person's own locale, in
a test of three names refused there with `iconv` never asked; and as
bash's own locale, in a test of `locale_reading` lifted out of each file
with a `locale` function, `C.UTF-8` standing in for a multibyte locale of
bash's own, where the readings are Python's codecs' and the count stays
in bytes.

NIT 3: a name the bytes pass (`café`) under a GB18030 locale, with an
`iconv` that leaves a mark if it runs: placed, no mark (R3, the trigger
replaced by `if :; then`).

NIT 4: an `iconv` that exits 0 and prints nothing passed the count. The
reading must now write back into the charset as the value, byte for
byte (`iconv -f UTF-8 -t <charmap>`), in both copies; a broken `iconv`
falls back to today's refusal, and a test holds it. The rows where
`iconv` must not be asked use the marking `iconv` instead of one that
reads anything, which the round trip would refuse anyway.

NIT 5: `locale_reading`'s comment, both callers' and `export_path_line`'s
say the trade: a multibyte locale that is not UTF-8, in a terminal that
reads UTF-8, is handed C1 bytes it reads as letters. NIT 7: the
launcher's comment names the ISO 8859 case (the charset is known and
`a`, `0x9B`, `b` is placed) as a candidate, left. NIT 6 is the PR body's.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…BIN_DIR` and then fails gives no reading, so the first `iconv`'s exit is pinned beside the round trip (#858)

Fix round 1 on rv858's review of #867, its last step. With the round trip
in `locale_reading`, dropping the first `iconv`'s `|| return 1` (F07)
survived the mutant table: a failing `iconv` that printed part of a
reading, or nothing, is refused by the round trip anyway. One that prints
the whole reading and then exits 1 is not: the round trip passes. A row
of `REFUSED_AS_THE_BYTES_SAY` now has such an `iconv` (`READS_THEN_FAILS`,
this machine's run in full, then `exit 1`) read a hanzi of the C1 bytes
under GB18030, through both files: refused as a control character, as
the bytes say, and install.sh before any request.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…O` that ends in a newline is refused too, and the order of the up-front refusals is pinned (#866)

Fix round 1 on #872, after rv866's review.

SHOULD-1: `checked_repo` looked at `url="$(shape_clone_url)"`, and `$(…)`
drops the newlines at the end of what it captures. So a URL, an scp
address or a path ending in a newline passed the check. `--version` then
printed it, and a scaffold run sent it in both requests. An owner/name
keeps the newline inside its URL, before `.git`, so it was refused
already. The misdrawn characters are now looked for in `$REPO` as typed,
and the case reads `"$url$REPO"`: the leading `-` is still looked for in
the URL's first character, so `-acme/r`, which becomes
`https://github.com/-acme/r.git`, still passes, and a control character
anywhere in either is refused. The words are unchanged.

NIT-1: a new test holds the order `refuse_bad_values` states. With a broken
record and a bad repository, the record is refused. With a bad repository
and a bad ref, the repository is refused.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t (`a.lock/b`, `a/.b`, `a//b`, `a/b/`, `a.`) is refused by each of them before the clone, the fetch or the record, and each refusal states git's rule (#874)

Each of the three kept the whole-name rule `*..* | *.lock`, which git does
not stop at: it asks each part between slashes. `a.lock/b`, `a/.b`,
`a//b`, `a/b/` and `a.` passed all three, so `setup.sh --shape-ref a.lock/b`
printed `(0) self-bootstrap`, cloned the standard and ended in git's
`pathspec 'a.lock/b' did not match`, `install.sh a.lock/b` asked the
network for `.../a.lock/b/openRepoShape`, and `openRepoShape --install`
with the opt-in wrote `a.lock/b` to the record every later fetch reads.

Each check gates something real (setup.sh: the clone, on the self-bootstrap
path only; install.sh: the fetch, the version being the URL's path;
openRepoShape: the record an install writes and a run reads, and the
doctor's checkout), so each holds git's rule itself instead of leaving it
to a later setup-project.py run. The same nine patterns in all three, in
plain `case` patterns bash 3.2 reads, inside the character set each had:
a part after a `/` that starts with `.`, a part before a `/` that ends in
`.lock`, an empty part, a `/` at the end, and a name that ends in `.`, with
`..` and the `.lock` ending as before. Over every name of up to five parts
from `a`, `0`, `.`, `/`, `-` and `.lock` that the character set takes (3110),
the old line disagreed with `git check-ref-format --branch` on 1122 and the
new one disagrees on none (git 2.43.0).

Each refusal's sentence now states the whole rule, in setup-project.py's
words where they fit (#857's `why_not_a_ref`), and setup.sh's comment says
it mirrors `checked_ref`, which asks `REF_RE` and then `why_not_a_ref`.

Tests: one table of names in tests/conftest.py (the issue's, rv873's, and
the refs that must still pass), fed to each copy and to git, which must
answer alike, one test in each of test_setup_sh.py, test_install_sh.py and
test_openreposhape_command.py. All offline; git is asked, never copied.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…able tests of #874's ref rule run under a `gh` and a `curl` that log and fail, and a refused ref asks neither, so the real network stays out of reach in the three of them as it did in install.sh's (#874)

The install.sh table test already ran under that file's fakes, which log
every call and fail what they do not serve, and asserts that a refused
version logs none. The setup.sh and openRepoShape ones were offline only by
construction (a path nothing is at; `--install` fetching nothing), so a
change that made either reach for `gh` or `curl` would have passed.

Both now put `fake_github(tmp_path, (), log=...)` first on `$PATH`: a `gh`
that serves nothing and a `curl` that refuses, each appending its arguments
to a log. The setup.sh one asserts the log was never written; the launcher
one asserts that no `curl` ran and no `gh` but the `gh auth status` the
machine report of an install that placed the command asks, and that a
refused install asked for nothing at all. `install_at` takes an
`env_also` for it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… as 5c54af2) as the stacked base of #874

Lane: openreposhape-2 (openRepoShape-2)
… its tree a4b0a79 is #867's final head 0ccda48's, merged above

Lane: openreposhape-2 (openRepoShape-2)
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR updates setup.sh, install.sh, and openRepoShape to enforce Git's branch-ref rules part by part, with consistent explanatory refusals and pre-side-effect validation, and adds shared parameterized tests that compare all three copies against the Git executable.

Sequence diagram for Git-compatible ref validation before side effects

sequenceDiagram
    participant User
    participant Script as setup.sh_or_install.sh
    participant GitOrCurl as Git_clone_or_curl
    participant Record as openRepoShape_record

    User->>Script: provide ref
    Script->>Script: checked_ref_or_checked_version
    alt ref violates Git's part-by-part rules
        Script-->>User: refuse with Git rule
        Note over Script,GitOrCurl: No clone, fetch, or record
    else ref accepted
        alt setup.sh
            Script->>GitOrCurl: git clone
        else install.sh
            Script->>GitOrCurl: curl version URL
        else openRepoShape --install
            Script->>Record: write openRepoShape.ref
        end
    end
Loading

Flow diagram for the shared Git ref rule

flowchart TD
    A[Ref input] --> B{Matches any Git-invalid part pattern?}
    B -->|Yes| C[Refuse with explanatory rule]
    B -->|No| D[Continue to clone, fetch, or record]
    E[Invalid patterns] --> E1[Starts with . after /]
    E --> E2[Part ends in .lock]
    E --> E3[Empty part between slashes]
    E --> E4[Name ends in .]
    E --> E5[Contains ..]
    E1 --> B
    E2 --> B
    E3 --> B
    E4 --> B
    E5 --> B
Loading

File-Level Changes

Change Details Files
Align all three shell-side ref validators with Git's per-part branch-name rules and reject invalid refs before side effects.
  • Added matching Bash patterns for dot-prefixed parts, .lock-suffixed parts, empty or trailing slash parts, and names ending in ..
  • Updated refusal messages and setup documentation to explain the complete Git-derived rule.
  • Preserved existing character, length, control-character, repository, and opt-in recording behavior.
install.sh
setup.sh
openRepoShape
Added shared, Git-backed parameterized coverage for ref acceptance, refusal wording, and side-effect ordering.
  • Added a 33-entry ref table and helpers that compare each copy directly with git check-ref-format --branch.
  • Verified setup rejects before cloning, install rejects before curl, and launcher install rejects before recording.
  • Asserted the common rule sentence and retained coverage for repository/ref control-character handling and accepted fork names.
tests/conftest.py
tests/test_setup_sh.py
tests/test_install_sh.py
tests/test_openreposhape_command.py

Assessment against linked issues

Issue Objective Addressed Explanation
#874 Make setup.sh, install.sh, and openRepoShape reject refs Git refuses part by part before their respective clone, fetch, or record side effects. ✅
#874 Update each refusal message and setup.sh’s comment to describe the per-part ref rule accurately. ✅
#874 Add table-driven checks comparing all three shell copies with Git’s ref-format decision, and verify them under Git 2.43 and 2.47.1 alongside the requested Linux tests. ❌ The PR adds table-driven tests that compare each copy with the Git on PATH and reports the requested Linux test suites passing, but it reports running only Git 2.43.0—not Git 2.47.1 as the issue’s verification checklist requests.

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-18T05:38:08Z) Independent review of #875 (#874) at a1fa6768, for lane openRepoShape-2: READY

No MUST. The change does what the issue asks. In each of the three copies it refuses exactly what git check-ref-format --branch refuses inside the character set, with one exception, HEAD, where the copies are right (see below). Nothing that git takes is now refused. The sentence is the same in all three copies, and every run is green.

Next: lane 2's writer takes SHOULD 1 and NITs 3 to 7 in a fix round. SHOULD 2 and NIT 8 go to Brett Heap as candidates; nothing is filed without his word.

SHOULD

  1. tests/conftest.py:2359-2381 (REF_PARTS): three over-refusing mutants survive. No row has a part that starts with an upper-case letter, _ or -. Each of these mutants passed all 33 rows of its copy's table test:

    • setup.sh: add */[-_]*. It refuses fix/-wip and fix/_tmp, which git takes.
    • install.sh: add */[A-Z]*. It refuses feature/ABC-123.
    • openRepoShape: change [!A-Za-z0-9]* to [!a-z0-9]*. It refuses Feature/ABC-123.
    • Fix: add ("Feature/ABC-123", True), ("fix/_tmp", True), ("fix/-wip", True) to the "still pass" group. Measured with those three rows: the three tables pass at the head (108 passed), and all three mutants are killed (rows fix/_tmp and fix/-wip; Feature/ABC-123; Feature/ABC-123).
  2. src/openreposhape/standard.py:198-219: a follow-up, not a change to this PR.

    • The package's ref_fault keeps the old rule (:215) and the old sentence (:219).
    • Its docstring (:200) says it is "the launcher's ref_fault, in its order and its words". That stops being true when this PR lands.
    • Probe: a detached copy of the head's package, with OPENREPOSHAPE_REPO a local clone, running --doctor .. For each of a.lock/b, a/.b, a//b, a/b/ and a., GIT_TRACE shows 1 git clone into the cache. The run then ends REFUSED: cloned ... but could not check out 'a.lock/b': fatal: invalid reference: a.lock/b. topic.lock is refused before any clone.
    • Fix: its own issue, as the PR body says.

NIT

  1. tests/conftest.py:2366-2368: a comment contradicts its own docstring.

  2. setup.sh:215 and :220: two loose phrases in the comment.

  3. install.sh:161: "before any request" is not true when no version is named.

    • With no version, the releases/latest request (and the gh api fallback) runs before checked_version "$version" "the newest release's tag" (install.sh:589).
    • Fix: write "before the launcher is fetched".
  4. PR body, "Not changed": one sentence is not true for any ref the rule refuses.

  5. The first half of the shared sentence says "letters" where setup-project.py: a passed-through --dry-run makes none of the preflight's offers and --shape-ref is held to git's rule part by part, so a rehearsal no longer writes git's global config and a.lock/b is refused before the clone (#857) #873's REF_RE words say "ASCII letters" (setup-project.py: a refused project name gets one sentence that explains only a leading -, a trailing space passes where a leading one is refused and hides in step (4)'s report, and a name of 101 to 255 characters passes the prompt, to end at step (5) or at GitHub #777, rv811).

    • Under LC_ALL=C.utf8, --shape-ref Café is refused with "uses only letters, digits, ...", but é is a letter.
    • The words predate this PR, and Decision 5 keeps them.
    • Optional fix: "starts with an ASCII letter or digit, uses only ASCII letters, digits, ..." in all three copies and in REF_RULE_WORDS.
  6. openRepoShape:834 clones before :839 checked_ref in shape_checkout: a follow-up candidate, unchanged here.

    • Probe at the head: an installed copy running --doctor with OPENREPOSHAPE_REF set to a.lock/b, a/.b, a//b, a/b/, a. or topic.lock. GIT_TRACE shows 1 git clone and 0 checkouts, and then the new refusal.
    • Fix: move checked_ref above the clone, in a later issue.

Checked and true

Could not verify

  • git 2.47.1 and CI's git: only /usr/bin/git 2.43.0 was run. The tests ask the git that runs them.
  • That GitHub's contents API takes ref=HEAD: there was no network.
  • bash 3.2: not available here. The new patterns are plain *, . and / globs.
  • Windows and macOS: CI is Linux only (ci: run the test suite on Linux only #597).

Lane: openreposhape-2 (openRepoShape-2)

Brett Heap and others added 2 commits October 18, 2026 05:55
…le's sentence says ASCII letters as setup-project.py's does, three rows hold the patterns off a part that starts upper-case, `_` or `-`, and two comments say what is true (#874)

rv874's review of #875 (READY, no MUST), fix round 1.

The shared sentence said "a letter or digit" and "letters", which `Café`
satisfies under a UTF-8 locale although the patterns refuse it; #873's
REF_RE words say "an ASCII letter or digit" and "ASCII letters" (#777,
rv811). The three copies' refusals and `REF_RULE_WORDS` now say the same,
byte for byte, and so does setup.sh's comment.

`REF_PARTS` had no part that starts upper-case, `_` or `-`, so three
over-refusing mutants passed all 33 rows of their copy's table test:
`*/[-_]*` in setup.sh, `*/[A-Z]*` in install.sh and `[!a-z0-9]*` for
`[!A-Za-z0-9]*` in openRepoShape. `Feature/ABC-123`, `fix/_tmp` and
`fix/-wip` join the names that must pass, and each mutant is killed by them.
`a/b.` and `a/.` move up to the names the old line took, where the
docstring already counted them, from "refused before #874 as well".

setup.sh's comment names `why_not_a_ref` as #857's, held here since #874,
and says each rule has one or two patterns (an empty part and a `.lock` part
have two). install.sh's note says a refused version is refused before the
launcher is fetched: with no version named, `releases/latest` is asked first.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-18T06:13:13Z) Re-verification of #875 (#874)'s fix round 1 at 8d6c5bdb: READY

Every finding from the first review is fixed as asked, or left as a candidate for Brett Heap as agreed. All runs are green. The fix round's code and tests are right. There is one new SHOULD and one NIT, both in text outside the code.

SHOULD

  1. The squash subject drops the issue number.

NIT

  1. tests/conftest.py:2366-2367: one comment's lead-in is loose.
    • The comment reads "The same kind, and taken by the old line too". It follows rv873's group, "a part that starts with .". a/. is of that kind, but a/b. is not.
    • Fix: say "Of the issue's kind", as the PR body's list does.
    • Lane 2 leaves it on record. It is a comment in a test, and a new commit would need a new gate.

Checked and true

Could not verify

  • git 2.47.1 and CI's git: only /usr/bin/git 2.43.0 was run.
  • bash 3.2, Windows and macOS: none are available here.

Lane: openreposhape-2 (openRepoShape-2)

Brett Heap and others added 3 commits October 18, 2026 06:13
…nded as c5d0bde) as the stacked base of #874

Lane: openreposhape-2 (openRepoShape-2)
… its tree 4156a29 is #872's final head a1cad96's, merged above

Lane: openreposhape-2 (openRepoShape-2)
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 09:47
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 5 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 33c5128 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/874-ref-parts branch October 18, 2026 09:48
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…and its round 2, and run to main 33c5128 (#883)

Round 2b of #883. It answers RV883's full NIT list on ede678c (lane 3,
2026-10-18T09:29:28Z) and RV883's round 2 on 86baec5 (#883
issuecomment-6248362246). Each finding was checked against the code at
main 33c5128 before the text changed:

- The #843 limit is two: what `adopt-work-dir-shared` leaves open (group
  write, Windows, root as the other user, this user's own processes, the
  default work directory), and what it now refuses that v1.1.0 used
  (another user's directory, a run as root included, a 0777 directory
  without the sticky bit, a drive mounted without ownership), with the
  way out. "What a script sees" names `adopt-work-dir-shared` and
  `adopt-work-dir-occupied`.
- The doctor's NEXT lines, not its `members` row, read no url from an
  unmerged `.gitmodules`; the `make siblings` limit names that row.
  `siblings.py` has no unmerged rule; the `validate.yml` templates do.
- `validate-family.py`'s `family ok` is scoped to `--no-members` and to a
  holder whose members are checked out.
- The wording points: `update-pr-no-repository` under `--pr`, family.py's
  `git rm -r --cached` line, `MSYSTEM` set and not empty, `..` and
  `naming-unclassified`, the keys git refuses with no value, the url the
  root's config keeps, `contracts: cannot be listed`, install.sh's newest
  tag and redirect location, the scan test's name,
  `update-rewrite-symlink`, `os.access` for root, `LC_ALL=C`,
  `update-validators-red`, `heads/main` in both keys, where
  `plan written to` came, junctions on Python 3.12, the approval click and
  the run's approvals, and the back-port route's one run.
- #457's L1-17 is narrowed by #825; #853 and #854 join #834 as test
  limits.

The refresh past c5359df adds #870 (#864), #876 (#839), #791 (#787),
#859 (#838), #780 (#776), #873 (#857) and #875 (#874): 57 pull requests.
It drops the limits for #776, #839 and #874's ref rule, rewrites the #838
clauses, and adds the limits those squashes name (#863, #879, #884, an
older git without `GIT_NO_LAZY_FETCH`, family.py's link guard). The open
list is the 14 that the 09:42Z milestone read leaves after #875: #675,
#707, #739, #755, #764, #834, #840, #853, #854, #856, #863, #878, #879
and #884.

The six-and-six shape-copy lists stand: from c5359df to 33c5128 the
templates and repo_shape.py changed only in the family root's
bootstrap.py and validate-family.py, both listed. No other file changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant