Skip to content

openRepoShape v1.1.1

Choose a tag to compare

@brettheap brettheap released this 18 Oct 18:31
· 194 commits to main since this release
5017830

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).

At main 5017830, on 2026-10-18, 70 pull requests had landed on main since
v1.1.0's commit, 190133d. At 14:17Z, 71 of the v1.1.1 milestone's 71
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-<id> 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 in the doctor's own checkout that does not
    load, or cannot be used, is an n/a row on the way in and, in a
    repository, the other 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<X.Y> 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:

    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 @ 50178305e4229275d18e71c2152403b4e9d3004e).
    v1.1.0's notes 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 <path> reports each
    as upstream-changed in a root pinned at v1.1.0 that has not edited it,
    and
    update-shape.py apply --root <path> --at 50178305e4229275d18e71c2152403b4e9d3004e --yes --branch shape/update-<sha>
    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-<id> 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<U+00A0>, 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 <out>, 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: <name> (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 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 <out> 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/<branch>. 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<U+00A0>, 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-<id> 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<U+00A0>, 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: <name> (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 <target>:<source> 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 <commit>. The missing-name refusal
    says the validators pass only where none of them meets the line. The
    readers skip an if False:, if 0: or if None: body and a statement
    after an unconditional raise, as Python does, and the binding reader
    an if TYPE_CHECKING: body, which the import reader already skipped
    (#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
    <repository>: N workflow @<sha> 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 <Project>-spec and <Project>-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-<id> 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
    ../<Project> 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 <code> 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 <shape> fetch --refetch --no-filter <remote> <commit>..., 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 <root> 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 <root> fetch --refetch --no-filter <first promisor remote>, 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 whose own AGENTS.md says so keeps its leg row and its Triad row
    over each such fault. Over a tab line its leg row was
    FINDING the way in refused, and over a pattern that does not compile,
    or a policy that raised on its name, the doctor ended in a traceback,
    exit 1. A leg known only by its name is read through that policy, so
    over a tab line or a pattern that does not compile it gets a
    repository's two n/a rows and a Triad row that calls it a single
    repository. The verdict is 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<major>.<minor> 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<X.Y> 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, for
    a name that is not UTF-8, whether the disk can 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 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).
  • Under the C or POSIX locale, a $OPENREPOSHAPE_REPO URL or path, or a
    ref, with a letter past ASCII, such as /srv/café/openRepoShape.git, is
    refused as "a control character", exit 2, by the bash openRepoShape and
    by setup.sh's two guards; install.sh's three checks, which refuse
    such a value in both locales, give the same words under C. It is a
    regression from v1.1.0: #872 moved the [![:print:]] check up front, and
    under C it matches each byte past ASCII, so --version, --install
    from a file, --doctor <dir> beside a checkout and --preflight through
    a setup.sh in a checkout now refuse a repository that v1.1.0 ran. Only
    a mirror at such a URL or path, in a shell at C or POSIX (cron, a CI
    step, a minimal container), meets it; an owner/name never does. It
    refuses and does nothing else, and a UTF-8 locale such as C.UTF-8 takes
    the URL or path. It is tracked as #940, for v1.2.0 (#872, #866).
  • 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 t<TAB>b.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 <b> origin/<b> 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)): #815's body
    measured it on Python 3.12, and 3.13 is read, not run. 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 <new>, 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 ../<Project> 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 <code> 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, since a GitHub Enterprise
    Managed User's login holds one, so an organisation's name holding _ is
    let by too, though GitHub gives no organisation one (#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_<n> name; not a product bug (#848,
    #835).

Full list