diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22d09b35..2c251ab4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,9 +71,10 @@ name: release # gate, and is then back-ported to `release/v`: by a fast-forward # to main while main holds only the patch's work, and otherwise by a # pull request to `release/v` of `git cherry-pick -x `, -# the squash commit main landed. The fast-forward stands unless a -# `release/*` ruleset requires a pull request (that ruleset waits on -# Brett Heap's word, #850); then every back-port is the cherry-pick; +# the squash commit main landed. The fast-forward stands: ruleset +# 25325536, made on Brett Heap's word "go with option A for #850" +# (2026-10-18T01:05:28Z), refuses a force-push to a `release/*` branch +# and its deletion, and requires no pull request; # - a push runs the tests.yml its own commit holds, so a release branch # runs it only once it holds #850's change: `release/v1.1` from its # first fast-forward past it, or a cherry-pick of it; `release/v1.0` diff --git a/docs/release-notes/v1.1.1.md b/docs/release-notes/v1.1.1.md new file mode 100644 index 00000000..b42a7ada --- /dev/null +++ b/docs/release-notes/v1.1.1.md @@ -0,0 +1,1272 @@ +# openRepoShape v1.1.1 + + + +v1.1.1 is a patch of v1.1.0. It adds no command and no flag. It fixes text +that a terminal acts on reaching it, names that are not ASCII or not UTF-8, +`.gitmodules` files read otherwise than git reads them, and adoptions and +scaffolds that failed late, after the plan was built or a repository was +made. It changes pinned shape copies, so a project or a family holder +takes those fixes into its own gates by `update-shape.py` (below). It is +cut from `release/v1.1` (#850). + +**[placeholder: the counts at tag time.]** At main `0d5d2b1`, on +2026-10-18, 67 pull requests had landed on main since v1.1.0's commit, +`190133d`. At 11:31Z, 69 of the `v1.1.1` milestone's 72 issues were closed, +#709 among them, closed as not planned. + +## What 1.1.1 fixes + +- **Text a terminal acts on stays off it.** `install.sh` and the bash + `openRepoShape`, the one `install.sh` places, refuse a + `$OPENREPOSHAPE_BIN_DIR` holding a control character, a C1 control + included, or one of 41 code points a terminal may draw as something + other than itself (a bidi control, a character of no width, a soft + hyphen, U+2028, U+2029), before any request and without echoing it + (#760, #837); where the bytes refuse a name, they read it again in a + multibyte locale's own charset, so that locale's letters are placed + (#858). The bash command checks `$OPENREPOSHAPE_REPO` and + `$OPENREPOSHAPE_REF` the same way before `--version` answers (#866); the + pipx command does not (see the known limits). `validate-pins.py` spells + every name and path it prints (#712), and `family.py init`'s plan rows, + seven refusals and `bump-leg.py`'s refusals that name the root spell the + paths they print (#766). +- **Names that are not ASCII, or not UTF-8.** The doctor keeps such a + character where the report can show it, so a NEXT or keep line naming a + path such as `café` runs as printed, and escapes a command's value whole + where it cannot (#785); it finds a member whose `.gitmodules` section name + is not UTF-8 (#732). `make bootstrap` moves back a leg's tracking branch + whose upstream is not UTF-8 (#710); `update-shape.py`, `family.py` and + `bump-leg.py` read the branch HEAD is on as git's bytes (#765); + `setup-project.py` and a project's `bootstrap.py` tell each Python child + the encoding it is read in (#836); and a byte-order mark no longer breaks + a YAML file or the upstream notice (#855). +- **`.gitmodules`, read as git reads it.** The doctor and both + `validate.yml` templates read only the sections, keys and values git + reads, so the doctor's NEXT lines name no url, and CI no App-token + repository, that git would not (#733, #741, #786); `make siblings` and + the doctor's `members` row read the same sections, keys and values, but + still read an unmerged file (#733). A holder's `make bootstrap` and its + `validate-family.py` name a value that git dies on, where bootstrap said + `family bootstrap ok` (#768), and bootstrap reads an index or HEAD copy + with a line git cannot parse up to that line, as git does (#856). +- **`adopt-project.py` asks before it makes anything.** `--source`, `--out`, + `--pin-source`, the source's default branch and both leg repositories are + asked about before any leg exists, and refused there (#796, #799, #800, + #797, #798). `check` holds a plan to the repository it records (#716), + the line-ending block governs a copy written through a symlinked + directory (#807), the recorded answers cover a directory split by hand + (#801), and the clone `plan` and `check` make is removed only while it is + still the one they made (#803). +- **One reading of `legs:`, `members:` and a branch name.** A `legs:` that + is not a list, or an entry that is not a mapping, is said in one sentence + by the naming gate, `validate-manifest.py`, `validate-pins.py`, the + doctor, a project's `bootstrap.py`, `bump-leg.py` and + `update-shape.py --pr` (#714, #832). A holder's `members:` that is not a + list, or an entry that is not a mapping, is said in its + `validate-family.py`'s words by the doctor, `family.py` and the holder's + `make bootstrap` and `make siblings` (#864). The YAML reader keeps + Unicode spaces as text, and a branch name holding a character that does + not print or is white space is refused (#715). +- **Git Bash gets `sh`'s quoting.** Under Git Bash, the lines the tools + quote for the host are quoted for `sh`, where they were quoted for + PowerShell (#711). +- **Names GitHub cannot hold.** `scaffold-project.py`, + `adopt-project.py execute`, `family.py init` and setup's plan refuse a + repository name over 100 characters, an id whose `xf-project-` topic + is over 50, and an `--org` over 39 or one GitHub gives no organisation, + before any repository is made, where an id over 39 failed after the + repositories existed; `adopt-project.py check` finds each of them as + `execute` refuses it (#838, #863). A project name typed at + setup's prompt is checked as `--project` is, a refusal names the check + the value failed, and a name too long for GitHub's repository names is + refused before step (4) (#729, #777). +- **setup-project.py's rehearsal, and a ref git refuses.** A run passed + `-- --dry-run` makes none of the preflight's offers, so it no longer + writes git's global config or signs `gh` in, and `--shape-ref` is held to + git's rule part by part (#857), as is a ref given to `setup.sh` or + `install.sh`, and one the bash `openRepoShape` records, reads from its + record or checks out for `--doctor` (#874). +- **A project's `make bootstrap`.** A validator's refusal ends the run + `bootstrap REFUSED`, exit 2, where it read as a finding; an upstream + path, cache or pin it cannot read is no answer, where the run ended in a + traceback; and a cherry-pick or a revert stopped in a leg whose refs are + a reftable is left alone, where bootstrap ended it (#764). +- **The doctor.** Every git it starts runs with `GIT_NO_LAZY_FETCH=1`, so + on a git that reads the variable a read a partial clone cannot answer + fails rather than fetches (#787). In a root that lost + `scripts/repo_shape.py`, the `legs` and `members` rows' NEXT puts the file + back, and the `agent-files` row's WHY puts it back before it names a gate, + where each named a gate that dies on the import (#750, #776). Outside a + shape root, a naming policy that does not load in the doctor's own + checkout is an `n/a` row on `the way in`, not a finding or a traceback + (#707). +- **Family holders.** More of `family.py add`'s and `validate-family.py`'s + remedies type as printed in the states they name, for a member under + another spelling (#761, #762, #839, #840) and for a gitlink outside + `members/` (#878); a holder's `make bootstrap` no longer prints the + `SHAPE_LEGS_TOKEN` advice where git fetched nothing, and tells a + member's finding, its refusal and make's own failure apart, through a + member's own nested make too (#839, #884); `add`, `bump` and `remove` + see a cherry-pick or a revert stopped in a reftable holder (#684); and + `make siblings` reports a slot that is not a clone, a link to nothing + among them and, on Python 3.12 and later, a junction to nothing, and + places the members after it (#735, #784, #793). +- **CI on a release branch.** `tests.yml` runs on a push to + `release/v` as on a push to main, and `release.yml`'s header says + how a fix is back-ported (#852, for #850). + +## Install and upgrade + +- **The newest release, by the one line:** + + ```sh + curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bash + ``` + + It places the bash `openRepoShape`. The same line again upgrades. + `… | bash -s -- v1.1.1` names this release. `openRepoShape --version` + prints `openRepoShape (opensoft/openRepoShape @ v1.1.1)`. +- **From PyPI:** `pipx install openreposhape==1.1.1` for this release, and + `pipx upgrade openreposhape` to upgrade one already installed. + `openRepoShape --version` prints + `openRepoShape 1.1.1 (opensoft/openRepoShape @ )`. + [v1.1.0's notes](v1.1.0.md#install-and-upgrade) say where `pipx` comes + from, and that the two installers do not replace each other's command. +- **A project's and a holder's shape copies.** A copied file reaches no + project by itself. v1.1.1 changes six of the copies an assembly root + pins: `scripts/validate-pins.py`, `scripts/validate-manifest.py`, + `scripts/bootstrap.py`, `.github/workflows/validate.yml`, + `scripts/repo_shape.py` and `scripts/validate-repository-naming.py`. It + changes six of the copies a family holder pins: + `scripts/validate-family.py`, `scripts/bootstrap.py`, + `scripts/siblings.py`, `.github/workflows/validate.yml`, + `AGENTS-shape.md` and `scripts/repo_shape.py`. From a checkout at the + v1.1.1 release commit, `update-shape.py check --root ` reports each + as `upstream-changed` in a root pinned at v1.1.0 that has not edited it, + and + `update-shape.py apply --root --at --yes --branch shape/update-` + re-copies and re-pins them, on the person's yes, as AGENTS.md's + "Updating a project's shape" says. Until then the root's own gates and + `make bootstrap` are v1.1.0's. +- **The rendered READMEs.** A project's or holder's `README.md` is rendered + once, when it is made, and `update-shape.py apply` rewrites only a + holder's closing `Shape:` line. So the new sentence on a `.gitmodules` + that git refuses, in the root's README and in the holder's (#790), and + the holder's new sentences on the one spelling of `members/` and on its + folder's name (#738, #822), reach new roots only. An existing holder + reads the last two in its re-pinned `AGENTS-shape.md`; #790's sentence + reaches no existing project or holder at all. +- **What a script sees.** Runs that passed on v1.1.0 and fail now, among + them: + - a holder's `make bootstrap` over a `.gitmodules` value that git dies + on, exit 1, where it said `family bootstrap ok`; and + `validate-family.py --no-members`, or any mode in a holder whose members + are checked out, over the same value, over a member's staged removal, + or over a section with no url, exit 1, where it said `family ok` (in a + plain clone, v1.1.0's members modes refused `member-uninitialized`, + exit 2) (#819, #768; #820, #762); + - the same `validate-family.py` runs over a gitlink outside `members/` + that no section of `.gitmodules` gives, `gitlink-unnamed`, exit 1, + where they said `family ok` (#882, #878); + - `family.py add`, `bump` and `remove`, and a holder's `make bootstrap` + and `make siblings`, over a `members:` that is not a list, or holds an + entry that is not a mapping, exit 2, where some such values passed, + `make bootstrap` saying `family bootstrap ok`; and + `validate-family.py --no-members` over such an entry, exit 1, where it + said `family ok` (#870, #864); + - `family.py add` beside a section of `.gitmodules` that gives a case + variant of a row's path, or beside a case variant of a row's path + under `members/`, `family-members-dir-variant`, exit 2, where it went + on, and committed such a section beside the new member (#865, #840); + - where a GitHub repository will be made, `scaffold-project.py --dry-run` + and `family.py init --dry-run` over a name over 100 characters, an id + whose `xf-project-` topic is over 50, an `--org` over 39 or one + GitHub gives no organisation (`.`, a trailing hyphen, `--`), and + setup's step (5) over such an id or `--org`, exit 2; setup refuses a + name over 95 characters before step (4) (#859, #838; #871, #863; #811, + #777); + - a project's own gates over a branch name that does not print or is + white space, such as `main`, once its copies are re-pinned + (#823, #715); + - CI over a `.gitmodules` that git refuses, whatever the credential, + where with no credential the run went green (#790, #786); + - `install.sh` and the bash `openRepoShape --install` over a + `$OPENREPOSHAPE_BIN_DIR` holding a control character or a character + drawn as another, exit 2, where the command was placed (#804, #760; + #841, #837), and the bash `openRepoShape --version` over such a + `$OPENREPOSHAPE_REPO` or `$OPENREPOSHAPE_REF`, exit 2 (#872, #866); + and the bash `openRepoShape --install` with + `OPENREPOSHAPE_RECORD_REF=1` over a ref such as `a.lock/b`, exit 2, + where it recorded it (#875, #874); + - `adopt-project.py check` over a plan it now finds a fault in, such as + `plan-pin-source-not-a-clone` (#842, #800), `plan-not-the-source` for a + plan whose source names no repository (#825, #716), a leg repository + that already exists (#861, #798), or a name, an `org:` or a topic + `execute` refuses (#871, #863); + - `adopt-project.py` given a `--work-dir` that is, or is under, a + directory every user can write in without the sticky bit, or one a user + other than this one or root owns, or whose `source-readonly` is such a + directory, `adopt-work-dir-shared`, exit 2; or one whose + `source-readonly` is a symlink, or already holds the clone's directory, + even empty, `adopt-work-dir-occupied`, exit 2 (#843, #803); + - `update-shape.py apply --branch` run from a branch whose name is not + UTF-8, exit 2 (#849, #765). + + `adopt-project.py plan` prints `plan written to `, after a blank + line, as soon as the plan is on disk, before its report, where it came + after the table, the totals, the follow-ups and any WARNING or NOTE, just + before the questions. It refuses `--out /dev/null` (#833, #799). A + project's `bootstrap.py` run by itself, as setup's step (7) runs it, ends + a validator's refusal `bootstrap REFUSED: (exit 2)`, exit 2, where + it said `bootstrap FAILED` and exited 1; `make bootstrap` exits 2 either + way, now with make's `Error 2` (#815, #764). +- **A project's pin** on the standard works as before. Follow + [How to pin 1.0.0](v1.0.0.md#how-to-pin-100) with the v1.1.1 release + commit. A scaffold run through the package pins that commit and digest by + itself. + +## What changed since v1.1.0 + +v1.1.0 was tagged on `190133d` on 2026-10-17. Everything below landed on +main after that commit, by area; `release/v1.1`, which is fast-forwarded to +main after each landing, holds the same commits. The list runs to the +commit v1.1.1 is tagged on. + +### adopt-project.py + +- **`--source`, a repository's top level.** A `--source`, or a plan's + recorded `source.local_path`, that is a file, a directory inside a + repository or a directory inside a git directory is refused as + `source-not-top-level`, naming what was given, before git reads anything + else in it; the way out is the top level, which + `git rev-parse --show-toplevel` prints, or its `org/repo`. So a file no + longer ends in a traceback, and a directory inside a repository no longer + plans that subtree alone under the repository's name. A directory in no + repository is still `git-failed`, in git's own words. An empty value and + an unknown `~user` are `source-unresolvable`, `owner/.git` is refused + with no `gh` call, `www.github.com` is read as `github.com`, and a source + that lost a blob is `git-failed`, naming the blob and `git fsck` (#831, + #796). +- **`--out`, asked first.** `plan` asks whether it can write `--out` before + it reads or clones the source or makes a work directory, and refuses a + folder, a pipe or a device, a missing folder or one nobody can write in + as `adopt-out-unwritable`, where a mistyped `--out` ended in a traceback + after the whole plan was built. `--out /dev/null`, which exited 0 and + threw the plan away, is refused too. `plan written to ` is printed, + after a blank line, as soon as the plan is on disk, before its report, + where it came after the table, the totals, the follow-ups and any WARNING + or NOTE, just before the questions (#833, #799). +- **`--pin-source`, a clone's top level.** `plan` and `scaffold-project.py`, + `--dry-run` alike, refuse a `--pin-source` that is not a clone's top + level, or a bare clone's, as `pin-source-not-a-clone`, naming the top + level git finds. `check` finds such a path in a plan as + `plan-pin-source-not-a-clone`, before a partial clone's NOTE, and + `execute` refuses it before any leg. So a folder inside a clone no longer + records the digest of part of its tree, or of nothing. What a top level + digests is unchanged, so existing pins stay valid (#842, #800). +- **The source's default branch, by its full name.** `plan`, `check` and + `execute` read `origin/HEAD` and HEAD by full name and whole, and read the + source only at a branch of its own, by `refs/heads/`. A + `git clone -b` source, whose `origin/HEAD` names a branch with no local + copy, is read at `--tracking-branch`, or refused + `source-default-branch-not-local` with ways through that work, where it + was `git-failed` with no remedy. A HEAD on `main`, or on a local + branch named `origin/x`, no longer adopts another branch's commit; an + `origin/HEAD` naming `main` beside a tag `main` no longer adopts the + tag's commit; and a HEAD beside such a tag no longer records `heads/main` + as `default_branch` and as `tracking_branch`, which `check` then refused + as `plan-tracking-branch-missing`. A default branch outside the + alphabet, such as `café`, is still refused, and the refusal now says the + way past it (#830, #797). +- **Both leg repositories, asked about first.** `execute` asks about both + leg repositories, under `--local-remote-dir` or on GitHub with + `gh repo view`, before it makes either, and refuses everything in their + way at once, saying that no leg repository was created. A code leg that + already exists no longer leaves the spec leg made. `check` asks the same, + so a `--local-remote-dir` that is a file or cannot be written in is a + finding, where `execute` ended in a traceback after `plan ok`. A `gh` that + cannot answer is `adopt-remote-unreadable` in `execute`, and a NOTE in + `check`, which stays usable offline (#861, #798). +- **The clone `plan` and `check` make, held to what the run made.** The + clone's directory is claimed with the run's own `mkdir` before `gh` + runs, and removed only while it is still that directory, never through a + symlink, so a `source-readonly` swapped for a link or for another + directory no longer turns the removal onto that directory, and two runs + in one `--work-dir` no longer remove each other's clone. A `--work-dir` + that another user can rename in, or rename from above, is refused before + anything is made in it, as `adopt-work-dir-shared`; the sticky bit + counts, so `/tmp` stays open, and neither Windows nor the default work + directory is asked. A link or a file at `source-readonly` is + `adopt-work-dir-occupied`, where a file ended in `FileExistsError`, and + so is a clone's directory already there, even empty; a skipped removal + says so in a NOTE (#843, #803). +- **A plan held to its recorded repository.** `check` prints the + rename-first remedy for a plan in the source's own name that the naming + policy refuses, as `execute` does. Where the live source names no + repository, a plan is held to its own recorded `source: repository:`, so + a plan for `acme/Atlas` edited to `Atlsa`, with `origin` then removed from + the source, is `plan-not-the-source` in `check` and `execute` alike, where + `check` said `plan ok` (#825, #716). +- **The line-ending block, by the path git tracks.** #471's block in a + split's `.gitattributes` names each pinned copy by the path git tracks it + at, and again by its pinned path where the two differ, so a copy written + through a root-kept `.github -> ci` no longer checks out with CRLF under + `* text=auto eol=crlf` and fails `validate-pins.py` after + `adoption verified`. `check` reports a nested `ci/.gitattributes` that + outranks the root's rule as `plan-nested-attributes`, and passes such a + source under `core.safecrlf=true` (#844, #807). +- **Answers recorded for a hand split.** `execute`'s `answered_paths:` also + records each plan entry that the doctor's walk of the leg it lands in + would put in another leg, or leave as a question, so a directory split by + hand out of an extension-majority fold no longer reads MISPLACED on the + doctor's first run after `adoption verified`. A plan left as `plan` wrote + it gets v1.1.0's record, byte for byte (#829, #801). +- **GitHub's limits, before either leg.** Where the legs go to GitHub, + `execute` refuses, before its confirmation and before either leg, a leg + name over GitHub's 100 characters as `adopt-name-too-long`, which named + only the disk's limit, an `org:` over 39 as `adopt-org-too-long`, and an + `id:` whose `xf-project-` topic is over GitHub's 50 as + `adopt-topic-too-long`, where an `--id` of 40 characters pushed both legs + and opened the pull request, then could not set the topic. A leg named + after a source of 96 to 100 characters is told to rename the source on + GitHub first, to at most 95 characters, and plan again. `plan --help` + states the bounds of `--project`, `--id` and `--org` (#859, #838). +- **`check` finds what `execute` refuses.** `check` asks the names, the + organisation and the topic from the same list `execute` asks + (`_what_cannot_be_held`), and finds each as a FINDING under `execute`'s + code and with its remediation, where it said `plan ok` over a plan + `execute` refused; `execute` still refuses the first before either leg. + Where the legs go to GitHub, an `org:` GitHub gives no organisation, one + holding a character other than an ASCII letter, a digit, `_` or `-`, one + ending in a hyphen, or one with two hyphens in a row, is + `adopt-bad-org` (#871, #863). + +### update-shape.py, bump-leg.py and a project's own scripts + +- **The pins gate, on what it cannot read.** A project's `validate-pins.py` + refuses by name a pin, the manifest, a shape copy or a leg checkout that + it cannot reach or open, named relative to the root, once, with advice + for the cause (`chmod u+r` for a permission, another sentence otherwise), + where it ended in a `PermissionError` traceback, or on Python 3.14 said a + present file did not exist. A copy it cannot open is + `shape-copy-unreadable`, and a leg checkout it cannot search + `pin-submodule-unreadable`. Every name and path it prints is spelled as + `repr` spells a character that does not print, so a newline, a bidi + control or an ESC in a workflow name or a pin entry no longer reaches the + terminal raw, and `--pin-source openss=` no longer answers for an `openß` + pin (#827, #712). +- **A `legs:` that is not a list.** In the naming gate, + `validate-manifest.py` and `validate-pins.py`, a `legs:` that is not a + list is the finding `manifest-legs`, and an entry that is not a mapping + `manifest-leg`, exit 1, where `legs: 5` ended in a `TypeError` and a + scalar was walked a letter at a time. Under `--project` the naming gate + never reads stdin, so no `legs:` can make it wait, and `--explain` no + longer ends in a `UnicodeEncodeError` on a name that is not UTF-8. A + chain link or a declared pin that is not a name is a finding (#826, + #714). The doctor and a project's `bootstrap.py` say the same sentences; + `bump-leg.py` refuses them as `bump-leg-manifest-legs`, exit 2, and + `update-shape.py` says them only under `--pr`, which refuses them as + `update-pr-no-repository`, where each ended in a `TypeError` or read the + project as one with no legs (#862, #832). +- **The YAML reader, and a branch name that does not print.** + `repo_shape.py`'s reader strips only YAML's space and tab, so a value + that a Unicode space opens reads as PyYAML reads it. `- :`, `: a`, + `{?: 1}` and a quoted value followed by a Unicode space are refused, and + `[?: 1]`, refused before as a single pair, is refused as an explicit key. + `validate-manifest.py`, `make bootstrap`, `bump-leg.py` and + `update-shape.py` refuse a branch name holding a character that does not + print or is white space, such as `main`, which the reader used to + read as `main`. `bump-leg.py` refuses it as `unseen-branch` before any + fetch or write. `update-shape.py apply` copies in v1.1.1's + `validate-manifest.py`, so it refuses `update-validators-red` + (`manifest-tracking-branch`) after its writes, before `--branch` reads + the branch, and rolls them back (#823, #715). +- **A byte-order mark.** A YAML file, or a `.shape-upstream-tip`, that a + Windows editor saved with a byte-order mark reads as the text it holds, + in every reader, the doctor's included: a marked `project.yaml` is no + longer `schema_version is None`, and the upstream notice names the tip + (#868, #855). +- **`make bootstrap` and a branch that is not UTF-8.** Step (a) decodes + git's output as the system decodes a name it is given, so a tracking + branch whose upstream is not UTF-8 is moved back, where it ended in a + `UnicodeDecodeError`, and a good branch this system's encoding cannot + write gets a line of its own, `did not try branch`, where it ended in a + traceback (#824, #710). +- **`make bootstrap` tells the validators their encoding.** A project's + `bootstrap.py` starts each validator with the encoding its words are read + in, so `make bootstrap` passes a project whose `name:` the Windows code + page cannot hold, where `validate-manifest.py` ended in a + `UnicodeEncodeError` (#851, #836). +- **`make bootstrap`'s ending, its notice and a reftable leg.** A + validator's exit 2 ends the run `bootstrap REFUSED: (exit 2)`, + exit 2, where it said `bootstrap FAILED` and exited 1, a finding's code, + as a plain clone's `pin-submodule-uninitialized` did; any other exit is a + finding, as before, and where one validator finds and another refuses, + both lines are said, the finding first, and the run exits 2. A + `SHAPE_UPSTREAM_PATH` nobody may enter, a `.shape-upstream-tip` nobody + may read or that is not UTF-8, a `contracts/` nobody may enter and a + shape pin that is no mapping are no answer to the upstream notice, where + each ended the run in a traceback. A cherry-pick or a revert stopped in a + leg whose refs are a reftable is read from its ref store and left alone, + `nothing moved`, where bootstrap ended it and carried its staged change + onto the branch. `docs/cli.md` says bootstrap places a leg at its pin + only where that loses no commit (#815, #764). +- **update-shape.py's `git show` lines and its kept-copy readers.** The + `git show :` a remedy prints is shell-quoted, so a source + path with a blank or a `'` pastes, and `update-add-would-overwrite` names + the run's target where it printed ``. The missing-name refusal + says the validators pass only where none of them meets the line. The + readers skip an `if TYPE_CHECKING:`, `if False:`, `if 0:` or `if None:` + body and a statement after an unconditional `raise`, as Python does, and + read an import in a `try`'s `else` or `finally` as taken (#802, #675). +- **The branch HEAD is on, as git's bytes.** `update-shape.py`, `family.py` + and `bump-leg.py` read the branch HEAD is on as the bytes git wrote, so a + branch ending in U+00A0, U+2028 or U+0085 is no longer read as its + neighbour: `family.py remove` no longer prints a push line for the + neighbour, and `update-shape.py apply` no longer reports a commit and a + push on it, nor switches HEAD to it in a rollback. A name that is not + UTF-8 is refused by name before anything is written, as + `update-branch-not-utf8`, `family-branch-not-utf8` or + `bump-leg-branch-not-utf8`, with a `git branch -m` remedy (#849, #765). +- **`validate.yml`, reading `.gitmodules`.** Both `validate.yml` templates, + the assembly root's and the family root's, read each submodule url by its + exact key, so a section named `members/My IRRS` gives the url git gives, + where it left that repository out of the App token and listed it wrong + (#782, #741). Their three url-reading steps read `.gitmodules` by git's + rules, as the doctor's NEXT lines do: a section git ignores, a section + with no `path` and an unmerged file name no repository, and a `path` or + `url` value beginning with `-` is passed over, that value alone, so an + earlier one stands. `siblings.py` reads by the same rules but for the + unmerged file, which it still reads. A file git refuses whole fails the + names step and the fetch step with an `::error::` naming the cause, + whatever credential is set, where with no credential the run went green + (#790, #786). +- **The workflow-reference note.** A project's `validate-pins.py` note + `: N workflow @ reference(s) agree with the gitlink` + counts only the references at the gitlink, says `N of M` where some are + not, and is not printed where none is, so no reference is both a + `pin-workflow-ref-mismatch` finding and one that agrees (#769, #739). + +### scaffold, setup, family and the shared readers + +- **Paths, spelled.** `family.py init`'s `folder` and `landing` plan rows, + seven refusals of `family.py` and `scaffold-project.py`, and the + `bump-leg.py` refusals that name the root, name each path as + `shown_path` does, so an ESC or a line break in a holder, a folder, a + lock, a local remote or a root no longer reaches the terminal from those + lines, nor prints a forged line there. `init`'s closing NEXT STEPS block + and the scaffold's `Working trees are at` line still print a path as it + is (see the known limits) (#814, #766). +- **Git Bash's quoting.** Under Git Bash (`MSYSTEM` set and not empty), + the lines the tools quote for the host are quoted for `sh`, not + PowerShell, through one question, `host_shell_dialect` in + `repo_shape.py`: the doctor's lines, setup's family lines and + `family.py`'s `NEXT`, `switch -c`, `add`, `cd` and + `member-path-taken`'s `git rm -r --cached` lines paste whole in the shell + that printed them, and the doctor gives Git Bash `cp` and + `env PYTHONPATH=...`, where it gave `Copy-Item` and `$env:PYTHONPATH` + (#821, #711). +- **setup-project.py's prompt.** A project name typed at the prompt is + checked as `--project` is, before step (3) builds a path from it, so + `../x`, `a/b`, `..` and `--help` are refused by name, exit 2, where + `../x` and `a/b` met an `--id` hint, `--help` a help page and `..` + `naming-unclassified` at step (4), and `../x` or an absolute name had a + probe folder made outside `--into`; an empty line says that no name was + typed (#756, #729). +- **setup-project.py's refusals and GitHub's limit.** A refused value is + told the first check it failed, in a sentence of its own (empty, a + leading `-`, a control character, or the pattern, in words and as the + pattern), where every refusal explained only a leading `-`. Where a + GitHub repository will be made, a project name over 95 characters is + refused before step (4), since `-spec` and `-code` add + five characters to GitHub's 100, and under `--local-remote-dir` it is + not asked. The prompt takes the spaces off a typed name, so `Atlas ` no + longer reaches step (4) with a space nobody can see (#811, #777). +- **setup-project.py's children.** Every Python child that setup starts is + told the encoding it is read in, so a cp1252 stdout gets one encoding + from setup, where a measured run printed 9 of its 129 stdout lines in + cp1252 among UTF-8 ones (#851, #836). +- **GitHub's limits in the scaffold and setup.** Where GitHub repositories + will be made, the scaffold's plan, `--dry-run` alike, refuses a name over + 100 characters (`scaffold-name-too-long`), an `--org` over 39 + (`scaffold-org-too-long`) and an id whose `xf-project-` topic is over + 50 (`scaffold-topic-too-long`, naming an id derived from `--project` as + derived), before any repository is made, where a `--project` of 40 + characters made three `gh repo create` calls and then failed at + `--add-topic`. `--help` states each bound. Setup's step (5) is that plan, + so setup refuses the same before step (6) asks for the yes; under + `--local-remote-dir` none is asked (#859, #838). An `--org` GitHub gives + no organisation is `scaffold-bad-org` (#871, #863). +- **family.py init and GitHub's limits.** `init` asks GitHub's limits on + the topic, the `--org` and the holder's name before any `gh` call, where + GitHub is asked, and `--dry-run` refuses the same: + `family-name-too-long`, `family-org-too-long`, `family-bad-org` and + `family-topic-too-long`. It set the topic last, after `gh repo create` + and the push, so a family of 40 characters made and pushed its holder, + then failed at the topic (#871, #863). +- **setup-project.py's rehearsal.** A run passed `-- --dry-run`, or + `-- --dry`, makes none of the preflight's three offers, an install, + `gh auth login` and `gh auth setup-git`: each is marked `not offered`, + and the plan's last line says a run without `--dry-run` makes them, + where a typed `yes` let `gh auth setup-git` write a credential helper into + the person's global git config, or `gh auth login` sign `gh` in. + `--preflight` still makes every offer, and a run with no terminal prints + what it printed. `--shape-ref` is held to git's rule part by part: no part + empty, none starting with `.` or ending in `.lock`, no `..` and no `.` at + the end, so `a.lock/b` is refused before the clone, where it cloned the + standard and then failed in git, exit 1 (#873, #857). +- **family.py in a reftable holder.** `add`, `bump` and `remove` refuse a + cherry-pick or a revert stopped in a holder whose refs are a reftable, + asking its ref store for `CHERRY_PICK_HEAD` and `REVERT_HEAD` by their + exact names, where they went on to their commit. With #816 in v1.1.0, + that closes #684 (#846, #684). +- **family.py add's move back.** The move back that `add` prints for a + member's gitlink under another spelling of its row's path points the + member's section of `.gitmodules` back too, sets aside first a file or a + link the index holds in its way, and puts back a member whose removal is + staged. Its closing `mv` is guarded, moves nothing where nothing is + there, never moves a checkout and leaves one to delete by hand; the + update comes last; and `--sparse` is printed where the sparse rules leave + an entry out, not only where its bit marks it. So, in the states it + names, no printed line leaves a section naming another spelling, or + un-registers a member (#828, #761). +- **family.py add, the states #840 names.** Where `add` refuses a member + recorded in another case, it names first, with a remedy that types as + printed: + - a section of `.gitmodules` that gives a case variant of a row's path + beside the member's gitlink: the section named after the row is + pointed back where no section gives the row's path, and any other is + taken out with the gate's own `--unset-all` lines; + - a second gitlink at such a variant, taken out of the index with the + gate's own `git rm -r --cached`, with `--sparse` and `-f` where git + needs them, its checkout named to delete by hand before the update; + - a case variant of a row's path under `members/` itself, where `add` + went on; + - a checkout inside a directory that a `git mv` or an `mv` moves, named + to delete by hand, where the move left it with a `.git` file that + points nowhere; + - a symbolic link the index holds entries under, moved out first; + - an unmerged entry at or under a row's path, set aside, where it kept + #574's remedy and the commit failed; + - a `.gitmodules` that is unmerged, deleted or left untracked, asked + first. + + Its closing guard takes `validate-family.py`'s form (#876), so a link at + the variant is moved, the link and never what it points at, where it was + kept (#865, #840). +- **validate-family.py's remedies.** The wrong-case move back mends + `.gitmodules`, adds `--sparse` where a sparse checkout needs it, and moves + aside first an entry in its way. A row's staged removal is a finding + before the commit that makes it, a section with no url is + `member-gitmodules-url-absent`, `member-uninitialized` names the gitlink + where git's init stops and the cure for it, and a pin whose commit the + member lacks names the fetch, or the `family.py bump`, that cures it + (#820, #762). +- **A `.gitmodules` value git dies on.** Where `.gitmodules` gives a value + that git dies on as it reads the file (a `path`, `url`, `update`, + `ignore` or `branch` key with no value, `update = None`, a `shallow` that + is no boolean, and more), a holder's `make bootstrap` fails, naming the + key, where it said `family bootstrap ok`, and `validate-family.py` + reports `gitmodules-value-refused` in every mode, with the line that + takes the key out. Where the work tree has no `.gitmodules`, bootstrap + reads the index's, then HEAD's, as git does (#819, #768). +- **A `.gitmodules` copy with a line git cannot parse.** Where the index's + or HEAD's `.gitmodules` holds such a line, a holder's `make bootstrap` + reads the sections before it, as git's submodule reader does, where it + read none and a run with no credential failed wrongly. A row the read + did not reach is told to put the file back, mend the line or the key it + names, and commit it: `git checkout --ignore-skip-worktree-bits -- + .gitmodules` for the index's copy, which keeps what was staged, and + `git checkout HEAD -- .gitmodules` for HEAD's. A parity test holds the + value rules that bootstrap and `validate-family.py` each write to one + answer (#877, #856). +- **`make siblings` and a slot that is not a clone.** A file, a folder that + `make siblings` cannot list or enter, or a link to nothing at + `../` is NOT A CLONE, reported and left as it is, and the members + after it are placed, where the run ended in a traceback, or the dry run + said `would clone` where the real run ended `CLONE FAILED`. On Python + 3.12 and later a junction to nothing there is NOT A CLONE too. `make park` + and `make resume` skip such a member as `SKIPPED no clone`, and the + doctor's `members` row calls each such slot `not-a-clone`, as + `make siblings` does (#772, #735; #788, #784; #794, #793). +- **A `members:` that is not a list.** A holder's `members:` that is not a + list, or that holds an entry that is not a mapping, is refused in its + `validate-family.py`'s words, `family-members-unreadable`, exit 2, by + `family.py add`, `bump` and `remove`, their dry runs included, before + they fetch, mount or rewrite anything, where they ended in a `TypeError`, + said `member-unknown` of a member mounted there, or committed a + `family.yaml` without the entry; and by the holder's `make bootstrap`, + `make siblings` and `family.py siblings`, where they ended in a + `TypeError`, read no members (`0 member(s)`, and `make bootstrap`'s + `family bootstrap ok`), or counted the rows beside the entry and said + nothing of it. `validate-family.py --no-members` finds such an entry, + `member-row`, exit 1, where it counted it as a member and said + `family ok`. An absent or null `members:`, or `[]`, is still a family + with no members; `0`, `false` and `''` are now refused (#870, #864). +- **A holder's `make bootstrap`, on what failed.** The `SHAPE_LEGS_TOKEN` + advice is no longer printed where git fetched nothing: where its init + stopped at a gitlink with no url, or `.gitmodules` was unmerged, gave a + value git dies on or could not be parsed. There it stood above "not for + want of a credential". Anywhere else a fetch failed it stands, and beside + a denial it is for the members the denial does not name. The closing + FAILED line takes the code from make's own `Error ` line, for a + member's `make bootstrap` and `make validate` alike, so a member's + finding reads `the member's command exited 1`, its refusal + `the member's command exited 2`, and make's own failure says make printed + no such line for a member's command, where all three read + `make bootstrap exited 2` (#876, #839). +- **A member's make, read a line at a time.** A holder's `make bootstrap` + reads each member's make on stderr a line at a time and names the + innermost command's code: a member whose `bootstrap` target runs another + make reads `the member's command exited 1` for an inner finding, where it + read `exited 2`, an inner make's own failure is said as make's own, and a + `make -k` run with more than 4 KiB on stderr after its `Error` line keeps + that line, where the run said make printed none. `run_make`'s docstring + says what passing make's stderr on changes for a member (#886, #884). +- **validate-family.py's lines, typed as printed.** A `git rm --cached` of + a staged gitlink whose checkout has moved on, or holds a file git does + not track, takes `-f`, which git otherwise refuses, in the finding for a + second path to a member and in the wrong-case move. The wrong-case move's + guarded `mv` moves nothing where nothing is at the variant, where it + exited 1, and moves a symbolic link there wherever it points, the link + and never what it points at. Where the work tree's `.gitmodules` is gone, + the first `member-gitmodules-url-absent` or `member-gitmodules-absent` + finding puts it back from where git reads it, and no later one does, so + the absent finding no longer makes a file holding its own section alone, + and a second finding no longer puts the file back over the url the first + wrote (#876, #839). +- **A gitlink outside `members/`.** `validate-family.py` asks about every + gitlink, not only those in `members/`. For a section at a gitlink outside + `members/`, `gitmodules-path-unnamed` prints that gitlink's + `git rm --cached` and its commit before the section's own lines, and a + gitlink there that no section gives is `gitlink-unnamed`, with + bootstrap's cure. Each such `git rm --cached` takes `-f` where git + refuses it without, and comes after `git add -- .gitmodules` where the + lines before it may leave that file unstaged. So the remedy typed as + printed no longer leaves a gitlink at which a plain clone's + `git submodule update --init` exits 128 while the gate passes (#882, + #878). + +### shape-doctor.py + +- **`.gitmodules`, as git reads it.** The doctor and `make siblings` read + only what `git submodule init` reads: a section whose name is empty or + holds a `..` component is skipped, a `path` or `url` beginning with `-` is + skipped, that key alone, a section with no `path` names no mount, and a + key with no value, or a value git dies on, gives no url where git refuses + the file. The doctor's NEXT lines also read no url from an unmerged + `.gitmodules`, which `make siblings`, and the doctor's own `members` row, + still read (see the known limits). So the doctor's NEXT lines name no url + where git says `No url found` or refuses the file (#775, #733). +- **Section names, as git's bytes.** A `.gitmodules` section name keeps + git's own bytes and is handed back to git as them, so, off Windows, the + doctor finds the url the root's config keeps and the kept git directory + of a member named `caf` and the byte 0xE9, gives its detached commit a + keep line under any locale, and no longer ends `--root` in a + `UnicodeEncodeError` traceback on a name that is not ASCII under + `LC_ALL=C PYTHONUTF8=0` (#774, #732). +- **Paths that are not ASCII.** The doctor keeps each character that is not + ASCII where the report's encoding can show it, where it printed every one + as `?`, so a NEXT or keep line naming a member `café` runs as printed. + Where the report cannot show a character, a command's value is escaped + whole (`$'...'` for `sh`, a `[char]` concatenation for PowerShell), and + other text, a WHY sentence's among it, still shows `?`. `--json` is + judged as UTF-8, whatever stdout holds, and written as ASCII, each such + character a `\u` escape its reader decodes (#792, #785). +- **Conflicted shape pins.** A conflicted shape pin's NEXT takes the side + whose pin reads, and names no line where no side's pin reads, so running + it no longer leaves the doctor naming the same NEXT again (#779, #752). A + shape copy renamed on one side of a merge, conflicted under a name no pin + lists, is named in the `why` and in `left_for_a_person` as renamed from + that copy, for a person to settle, and never taken (#781, #753). A copy + renamed on both sides, `DD` at its old name, is named gone on both sides, + with each side's new name where that side's own commit proves it, and + never as a merge of hunks git did not merge (#789, #783). +- **A partial clone of the standard.** The partial-clone row's NEXT fetches + by id each commit the clone lacks objects at, + `git -C fetch --refetch --no-filter ...`, from a + promisor remote whose branches hold it, one remote to a run, or from the + first promisor remote where none does, so it no longer loops where the + pinned commit sits only on another remote or another branch. Its `why` + says where each commit is, or that the clone does not hold it at all, and + from git 2.45 a commit the clone does not hold is no longer counted as + one object lacked (#773, #754). +- **A root that lost `repo_shape.py`.** The `legs` and `members` rows' + lockstep NEXT is the `git -C checkout HEAD -- scripts/repo_shape.py` + that the `pins` and `family` rows print, or none where HEAD no longer + holds the file, where it named the root's own gate, which dies on the + import (#771, #750). +- **A pin this user cannot read.** The `legs`, `manifest kinds` and + `shape currency` rows name a pin or a `contracts/` this user cannot read + in `validate-pins.py`'s words, `cannot be read (Permission denied)`, or + `contracts: cannot be listed (Permission denied)` for a folder it cannot + list, with its advice, and never `git checkout HEAD --`, which discards + what the file holds. The verdict is `INVALID`, not `DRIFTED` (#860, + #847). +- **A `legs:` that is not a list** (#862, #832), above: the `legs`, + `leg shape files` and `placement` rows are `n/a` and wait on the + `manifest` row, and `--placement-plan` refuses by name and writes + nothing. +- **A slot that is not a clone** (#788, #784; #794, #793), above: the + `members` row calls a link to nothing, or on Python 3.12 and later a + junction to nothing, `not-a-clone`, where it called the slot `absent`. +- **A `members:` that is not a list** (#870, #864), above: the `members` + row is `n/a` and waits on the `family` row, where `members: 5` ended the + whole report in a `TypeError` and other values read as `no members yet`, + or as members mounted at their pins beside an entry `5`. +- **No lazy fetch.** The doctor runs under `GIT_NO_LAZY_FETCH=1`, so every + git it starts, and every git that `update-shape.py`'s reads and the + validators it runs start, fails a read of an object a partial clone + lacks rather than fetching it. git 2.45 and later reads the variable, as + do the security releases of 2024-05-14 back to 2.39.4. No printed line + carries it, so a pasted fetch still fetches. Where a pin that does not + read is put back from history a partial clone of the root lacks, the + `shape currency` and `manifest kinds` rows' NEXT is + `git -C fetch --refetch --no-filter `, and + their `why` spells the same fetch from each other promisor remote, where + the doctor fetched as it walked the pin's history (#791, #787). +- **The `agent-files` row's gate.** The `agent-files` row's WHY for a + deleted pinned copy ends by naming the root kind's own gate to confirm the + digest, `validate-pins.py` in an assembly root and `validate-family.py` in + a holder, which has no `validate-pins.py`, or the standard's copy of it + where the root has none. In a root that lost `scripts/repo_shape.py` it + puts that file back first, or, where HEAD no longer holds it, says why no + command does. It named the root's own `validate-pins.py` everywhere, which + exits 1 with `ModuleNotFoundError` in such a root. In an assembly root + that keeps its gate and `repo_shape.py` the WHY is unchanged (#780, + #776). +- **`the way in`, beside a policy that does not load.** In a directory that + is not a shape root, a fault in the doctor's own checkout's + `contracts/repository-naming.yaml` makes `the way in`, and in a + repository `the other way in`, an `n/a` environment row: it quotes the + fault, says the file is that checkout's and not this directory's, and + prints no NEXT. A tab line gave `FINDING the way in refused`, and a + pattern that does not compile ended the doctor in a traceback, exit 1. A + policy that loads and then raises when asked "cannot be used". A leg + clone keeps its leg row and its Triad row over such a pattern, where + `scripts/shape_advisory.py` read it as no leg and the doctor offered it + `the other way in` and the scaffold. The verdict stays NOT A SHAPE ROOT, + exit 2. The doctor's EXIT CODES list says what INVALID and + `manifest-kinds` name (#778, #707). + +### Install, CI and the docs + +- **`$OPENREPOSHAPE_BIN_DIR` and control characters.** `install.sh` and + the bash `openRepoShape --install` refuse a `$OPENREPOSHAPE_BIN_DIR` + holding a control character, a tab, DEL and a C1 control included, before + they name it, exit 2, and `install.sh` refuses one holding `:` too; + `install.sh` refuses each before any request. `--install` used to place + the command under such a directory and print a carriage return or an + escape as typed (#804, #760). +- **Characters a terminal draws as another.** `install.sh` and the bash + `openRepoShape` refuse a `$OPENREPOSHAPE_BIN_DIR`, `install.sh` a + repository, a version or an option, and the command a repository or a + ref, that holds one of 41 code points, `MISDRAWN_CHARACTERS` in the + launcher: U+00AD, the twelve bidi controls, U+2028, U+2029, and the + format characters of no width that nothing needs, U+2065 among them, + which Unicode leaves unassigned. Each is refused by its code point, + before any request, and never echoed, where `~/` U+202E `.local/bin` + could read as another directory. The newest release's tag, which + `install.sh` reads from the redirect, or from `gh api` where the redirect + names none, is refused the same way once that request has answered, and + a redirect's location holding one is left unnamed in the message that + says it names no release. U+200C and U+200D stay allowed (#841, #837). +- **A name in a multibyte locale.** Where the bytes refuse a + `$OPENREPOSHAPE_BIN_DIR`, both files read it again in the locale's own + charset, as `locale charmap` names it, through `iconv`, so a GB18030, GBK + or Shift_JIS name that holds no control character and nothing drawn as + another is placed, a default `$HOME/.local/bin` under such a user name + among them. Where nothing can read it, the bytes decide as before (#867, + #858). +- **`$OPENREPOSHAPE_REPO` and `$OPENREPOSHAPE_REF`.** The bash command + checks both once, before `--version` answers and before any fetch, so a + control character or a character a terminal draws as another in either + is refused, exit 2, and never printed or sent, where `--version` printed + both as typed. `--help` still answers first, and names the variables, + never their values (#872, #866). +- **A ref git refuses part by part.** `setup.sh` and `install.sh` hold a + ref to git's rule part by part, as `setup-project.py` does (#873, + #857), and so does the bash `openRepoShape` where it records a ref, + reads its record, or checks one out for `--doctor`: 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 `.` are refused, beside + `..` and the `.lock` ending as before, and each refusal states the whole + rule. So `setup.sh --shape-ref a.lock/b` no longer clones the standard and + ends in git's `pathspec 'a.lock/b' did not match`, `install.sh a.lock/b` + no longer asks the network for the launcher there, and + `openRepoShape --install` with `OPENREPOSHAPE_RECORD_REF=1` no longer + writes such a ref to the record every later fetch reads (#875, #874). +- **CI on a release branch.** `tests.yml` runs on a push to a + `release/v.` branch that holds this change as on a push to + main, so a patch's commit there gets its own push run with no dispatch. + Its concurrency group includes the ref, so a fast-forward of that branch + to main's commit no longer cancels main's run on it. `release.yml`'s + comments say how a patch is cut from `release/v` and how a fix is + back-ported; its YAML is unchanged. `release/v1.0` does not hold the + change and still needs a dispatch (#852, part of #850). +- **Tests.** Tests append what they tell git to the `GIT_CONFIG_COUNT` + entries the environment holds, through `appended_to_command_scope`, so a + pasted line keeps the session's maintenance settings and the Windows + job's two entries. A scan test, + `test_no_test_sets_a_git_config_count_of_its_own` in + `tests/test_no_auto_maintenance.py`, fails where a test sets a count of + its own, but for the files `OWN_COUNTS` lists, which build one on purpose + (#818, #758; #848, #835). The doctor tests' `paste()` gives a pasted + `git submodule update` the `insteadOf` rewrite, so it reads a test's bare + remote through git's transport (#848, #835). The quoting-sentence + inventory sees a quoter handed on as a value, and the launcher guard + reads `run_child` and `run([PYTHON, ...])`, with rows for + `src/openreposhape/cli.py` and `setup-project.py` (#808, #759). Four + tests, `directory_named` and its file twin ask one helper whether a name + that is not UTF-8 can be made, and skip on its two answers only, so a + full disk or a folder nobody can write in fails them, where they read as + SKIPPED; three tests skip where an ASCII file-name encoding cannot carry + their text (#806, #757). Three more tests ask that helper before they + make their own name, `make_file` skips on EILSEQ or a `UnicodeError` + only, and the U+202E directory of `test_adopt_template_values.py` has no + guard, so a full disk or a read-only `tmp_path` fails those tests, where + they read as SKIPPED (#845, #834). Six skips on a failed git call ask the + system first, whether the file-name encoding can spell the name and the + disk hold it, so a failed git call fails the test; the U+202E tests, and + six template-value tests, skip where a C locale's encoding cannot spell + their text, where they raised `UnicodeEncodeError` (#869, #853, #854). + The `pins` row's merged-away check is tested where `check` refuses the + standard, and beside a `both`, an `upstream-removed` and an `unmapped` + row (#770, #755). None of these changes a shipped file. +- **The docs.** README.md and the handbook say that `family.py init`'s + `--into` refuses its default inside a checkout of the standard + (`family-landing-in-standard`), and describe `make siblings` as it stops + before cloning; one sentence in five places, AGENTS.md and the holder's + README and `AGENTS-shape.md` among them, says that a holder keeps + `members/` in one spelling (#738, #695). That closes a known limit + v1.1.0 named. The holder's README and `AGENTS-shape.md` say its parent + folder is named EXACTLY after its repository, case included, and + AGENTS.md and `AGENTS-shape.md` say the validator may name a hand repair + of a `members:` row, whose commit is then the person's (#822, #763). + README.md and the handbook also say that Git Bash is handed `sh`'s + quoting (#821, #711), that a path the doctor cannot print as itself is + written `$'...'`, which dash does not run (#792, #785), and that a + `.gitmodules` git refuses fails CI whatever the credential (#790, #786). + +## Known limits + +v1.1.0's [known limits](v1.1.0.md#known-limits) that this release does not +close still hold; this list is not all of them. Of the two that work on +v1.1.0 named and left open, this release closes the README's and the +handbook's `--into` sentence (#738, #695). The other, +`bump-leg.py --dry-run` exiting 0 over a pin its own validators refuse, is +unchanged but for one case: a `legs:` entry that is not a mapping is now +refused by the dry run as by the real run (#862, #832). And #457's L1-17, +which v1.1.0 names, is narrower: where the source has lost its `origin` and +the plan records its repository, `execute` now compares the plan with that +record before it reads `adopt_branch:`, so an `org:` that differs from it, +as one outside the alphabet does unless it casefolds to the recorded +owner, is `plan-not-the-source` there first (#825, #716). + +New in v1.1.1, from the pull requests that landed it: + +- A project or a holder keeps v1.1.0's copies until `update-shape.py apply` + re-pins it, and until then its own gates and `make bootstrap` behave as + v1.1.0's: for example, they read a `tracking_branch:` ending in U+00A0 as + the branch without it (#823, #715). +- The pipx command, the `openRepoShape` that `pipx install openreposhape` + places, is unchanged since v1.1.0, and none of the bash command's new + checks is in it. Measured on the package at main `c5359df`: its + `--version` prints a `$OPENREPOSHAPE_REPO` holding U+202E as it is, exit + 0, and it refuses a `$OPENREPOSHAPE_REF` holding one by its pattern, exit + 2, with the character as it is in the refusal. A control character in + either is refused, as in v1.1.0 (#837, #866). Nor does it hold a ref to + git's rule part by part: its `ref_fault` refuses only `..` and a `.lock` + ending (#875, #874). +- The bash `openRepoShape` holds a ref to git's rule part by part only + where it records a ref, reads its record, or checks one out for + `--doctor`. With `OPENREPOSHAPE_REF=a.lock/b` set for one run, a + scaffold still asks GitHub for `setup.sh` at that ref, and `--version` + exits 0 (#875, #874). +- A tracked name that the doctor sets aside before it walks a leg, one + holding a control character or a byte that is not UTF-8, still counts in + `plan`'s and `execute`'s extension-majority fold. So `engine/` holding + `a.py`, `b.py`, `x.md` and `tb.py` folds to code, 3 of 4, and the + doctor, which sees 2 of 3, reads `code/engine/x.md` as MISPLACED, with the + plan exactly as `plan` wrote it. It is on #457's checklist as L1-104 + (#829, #801). +- A `--source` directory this user cannot enter, or one under a directory + they cannot enter, still ends `adopt-project.py` in a `PermissionError` + traceback, exit 1. It is on #457's checklist as L1-105 (#831, #796). +- A `--pin-source` inside a non-bare clone's git directory, such as + `glass/.git`, is refused `pin-source-not-a-clone`, naming the git + directory, although its digest would have been the whole tree's. A git + that refuses a clone it finds (`safe.directory`, "dubious ownership") is + "git finds no clone there", followed by git's own reason (#842, #800). +- A plan read at `--tracking-branch`, because the source's default branch + had no local copy, records that branch as `source.default_branch`, and + goes stale once the default branch is made local: `check` then reads the + source at it, finds `plan-stale` where its commit is not the plan's, and + `plan` is run again. The printed `git branch origin/` can bring + back a branch the remote has since deleted, where the clone was fetched + without `--prune` (#830, #797). +- Whether `--local-remote-dir` can be written in is asked with `os.access`, + which tells root yes whatever a folder's mode says, and tells yes of any + folder on Windows; an ACL that says otherwise is met at + the `mkdir` or `git init`, after nothing, or after the spec leg, was + made. A private repository the token cannot see answers as not found, so + `execute` keeps its notice for what can fail after the first create. A + name that GitHub only redirects, after a rename or a transfer, is free, + and `gh repo create` takes it over, which ends the redirect; neither + `check` nor `execute` says so. `check` without `--local-remote-dir` needs + `gh` for a clean read; offline it passes, with a NOTE (#861, #798). +- `adopt-work-dir-shared` does not guard against another user who can + write, through a group, in the work directory, its `source-readonly/` or + a directory above it, since group write is not asked; Windows, where + nothing is asked and the removal walks by path; root, as the other user; + this user's own other processes; or the default work directory, which is + not asked, and which only a `TMPDIR` every user can write in without the + sticky bit would open (#843, #803). +- `adopt-work-dir-shared` refuses some `--work-dir` that v1.1.0 used: one + that is, or is under, a directory a user other than this one or root + owns, a run as root over another user's directory included (such as a + host directory mounted into `docker run` as root), or a directory every + user can write in without the sticky bit, such as a shared folder at + 0777; and one on a drive mounted without ownership, which reads as 0777 + (on macOS an exFAT, FAT or NTFS drive, on Linux ntfs-3g by default, and + under WSL a Windows drive). The way out is a `--work-dir` this user owns + on a disk that keeps owners, `chmod +t` on such a directory that is this + user's, running as the directory's owner, or no `--work-dir` (#843, + #803). +- `update-shape.py apply` refuses as `update-rewrite-symlink`, before any + write (#286), a root that keeps a symlinked directory above a pinned + copy, such as `.github -> ci`, so it never extends the line-ending block + with the tracked spelling; the link is replaced first, as the remedy says + (#844, #807). +- `update-shape.py`'s missing-name refusal asks one hop: a validator that + reaches a line's importer through another copy is not found, so the + refusal can say the validators pass where one does meet the line. What + its readers cannot tell runs or not they read as running, so a copy can + be refused though Python starts it; a handler that binds and re-raises + is a line on #458 (#802, #675). +- On Python 3.12 and 3.13, a project's `make bootstrap` still ends in a + `PermissionError` traceback over a leg directory nobody may enter (step + (a)) or a `governance/` nobody may enter (step (c)), as #815's body + measured. Beneath a `bootstrap REFUSED` line, `setup-project.py`'s step + (7) still says `bootstrap failed (see above)`, and `siblings.py` that the + member's own bootstrap exited 2 (#815, #764). +- `update-shape.py apply --branch `, run from a branch whose name is + not UTF-8, is refused, exit 2, before anything is written, where v1.1.0 + committed and pushed the new branch, or said `nothing to do`; `apply` + without `--branch` runs as before, and the way out is one `git branch -m`. + `family.py`'s dry runs on such a branch print their plan and then refuse, + exit 2 (`bump --dry-run` has fetched in the member by then), and no test + holds that refusal; their real runs refuse before anything is fetched or + written. A push line naming a branch that holds U+2028 or U+0085 names it + exactly, inside single quotes, so a viewer that breaks lines there shows + the line broken (#849, #765). +- The commands a remedy prints for a person to paste, such as + `family.py`'s `switch -c`, `add` and `rm -f` lines, are quoted for a + shell, and still hold a name holding a control character raw inside the + quotes; the follow-ups are on #459 and #458. `family.py init`'s closing + NEXT STEPS block and `scaffold-project.py`'s `Working trees are at` line + still print a path as it is, so a line break in it prints a line of its + own there (#814, #766). +- A path the doctor cannot print as itself is written `$'...'` off Windows + and in Git Bash, which bash, zsh and ksh run and dash does not; README.md, + the handbook and AGENTS.md say so. Its PowerShell spelling is held as + text, and was not run in PowerShell (#792, #785). +- A PowerShell started from inside Git Bash inherits `MSYSTEM`, so it is + handed Git Bash's quoting; the handbook says so (#821, #711). +- On Windows, a `.gitmodules` section name that is not UTF-8 is still not + found by the doctor (#774, #732). +- On a git older than 2.45 without the security releases of 2024-05-14, + which ignores `GIT_NO_LAZY_FETCH`, the doctor's reads of a partial clone + are held only by the guards each read had before (#791, #787). +- Before Python 3.12, which has no `os.path.isjunction`, a junction to + nothing at `../` still reads as an empty slot to + `make siblings` and to the doctor. No Windows run was made: the tests + stand in for the junction (#794, #793). +- `make siblings` has no unmerged check: with `.gitmodules` unmerged, and + resolved in the work tree but not added, git says `No url found` while + `make siblings` takes the work tree's url, and so does the doctor's + `members` row, which reads the file as `make siblings` does. Where git + finds no url, `make siblings` still clones the member from + `family.yaml`'s `repository:` (#775, #733). +- A holder's `make bootstrap` reads each member's make through a pipe on + stderr, to read make's `Error ` line, as `run_make`'s docstring + says: a member's recipe has no terminal on stderr, its stdout and stderr + lines can come out of order, a process a command leaves running with + stderr open holds the run until it closes it or exits, and on Windows a + member's text that is not ASCII comes out garbled (read, not run). The + run's last line names the innermost command's code only as far as + make's lines tell it: in the nested-make cases `MakeErrors`' docstring + lists, such as a recipe that lets an inner make fail and then exits 2 + itself, it names another command's code, or none; and a command a + signal ends under dash, `/bin/sh` on Debian and Ubuntu, reads as exiting + 128 and the signal's number, 137 for SIGKILL. Make before 4.0 was not + run (#876, #839; #886, #884). +- Where the work tree's `.gitmodules` is gone, `validate-family.py`'s + wrong-case move writes the file with one key, and typed as printed it + commits a `.gitmodules` that holds that key alone. That line, and + `member-gitlink-removal-staged`'s `git checkout HEAD -- .gitmodules`, are + not ordered with the url-absent and absent findings that put the file + back (#876, #839; #879). +- In the same state, with every member checked out and a line git cannot + parse after the first section of HEAD's `.gitmodules`, git's update exits + 128 while a holder's `make bootstrap` prints the `SHAPE_LEGS_TOKEN` advice + and `family bootstrap ok` (#876, #839; #879). +- Beside a committed value git refuses, a stray gitlink in `members/` + under another spelling of a row's path is still told a `git rm --cached` + that exits 128 after the value finding's lines + (`please stage your changes to .gitmodules`), `-f` or not: #882 stages the + file first only for a gitlink outside `members/` (#882, #878; #879). +- After the lines for a member moved by `git mv` to another spelling, the + checkout stays at that spelling, untracked: the gate passes, `family.py + add` then refuses it as `family-members-dir-variant`, and no finding says + what the directory is or that its commits are in the member's store + (#876, #839; #879). +- A `path` key with no value in HEAD's `.gitmodules` is reported by + `validate-family.py` twice, as `gitmodules-value-refused` and as + `member-gitmodules-absent`; both lines write the same path back, so + typing both is harmless (#819, #768). +- `validate-family.py`'s reading of where `git submodule init` stops is + simpler than a holder bootstrap's: for a section name with a `..` + component, a url beginning with `-` or an unmerged `.gitmodules`, its + refusal keeps the plain update, which dies. Its `git rm --cached` line + for a stray gitlink that a sparse checkout keeps off disk has no + `--sparse`, so it exits 1, with git's own hint (#820, #762). +- `family.py add`'s case-spelling remedies leave three states unruled. A + second gitlink at a case variant of a row's path, where the row's path + holds index entries under it rather than a gitlink, keeps #574's + remedy. An unmerged entry the sparse rules leave out may need + `--sparse`, which is not printed (not measured). And the move back of a + member whose gitlink is at a variant points back every section that + gives a variant of its path, so one that is another submodule's would be + pointed back too and give the path twice (read from the code, not + measured) (#865, #840). +- GitHub's limits are asked only where a GitHub repository will be made, + and none was measured against GitHub: a repository name's 100 and a + topic's 50 are from GitHub's documentation, and an organisation's 39 is + its documented limit on a user's name, since it documents none for an + organisation (#859, #838). The characters an `--org` may hold are from + its documentation too, and `_` is let by, for GitHub Enterprise Managed + Users (#871, #863). A leg the plan was edited to name, over a limit, is + told "Use a shorter --project", which does not shorten it (#859, + #838). +- In `setup-project.py`, `--project 'Atlas '`, given as an argument, still + reaches step (4) with a space nobody can see, and under + `--local-remote-dir` a name of 247 to 255 characters prints step (4) + before step (5) refuses it (#811, #777). A refused name ends the run, + with no second prompt (#756, #729). +- A lone `0x9B` byte in `$OPENREPOSHAPE_BIN_DIR` is placed: a terminal that + reads UTF-8 draws it as a character it cannot read, and only one set to + an 8-bit charset acts on it (#867, #858). +- A multibyte locale that is not UTF-8, left in a terminal that reads UTF-8, + is handed the C1 bytes that the locale reads as letters, and that terminal + acts on them. No GB18030, GBK or Shift_JIS locale was built: the tests + stand in a `locale` that answers `locale charmap` as glibc does (#867, + #858). +- Windows and macOS: CI runs on Linux only (#597), but for the launcher's + and `install.sh`'s suites, which it also runs on Windows in Git Bash. A + disk that folds case (#820, #828), Git Bash's and PowerShell's quoting + (#821, #792) and a Windows disk refusing a name (#806) are emulated in the + tests, not run there. No CI job runs the bash `openRepoShape` or + `install.sh` on macOS, nor under its bash 3.2: `release.yml`'s macOS leg + runs only the pipx wheel's commands. Their control-character match on + `$OPENREPOSHAPE_BIN_DIR` and their search for the 41 code points read + bytes under `LC_ALL=C` (#804, #760; #841, #837); the `[![:print:]]` + checks of a repository, a ref, a version and an option, and the + multibyte re-read through `locale charmap` and `iconv` (#867, #858), use + the person's locale. +- The tests' scan for a `GIT_CONFIG_COUNT` set by hand does not read a key + name imported from another module, a value built by `format()`, + `repr()` or `os.fsdecode()`, a conditional value, `setenv` with + keywords, an annotated subscript, a value held in a variable and set + later, or an indexed `GIT_CONFIG_KEY_` name; not a product bug (#848, + #835). + +## How this release is cut (remove at tag time) + + + +v1.1.1 is cut from `release/v1.1`. The branch model is Brett Heap's ruling +of 2026-10-17T19:52:26Z, "back-port to release/v1.1 as 1.1.1, open the +issue" (#850): main stays the trunk, and every change lands there first, +through its own review, CI and gate; `release/v1.1` was cut at the `v1.1.0` +tag, and each patch is tagged on it. His word "go with option A for #850", +at 2026-10-18T01:05:28Z, chose ruleset 25325536, "Release branches +release/*: no force-push or deletion", made a minute later: it refuses a +force-push to a `release/*` branch and its deletion, has no bypass, and +requires no pull request, so the fast-forward stands. + +Lane openRepoShape-3, the release owner, fast-forwards `release/v1.1` to +main after each landing, so the branch holds main's commits and no other. +If a pull request that is not v1.1.1 work lands on main first, +`release/v1.1` takes v1.1.1 fixes from then on by back-port only (#473 +issuecomment-6244650070): a pull request to `release/v1.1` of +`git cherry-pick -x `, as `release.yml`'s header says. The release +commit is then the cherry-pick of this pull request's squash on +`release/v1.1`, not main's commit. + +This notes pull request lands last, as the release commit, under lane 3's +freeze, and lane 3 reviews it. The tag goes on the release commit, at +`release/v1.1`'s tip. `.github/workflows/release.yml` publishes the wheel, +and its header's "RELEASING TO PyPI" and "RELEASE BRANCHES AND BACK-PORTS +(#850)" are the authority for these steps. The acts made once, which +[v1.1.0's notes](v1.1.0.md) record, are not made again; the build job +checks the two environments before every dispatch. + +0. **Put `release/v1.1` at the release commit, and read both refs back.** + On the fast-forward route, push main's squash of this pull request to + the branch, which is refused unless it is a fast-forward; on the + back-port route, the cherry-pick's pull request lands it there: + + ```sh + git push origin :refs/heads/release/v1.1 + git ls-remote origin refs/heads/main refs/heads/release/v1.1 + ``` + + `release/v1.1` must name ``, and on the fast-forward + route main must name it too. +1. **The release commit passes `tests` on itself.** `release/v1.1` holds + #850's `tests.yml`, so the fast-forward of `release/v1.1` to the release + commit starts the branch's own push run on it, beside main's push run + that landed it. The branch's newer run decides: tag once every + `tests.yml` run on the commit has finished and the newest is green. On + the back-port route the commit is not on main, so the push run the + merged cherry-pick starts on `release/v1.1` is the only run. A + pull request's run, or a run on another commit whose tree is the same, + does not count. Where no finished green run tested the commit, dispatch + `tests.yml` on a branch at it before tagging, and wait for `tests` to + pass: + + ```sh + git push origin :refs/heads/ + gh workflow run tests.yml --ref -R opensoft/openRepoShape + ``` + +2. **Tag that commit, annotated, and push the tag.** The build job refuses + a commit that is on neither `main` nor `release/v1.1`: + + ```sh + git tag -a v1.1.1 -m "openRepoShape v1.1.1" + git push origin refs/tags/v1.1.1 + ``` + +3. **Dispatch the release workflow on the tag, with the version:** + + ```sh + gh workflow run release.yml --ref v1.1.1 -f version=1.1.1 \ + -R opensoft/openRepoShape + ``` + +4. **Brett Heap approves the `testpypi` deployment,** by his own click in + GitHub, on the run's page: "Review deployments", then "Approve and + deploy". A word in a chat is not an approval, and a session never + approves. The install check then runs on ubuntu, macOS and Windows and + on Python 3.9, and he approves the `pypi` deployment the same way, only + once it has passed. Before either is reported done, read the run's + approvals, which the jobs themselves check: + + ```sh + gh api repos/opensoft/openRepoShape/actions/runs//approvals + ``` + + A failed job is re-run with "Re-run failed jobs", never by re-running + the whole run or dispatching the version again. +5. **Only then create the release, with `install.sh` attached,** from a + checkout at ``. `` is a copy of these notes made + outside the tree, with the draft comment at the top, both sections + headed "remove at tag time" and every `**[placeholder]**` marker cut, + the counts beside the first marker brought up to the tag, and + `` filled in under "Install and upgrade", in the PyPI + `--version` line and the `update-shape.py apply` line. This must print + nothing: + + ```sh + grep -n -E 'placeholder|remove at tag time|FIRST DRAFT|' + ``` + + Check that the release GitHub marks as latest is v1.1.0, as it was when + these notes were drafted, so that v1.1.1 is the newest release of the + newest line: + + ```sh + gh release list -R opensoft/openRepoShape + ``` + + Then mark it latest, and the one line installs it: + + ```sh + gh release create v1.1.1 install.sh --verify-tag --latest \ + --title "openRepoShape v1.1.1" --notes-file -R opensoft/openRepoShape + ``` + +6. **Check the published asset, by redirect and by bytes,** as v1.1.0's + step 6 does, with `v1.1.1`: + + ```sh + curl -fsSI --proto '=https' --proto-redir '=https' \ + https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh + curl -fsSL --proto '=https' --proto-redir '=https' \ + https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh \ + | cmp - <(git show v1.1.1:install.sh) + ``` + + The first must answer with a `location:` naming + `…/releases/download/v1.1.1/install.sh`; the second, run in bash or zsh, + must print nothing and exit 0. + +A version is uploaded once. A bad upload is fixed by the next patch, never +by moving a tag. + +## Before tagging (remove at tag time) + + + +- **``** stands for the release commit wherever it appears + above. +- **Not landed at drafting time:** #778 (#707), which lands next, and + whose line above is written from its reviewed head `8259ea1`; and this + pull request, which lands last, as the release commit. +- **[placeholder: re-read at tag time.] Open `v1.1.1` milestone issues once + #778 has landed (1 open; 71 of 72 closed: the 69 that the 11:31Z read + found, #709 among them, #884, which #886 closes, and #707, which #778 + closes):** + - #879, the family root's `validate-family.py` and `bootstrap.py`: with + the work tree's `.gitmodules` gone, three remedies undo each other or + refuse; a `git mv`'d checkout is never named; bootstrap says `ok` beside + git's exit 128. + + Each that lands before the tag gets its line above, with its pull + request, and a known limit above that names it goes; each that does not + is a known limit or moves to the next milestone. +- **#709** was closed as not planned, at 2026-10-16T14:24:32Z, before + v1.1.0 was tagged, and no commit since v1.1.0 names it, so it has no line + here. +- **The shape copies, again at the release commit.** Measured at main + `85555d0`, and again at main `c5359df`, with a stub `gh` and local + remotes: a project scaffolded by v1.1.0's `scaffold-project.py` and a + holder made by v1.1.0's `family.py init` each got the six + `upstream-changed` rows listed under "Install and upgrade" from + `update-shape.py check`, and every other copy `unchanged`. `apply` + re-pinned each with its own validators green, the project once its legs + were checked out, and the doctor then said `COMPLIANT` of both. From + `c5359df` to `e7039b7`, `templates/` and `scripts/repo_shape.py` changed + only in the family root's `scripts/bootstrap.py` and + `scripts/validate-family.py` and the assembly root's + `scripts/validate-pins.py` and `scripts/bootstrap.py`, each on those + lists, and #778's head changes neither. A pull request that lands before + the tag and changes another pinned copy adds it to that list: run + `check` again at the release commit. + +## Full list + +- Merged pull requests: +- The `v1.1.1` milestone: +- Every commit since v1.1.0: