Repository navigation
openRepoShape v1.1.0
openRepoShape v1.1.0
v1.1.0 puts openRepoShape on PyPI. pipx install openreposhape is a second
way in, beside the one-line install, and the first on native Windows, where
PowerShell cannot run the one line (#563). This release also carries v1.0.1's
one-line install into main (#564), fixes limits that v1.0.0 listed, and runs
CI on Linux only (#597).
At main 190133d, on 2026-10-17, 123 pull requests had landed on main since
v1.0.0's commit, bfed143; at 21:55Z, 133 of the v1.1.0 milestone's 133
issues were closed.
What 1.1.0 adds
-
openRepoShape on PyPI, as
openreposhape(#590, #599, #563). One
command installs it:pipx install openreposhape
It places five commands.
openRepoShapehas the launcher's modes,
<Project>,--preflight,--doctor,--versionand--help, written
again in Python, so native Windows gets the command, as an.exe.
scaffold-project,adopt-project,update-shapeandshape-doctoreach
run the tool of that name with every argument unchanged, sodocs/cli.md
is their reference.git-filter-repo2.47.0 comes as their dependency,
and each command puts it first on thePATHof the tools it starts.The wheel is thin. It carries the release's commit and the
sorted-ls-tree-r-v1digest of its tree, and the tools run from a full
clone of this repository at that commit, made once in your per-user cache
and kept. Before each run the clone must be at that commit, unchanged, and
digest to that value, so a project scaffolded through it pins exactly this
release.$OPENREPOSHAPE_REFnames another ref for one run, with no digest
checked. The package's--installrefuses and names
pipx upgrade openreposhape. Python 3.9 or newer.The version is the tag's, so 1.1.0 is the first version on PyPI. The
v1.0.x line is not published there. -
The one-line install, on main (#564, carried into main by #587):
curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bashIt installs the newest release's bash
openRepoShape, pinned to the
release tag, and ends withopenRepoShape --preflight, as
v1.0.1's notes say. Since v1.0.1:- it installs git-filter-repo with
uvwhere there is nopipx, and never
withpip install --user, which an externally managed Python refuses
(PEP 668). With neither, it prints one command for this machine: brew's,
pipx's from apt or dnf, pacman's, or uv's (#598, #595); - it ends with two lines,
adoption:and thendone:, so a machine that
is ready to scaffold but not to adopt is told so (#598, #595); - Git Bash, the bash that Git for Windows installs, runs it and the
openRepoShapeit places, tested in CI but for a typedyes, which the
preflight cannot take there (#596, #585); - it refuses a target that is a symbolic link, such as the
openRepoShapepipx places, and so does the launcher's--install
(#563).
- it installs git-filter-repo with
-
CI on Linux only (#597). Brett Heap decided on 2026-10-15 to drop the
macOS and Windows test jobs across the estate.tests.ymlruns the whole
suite onubuntu-latest, on every push to main, every pull request and
every dispatch. Since #593 each push run on main is grouped by its own
commit, so the next landing no longer cancels it, and every landing keeps
its own Linux result. One exception, on his word the same day: a
tests-git-bashjob runs the launcher's andinstall.sh's suites, and
nothing else, on Windows under Git Bash (#596, #585). The rest of the
suite no longer exercises Windows file-system behaviour, and the stock
macOS bash 3.2 is a goal the bash entry points are written for rather
than a checked claim. The release workflow still
installs the package on Linux, macOS and Windows before it reaches PyPI.
Install and upgrade
- The newest release, by the one line: the line above. The same line
again upgrades.… | bash -s -- v1.1.0names this release.
openRepoShape --versionprintsopenRepoShape (opensoft/openRepoShape @ v1.1.0). - From PyPI:
pipx install openreposhape, or
pipx install openreposhape==1.1.0for this release.
pipx upgrade openreposhapeupgrades it.pipxitself comes from
sudo apt-get install -y pipxon Debian and Ubuntu and
brew install pipxon macOS; on Windows,python -m pip install --user pipx,
thenpython -m pipx ensurepathand a new terminal.
openRepoShape --versionprints
openRepoShape 1.1.0 (opensoft/openRepoShape @ 190133d5e704e380c430f87ca9c4e31746de0012), then the
version, tag, commit, digest and the clone it would use. - One installer or the other. Both place
~/.local/bin/openRepoShape.
pipx does not replace a command it did not place, andinstall.shand the
launcher's--installrefuse the link pipx places, naming
pipx upgrade openreposhapeor removing the link. - Native Windows:
pipx install openreposhape, then
openRepoShape <Project> --org <your-org>. Downloadingsetup-project.py
and runningpy setup-project.py <Project> --org <your-org>still works,
and installs nothing. Git Bash runs the one line and the bash
openRepoShapeit places, tested in CI but for a typedyes, which the
preflight cannot take there (#596, #585). - A project's pin on the standard works as before. Follow
How to pin 1.0.0 with the v1.1.0 release
commit. A scaffold run through the package pins that commit and digest by
itself. - A family holder made before #642. A holder made with
--family X --name Ybefore #642 (the fix for #629) keepscd Yin its
own README, andupdate-shape.py applynever re-renders a README, so
change that one line by hand tocd X: the holder's directory is named
after its repository,X, never after the display name. The same README
draws its layout asY/Yand calls the doubledY/Ydeliberate; read
X/Xthere, which is whatmake siblingschecks the folder against.
What changed since v1.0.1
v1.0.1 was cut from release/v1.0 and added only the one-line install.
Everything below landed on main after v1.0.0's commit, by area. The list
runs to main 6f64eea.
adopt-project.py
- The source's
origin, read one way.planreads<org>/<repo>from
everyorigingit accepts, a local clone or mirror path and a URL ending
/included, so a local clone's right--orgis no longer refused
(#591, closing #521 and #427). - Refused before any leg exists.
checkfinds, andexecuterefuses
before any leg exists, a file the root keeps at the shape's own pin path
and a--work-dirthe user cannot write in. Both used to fail only after
both legs were pushed (#581, #524).checkrecomputes each neutral pin's
digest at the plan's commit, in the cloneplanread it from, where that
clone is still on the machine and is no partial clone, so a commit or a
digest edited into the plan no longer reaches the root as
adoption verifiedover a redvalidate-pins.py(#589, #449). Before any
leg too: the branch namesexecutefailed on, such as
adopt_branch: a..b, a source branchAdoptor one hidden byhideRefs
(#583, #520, #420); a kept nested.gitattributesthat outranks the
root's line-ending rule for a pinned copy, and a root.gitattributes
thatgit addwould re-encode (#619, #530); and a kept submodule url on
this disk wheregit ls-remotereaches no repository, which used to end
adoption verifiedover a recursive clone that fails (#618, #418). - Names outside ASCII.
executehandsgit rmeach path the split
moves or drops as the bytes the tree holds, so such a name under
LC_ALL=C PYTHONUTF8=0no longer stops the run withUnicodeEncodeError
after both legs are pushed (#614, #527). - The work directory. A run without
--work-dirremoves the work
directory it made where it holds no file, andexecutenames the one it
keeps.checkandexecuteclone the plan'ssource.repositoryagain
where the clone it records is gone, so neither leaves an empty
openreposhape-adopt-*directory in the system temp folder, nor refuses
source-unclonableonce that folder is cleaned (#594, #526). - Answers kept.
executerecords the plan's answers under
answered_paths:, and a moved submodule names its row, so an answered
examples/is not asked again, and the doctor names no plan for a
deliberate move (#617, #525). - Leg paths that are not the defaults. The Makefile block
execute
appends, and the cross-leg follow-up, name each leg where the plan mounts
it, so--spec-path legs/specexports aCONTRACTS_DIRthat exists and a
nested--code-pathgets the right../path (#572, #522). - A leg path edited by hand. The read-across follow-up names
legs.code_pathandlegs.spec_pathand no path, and thecontracts/
rule's reason names none, so the hand edit of a leg's mount thatplan
asks for leaves no plan-time path incheck, the split commit or the pull
request (#628, #600). A plan written before v1.1.0 whoselegs.spec_path
orlegs.code_pathwas edited by hand keeps its old value-form
read-across follow-up.checkaccepts it, becausefollow_ups:is free
text, andexecutecarries the stale line into the split commit and the
pull request's body. Re-runplanfor such a plan. - A source's own
single-repository.yamlis asked drop or root, drop by
default, under a rule of its own, and no longer gets the general top-level
YAML question (#566, #528). - What a run says is what it does. The plan's last follow-up is a review
of the pull requestexecuteopens itself. A failed verification names
the pull request and the topic it left in place. Under
--local-remote-dir, NEXT names the split's branch. AGENTS.md says that
checkpasses an answer with noresolution:, and the ruleset hint no
longer offers the seed branch the scaffold dropped (#582, #523). With no
--elected-byand nogit config user.name, the refusal says where the
empty value came from (#567, #529). - An id
planderived. With no--id,planderives one from
--project, andadopt-bad-idnow says so:--project North_Windno
longer says--id 'north_wind' must matchover a flag nobody passed
(#620, #601). - Empty and derived values.
planrefuses--id ''and
--elected-by ''by name, a refused derived id says to change
--project(or, adopting in place, to rename the source first), and an
organisation read from a url is named as not given (#699, #658). --work-dir. A--work-dirthat is a file or cannot be made is
refused by name inplan,checkandexecute, and--helpsays what
--work-diris and where--orgcomes from (#719, #650).--source, by its spelling. Anorg/repothat is also a directory
here is refusedsource-ambiguous, naming both; any otherorg/repo, in
any spelling, is cloned by its name. The plan recordssource.cloned,
and a clone no plan records is removed, so a recorded clone that is gone
or emptied is refused, or cloned again, for what it is (#749, #652).- Names in a C locale. Under
LC_ALL=C PYTHONUTF8=0,plan,check
andexecutehand git andgheach name, url and branch as the UTF-8
bytes it spells, and the leg description, whose em dash stopped every
executeagainst GitHub at the first leg, so none of them ends in a
UnicodeEncodeErrorbefore any leg exists (#727, #653). - A default branch that is not UTF-8. A source whose HEAD or
origin/HEADis on a branch such ascaf<e9>is refused, or adopted past
it, instead of endingplan,checkandexecutein a traceback (#717,
#425). - The Free-plan secrets hint.
adopt-project.pyand
scaffold-project.pygo by the assembly root's own visibility on GitHub
and name the root as the reason, so a public source adopted with private
legs is no longer told that its organisation secrets resolve to EMPTY
(#621, #602). The hint, in the scaffold, the adoption andfamily.py,
goes by what GitHub says of the root or holder and of its organisation's
plan, and names any answer GitHub did not give, so
init --reuse-empty-repono longer calls a public holder private (#728,
#660). - Caught by
check, not after both legs.checkasks this machine's
git what the split stores for each file the plan keeps or moves and each
pinned copy, so one it would store as other bytes is a finding (#743,
#654). A source symlink whose target begins with a NUL byte is
plan-empty-symlink, whichexecuterefuses before any leg (#720,
#388). check --local-remote-dir.checktakes--local-remote-dirand
resolves each relative submodule url a leg keeps from the bare
repositoriesexecute --local-remote-dirwould create, so under that test
pathcheckno longer refuses an adoptionexecuteverifies, nor passes
oneexecuterefuses (#718, #417).adoption verifiedmeans a shape root. The verification fails a
split whose rootproject.yamlis not the shape's manifest
(adopt-manifest-not-shape), so it no longer passes an assembly the
doctor calls NOT A SHAPE ROOT (#722, #389).- Answers that stay answered. A plan's answer against a rule is
recorded, and an empty.gitmodulesasks nothing, sodocs/answered
codeis not MISPLACED afteradoption verified, nor is a.gitmodules
emptied bygit rmasked on every run (#721, #655). - Symlinks checked out as files.
check's finding for a link kept above
a path the shape writes says that the clone holds it as a file (#584,
#531). - Installing git-filter-repo.
filter-repo-missingnames its install
ininstall.sh's order:pipx install git-filter-repo==2.47.0, else
uv tool install git-filter-repo==2.47.0, else brew's, pipx's from apt or
dnf, or pacman's. It led with apip installthat an externally managed
Python refuses (PEP 668). It says thatpipx install openreposhapebrings
it for theadopt-projectcommand it places (#563).
update-shape.py, bump-leg.py and a project's own scripts
- A kept copy that would break bootstrap.
apply --accept-local
refuses, before anything is written, to keep a helper that lacks a name a
newly written copy imports from it (#562, #536), or a copy that imports a
name the module written beside it no longer defines (#616, #604). Both
used to pass the validators and then endmake bootstrapin an
ImportError. - Hard links and rollback.
applyrefuses to rewrite a pinned file that
has a second, hard-linked name. A rollback that cannot put a byte back
names that file with the line that finishes the job, and puts back the
rest (#560, #422, #428). - Headers, branches and messages. A
files:orshape:header with a
trailing comment is rewritten and keeps the comment,--branchis judged
the same from any directory, andapply's messages say what is true
(#558, #437, #431, #535). apply's lines, its branch and its headers. A rollback line for a
file that is gone, under a directory this user cannot write in, says to
make that directory writable first.committed on <branch>:names what
the commit holds, where it namedproject.yamlbeside a pin that moved
alone. A branch named like a tag is read, and pushed in the run and in
both printed push lines, by its full name, where the push failed
matches more than one. Afiles:orshape:header spelled
"files":orfiles :is found as the reader finds it, whereapply
refused the pin thatcheckhad sent it to (#810, #674). Of v1.0.0's
known limits, this closes the quoted header.bump-leg.py --dry-rungives the real run's answer.--toan
annotated tag's own id is peeled to its commit in both runs, and a pin
layout the rewrite refuses is refused by the dry run too (#561, #534,
#438).bump-leg.py's branch and its lines.bump-leg.pyreads the branch
in full, so a tag of the same name no longer hides the tracking branch;
its push line names the branch on both sides, so a branch beginning+
is no longer pushed as a forced update; it spells each file name it
lists; itscommittedline lists what the commit holds, and a gitlink
moved by hand is no longer reportedX -> X; and a dry run says it runs
no validator, where its behaviour is unchanged (#746, #672). With #810,
that closes v1.0.0's known limit on the pathsbump-leg.pyandapply
listed as staged, and onX -> X. A dry run that exits 0 over a pin its
own validators refuse stays one, which the dry run now says.- A
git switch -cline that is no dead end.bump-leg.pyand
setup-project.pysay under theirgit switch -cline what to do where
an earlier act left that branch, soalready existsis no dead end, as
family.pysays since #637 (#657, #647). - Paste lines.
bump-leg.pyandupdate-shape.pyquote the roots and
paths in the lines they print, the doctor quotes a leading=for zsh,
and the README says the printed commands are for a POSIX shell (#588,
#532). - Bootstrap. A leg detached off its pin is left where it is, and said,
so a commit made there is no longer stranded (#556, #533). A leg on
another branch whose tracking branch is behind the pin is fast-forwarded
and checked out, and the shipped text says bootstrap places a leg where
that loses no commit (#615, #603).make bootstrapmoves a leg only where
nothing the person has is lost, says which branch it left, and refuses by
name when git cannot run (#745, #704, #694). With no git onPATH, both
shippedbootstrap.pyscripts are refusedgit-not-runnable, exit 2,
where they ended in aFileNotFoundErrortraceback, which v1.0.0 listed
as a known limit. - A branch name git cannot take. A
tracking_branch:or a leg's
branch:that holds a NUL or a lone surrogate is avalidate-manifest.py
finding, andmake bootstrapdoes not hand it to git, so it no longer
ends in a traceback under amanifest ok(#644, #455). - The pinned validators. THE LOCKSTEP RULE is printed only where a leg
or member is out of step. A lone digest or workflow-reference mismatch
names itsbump-leg.pyline, and a holder's remedies name the branch to
make first (#575, #539). One leg's gitlink off its pin, beside another
leg's digest or workflow-reference mismatch, names the step that comes
first: put that gitlink back by hand, then run the other leg's
bump-leg.pyline (#702, #677). - The pinned shape copies, in one bump.
AGENTS-shape.mdis true of
every root, the scaffold's chain WARNING offers only its own sources and
spells--pin-source, the root's not-used note spells it too, THE
LOCKSTEP RULE'sbumpruns as printed, andrepo_shape.py'sgit_out
spells the folder it names and git's stderr (ESC, CR, BEL, C1 controls)
for every caller (#634, #610). - A workflows directory the pins gate cannot read.
validate-pins.py
refuses a.github/workflows/directory this user cannot list or search
asworkflow-unreadable, naming it, exit 2, where it ended in a
PermissionErrortraceback, exit 1 (#633, #434). - A pin list that is not a list. A project's
neutral_product_pins:or
a leg'snaming.referent_chain:that is not a list is
validate-manifest.py's finding for that field in every gate that reads
it, so5no longer endsmake validatein aTypeError, and a scalar
openXdoxis no longer read as its letters (#631, #443).
scaffold, setup, family and the shared readers
- Dry runs ask the real run's question.
scaffold-project.py --dry-run
andfamily.py init --dry-runask whether the remotes already exist, so
scaffold-remote-existsandfamily-remote-existscome at the plan, and
a missingghisgh-not-found, not a traceback (#573, #537). - Folders, asked before they are made.
scaffold-project.pyand
family.py initask whether each folder they make can be made, and take
onlygh's not-found answer as absent, so--dry-runrefuses a file
where a folder goes, or agh repo viewthat fails another way, by name,
as the real run does, and a refusedinitleaves no empty family folder
(#622, #609).
Those refusals, andscaffold-target-exists,family-target-existsand
family-operation-in-progress, spell a path holding a character that is
not printable as Python'sreprdoes, so an ESC or a line break in a
folder's name no longer reaches the terminal (#731, #692). --tracking-branch. The tracking branch is pushed by its full name,
so--tracking-branch +xscaffolds a project and initialises a family
instead of failing at the push after every remote exists (#643, #421).
family.py add,bumpandremoveprint their push line with the
branch named in full on both sides, so a branch beginning+or named
like a tag pushes as itself, where the line force-pushed another branch
over origin's, or failed. A branch name with a part longer than a disk
holds (a last part over 250 bytes, which git locks as<part>.lock) is
refused before anything exists byscaffold-project.py,family.py init
andadopt-project.py,--dry-runalike, where the first commit failed
with every remote made (#813, #689).--local-remote-dir. A--local-remote-dirwhose clone URL is not
text a README can hold, such as a directory named in a legacy code page,
is refused before any remote or folder exists,--dry-runalike, where
it ended in aUnicodeEncodeErrortraceback after the bare remotes were
made (#640, #433).family.py add --local-remote-dirrefuses a member
clone URL that the holder's.gitmodulescannot hold as text,
family-unwritable-value,--dry-runalike, before the member is read,
where it committed the URL raw andmake siblingsread another path
back. Each clone-URL remedy says that the URL is the path the flag
resolves to, so the directory to rename may be a link's target, or the
one a relative path was given from, and names a lone surrogate. The
READMEs the scaffold andfamily.py initwrite quote a local clone URL
that needs it for a POSIX shell, so thegit cloneline undera b/
pastes as one command (#812, #693).- Names that are not UTF-8. In
bump-leg.py,scaffold-project.pyand
family.py, a file name that is not UTF-8 ingit statusis
bump-leg-root-dirtyor the scaffold's DIRTY warning, and a
git config user.namethat is not UTF-8 is refused
family-unwritable-value, naming--created-by, where each ended in a
UnicodeDecodeErrortraceback (#641, #423, #426). - What
family.py initrecords and commits as. It asks whether
family.yamlcan hold the creator whichever of--created-by,
GIT_AUTHOR_NAMEorgit config user.namegave it, and the remedy names
that source; it asks git, before the remote exists, whether the holder's
first commit can be made with theGIT_AUTHOR_*andGIT_COMMITTER_*
values set (family-bad-commit-identity); and it refuses a--family
that is a Windows device name, such asCON(family-bad-holder-name).
--dry-runrefuses alike, where a creator holding a line break or a
control character was taken and a name git refuses failed after the
remote and the work directory existed (#809, #688). - A re-run's legs. A leg that
ghsees and git cannot read is refused
scaffold-remote-exists, with git's terminal prompt off, and the reuse
re-run's visibility lines go by each leg as it stands (#565, #542). --pin-source. A pin file recordsdigest_source: local-clone, never
the path, in the scaffold and inadopt-project.py, and a--pin-source
nothing reads is refusedpin-source-unused(#570, #540, #451, #450).- A
--pinnaming a tag. Inscaffold-project.pyand
adopt-project.py, a--pinthat names an annotated tag's own id pins
the commit the tag names, and one that names a tree or a blob is refused,
so a pin file no longer records a tag's id as its commit (#725, #651). - A derived id, named as derived.
scaffold-bad-idand
family-bad-idname an id derived from--projector--familyas
derived and say to change that flag, so--project North_Windno longer
sends the person to an--idthe naming policy then refuses (#671,
#659). - setup-project.py. Its paste lines quote each path, the scaffold's NEXT
STEPS wait for step (7), step (3) refuses a file above the clone,
-- --dry-runends at the plan, step (4) hears the pins passed to it, and
the upstream-organisation guard ignores case (#569, #538). - setup-project.py's checks before a run. Step (3) refuses a parent
folder nobody can write in and a drive that is not there, step (4)
verifies a chain from--pin-source,--pinno longer takes a flag as
its value, and step (5) says when a printed plan was refused (#623,
#608). - Pin chains. A bare
--link-sourceis the tree of the chain's first
link alone, and a source that names no path is refused (#576, #441,
#442). - A chain link no tree answers for. The root's
validate-manifest.py,
the naming gate and setup's step (4) each offer what that caller reads,
so neither the validator nor setup offers--link-source, which neither
takes, and the gate spells every--link-sourcevalue and path it prints.
The naming gate takes--link-source-labeland--link-remedy, so setup
passes its own flag and words for a holder (#744, #661). - One product, one spelling. A chain link, a declared pin and a
--link-sourcekey name one product exactly when they share a pin file,
so a tree declaringopenssno longer verifies a chain that says
openß, a chain naming both is two links and not a cycle, and
--link-source openss=no longer answers foropenß(#638, #444). - The YAML reader. The standard's own reader in
repo_shape.py, which
the tools use to readproject.yamland the pin files, refuses a plain
value opening with an indicator, an empty flow entry and a single pair in
a flow sequence, sok: ]is not the text],pins: [,a]is not
['a'], andk: [a: b]is not the string'a: b'(#635, #392, #393).
A value refused for opening with an indicator is named by its line, and
the refusal says to write it in double quotes, spelled as the reader
reads it back, or to reword it.plan --org @xand
scaffold-project.py --org @xstill name--org, now in the reader's
words and under its remedy, andcheckreports atracking_branch
holding a NUL or a lone surrogate once, asunsafe-value, without the
validate-manifest.pyfinding above (#644), which names no plan key
(#817, #708). - family.py.
addrefuses, before the member is read and on every disk,
any path under another spelling ofmembers/, with a remedy per entry
that never moves the members' checkouts (#574, #362, #363, #364).add
andremoverefuse a.gitmodulesthe work tree does not hold as the
index does (#571, #407).bump --toasksadd's duplicate-id question,
peels an annotated tag's own id asadd --atdoes, and says to push first
a commit the member does not hold (#568, #541). It also refuses a commit
that only the member's mount holds, reachable from no branch of the
member's remote, asbump-leg.pyrefuses its twin (#625, #613).
make siblingsasks about the family folder before it clones, clones each
member on its tracking branch, andfamily.py siblingsruns the holder's
own copy (#578, #543). bump --to, reachable or not.family.py bumpfetches every branch
of the member'sorigin, whatever fetch refspec the mount has, and
prunes the remote-tracking branchesoriginhas deleted, so a commit a
branch holds is no longer refused for a refspec narrowed by hand, and one
only a deleted branch held is no longer pinned. A commit only a tag of the
remote reaches is told that a tag is not a branch, and a mount with no
remote namedoriginis told so, where each was told the pin cannot be
fetched, or to fix the network (#795, #686).- family.py refuses before it writes.
add,bumpandremoverefuse,
before their first write, a merge, cherry-pick, revert, rebase or
git amin progress, a.gitmodules,members/orfamily.yamlthat
the sparse checkout leaves out, and a file namedmembers. Each failed
after writing and was rolled back, or was committed in the wrong place
(#626, #612). - More holder states, named.
add,bumpandremovename, before
their first write and--dry-runalike, agit bisectin progress, a
rebase directory that holds no rebase, non-cone sparse rules that hold
no pattern, a merge conflicted infamily.yamlitself, and amembers
file that HEAD holds and the work tree no longer does, each with the way
out that works there. They refuse afamily.yamlholding a change HEAD
does not, staged or not (family-manifest-uncommitted), which their
commit took in beside their own row, so abumpan earlier run staged
and did not commit is told to put back HEAD's copy and run again (#816,
#684). - The
switch -cremedy. Where an earlier act left the branch,
family.pyandvalidate-family.pysay to give the new branch another
name, or to go back to the old one only if it holds work not merged yet,
and the docs spell itfamily/add-<Project>(#637, #607). - A holder's gitlinks and its
.gitmodules. A gitlink that no section
of HEAD's.gitmodulesnames isvalidate-family.py's finding
member-gitmodules-absent,family.py removerefuses a holder with no
.gitmodulesbefore it deinitialises anything, and two gitlink remedies
name what they missed (#639, #611). validate-family.py's remedies, typed as printed. After thegit rm
thatmember-gitlink-unnamedprints, and before the commit, the finding
says the removal is staged and to commit it, and prints nogit rm,
where it printed the same command again, which then failed.
member-gitlink-absent's remedy puts the gitlink back by hand, in the
index at the commit the row pins, with its.gitmodulessection where
HEAD's gives none, where it named afamily.py addthat refuses the row.
Themvfor a directory under another spelling of the row's path moves
nothing where the disk takes the two names for one directory or the
directory holds a.git, so it no longer moves a member's own checkout.
In both validators, a shape-pinfiles:row whosepath:is absent,
empty or not text is the findingshape-pin-row, where it was reported
as the pathNone(#747, #681). The first and the last were known
limits of v1.0.0.- A family's name, printed.
make siblings,parkandresumeprint
the family's name and every member row, and no longer end in a traceback
on a name such as"a\ud83db"or on a dash under
PYTHONIOENCODING=ascii. The holder's README names its folder after the
repository, so--family Quill --name QuillWorkslays out as its README
says (#642, #454, #629).setup-project.pynames a family folder after
the holder's repository too, so theQuill/Quillthatfamily.py init --name QuillWorksmakes is detected with no flag (#703, #696). make siblings's inputs. The move it prints to put the holder in a
folder named after the family runs as typed past a staging name
something already holds, and says first to move aside a link to nothing
in its way. A member whoseproject:or url holds a NUL or a lone
surrogate is a finding,NOT AN ARGUMENT, in the dry run too, and the
members after it are still placed, where the run ended in a traceback. It
reads the holder's.gitmodulesthrough onegit config -z --list, as
validate-family.pyand the doctor do, so a url holding NEL or U+2028,
or a section namedmembers/a=b, gives the url git uses. Its warning
spells the holder, the folder and the family, and atracking_branch:
that is only a tag of the member's remote is said to leave the clone
DETACHED (#805, #690).- A member's gitlink under another spelling. Where a member's only
gitlink sits under another spelling of its row's path,family.py add's
refusal moves it back to that path in the index alone, so the printed
remedy no longer takes the member's gitlink away from its row (#670,
#646). - A
members/renamed withgit mv.family.py add's
family-members-dir-variantrefusal renames it back in two steps through
a name nothing holds,git mv -- Members members.renaming, then
git mv -- members.renaming members, which takes each checkout and each
section of.gitmoduleswith it on every disk, where the move back left
each section naming the other spelling. It says first to put back what
the disk lacks under it, and to stage, or put back, the work tree's
.gitmodules. A printedgit mv --sparsesays it needs git 2.38 or
later, and the move back'sgit rm --sparsegit 2.34, so no remedy fails
as typed (#767, #685). - A holder's
make bootstrapthat left a member unchecked-out. It no
longer saysfamily bootstrap okthere: a subset clone, a member with
update = none, a gitlink git stops at (a strayMembers/<P>beside
members/<P>, a member mounted by hand with no row, a.gitmodules
section with no url), a.gitmodulesthat git reads otherwise than
git configdoes (two sections for one path, a section name git
ignores, a value beginning with-), and a.gitmodulesa merge left
unmerged now fail, and the line names the cause and the cure. This
changes an exit code a script can see: ok became exit 1 (#682, #648).
shape-doctor.py
- Both ways in. A git repository that is not a shape root is told of
the adoption and the scaffold, and the adoption line ends
--org <your-org>whereverplancan read no organisation fromorigin
(#553, #545; #591). - What it writes, and its clone.
openRepoShape --doctorsays it writes
nothing into the repository but a--placement-planfile you name, keeps
its clone for the NEXT lines that name it, and refuses a second
--placement-planor a second path (#580, #546). - A validator's verdict. A row's verdict is a printed
FINDINGor
REFUSED, and any other non-zero exit is a crash, not a finding about the
project. A crash of the standard's own naming script isCANNOT ANSWER,
and a pass that printedWARNINGor note lines is anotethat quotes
them (#557, #410, #447, #448, #460). - A root that lost
repo_shape.py. Every validator the doctor runs
ignores site-packages, so a.pthfile in the user's or a virtualenv's
site-packages no longer stands in for thescripts/repo_shape.pya root
lost: themanifestrow is the gate's ownModuleNotFoundError, as in
CI (#636, #342). An own-copy validator row whose crash is that loss names
git -C <root> checkout HEAD -- scripts/repo_shape.pyas its NEXT, or
none where HEAD no longer holds it (#737, #697). - A family holder's
.gitmodules.--rooton a holder reads a member
url that is not UTF-8 asmake siblingsdoes, so a member mounted from a
legacy-code-page directory gets a verdict, where the doctor ended in a
UnicodeDecodeErrortraceback, exit 1 (#632, #432). It reads the file
through onegit config -z --list, so a section name holding a space or a
byte that is not UTF-8, or a url holding NEL or U+2028, gives the mount
url git andmake siblingsuse, under any locale (#700, #668). - The
membersrow. It says, member by member, whatmake siblings
does with the directory beside the holder, and spells every value it
quotes, so an ESC or a CR in a url no longer reaches the terminal (#705,
#669, #673). It saysmake siblingsplaces the clones only from a folder
named after the holder's repository (#624, #605). - NEXT lines. The
legsandmembersNEXT names every detached commit
it would move, and sends a mount with no.git, an unborn HEAD or no url
to a line that cures it, or to none (#559, #413, #414).manifest kinds
prints one command for its NEXT, and counts a manifest validated only at a
path its gate reads (#555, #548, #439). A conflicted shape pin's NEXT takes
whole each copy both sides hold as the standard wrote it, and a leg pin put
back from before its bump says to commit it and run thepinsrow's line
(#586, #547); it reads both sides' pins, so a copy that only the side not
taken has afiles:row for is named for a person to merge, and never
taken (#736, #687). - The
machinerow of a machine that is not ready names the preflight
checks that failed (#552, #549). - A partial clone of the standard. Run from one, the
shape currency
row asks, without fetching, whether the clone holds what the comparison
reads, and where it does not it is CANNOT ANSWER, exit 3, with the fetch
for a person to run;update-shape.pyrefuses a read that fails
(update-upstream-unreadable), never calling the copyboth(#742,
#683). - A naming policy that does not load. A checkout whose own
contracts/repository-naming.yamldoes not load is CANNOT ANSWER, exit 3,
where it wasINVALID (naming), exit 1, and the EXIT CODES list names an
unfinished merge (#701, #678). - The
pinsrow and REFUSAL AHEAD. The row keeps its gate where
update-shape.py checksays REFUSAL AHEAD beside a merged-away row, so it
no longer says theapplythatcheckprints drops the row when that
applyis refused, and theshape currencycounts name every state
checkcounts (#751, #676). placementand a leg'ssingle-repository.yaml. The row asks a
leg's top-levelsingle-repository.yamlthe staying-single record's
question only wherekind:makes it the record, asplanasks it
(#734, #691).- A signal to
openRepoShape --doctor. A signal sent to the command's
shell alone waits for the doctor, so its clone of the standard is never
removed whileshape-doctor.pyruns from it, and a doctor ended by a
signal leaves nothing behind (#740, #680).
Install, CI and the docs
- PyPI (#590, #599, #563) and the one-line install (#587, #564;
#598, #595), above. - The launcher in Git Bash. Its
machine:row findspython, a
drive-letter$OPENREPOSHAPE_REPOis read as a path, a Windows-spelled
$OPENREPOSHAPE_BIN_DIRis respelled or refused, and thetests-git-bash
job runsinstall.sh's cases from #598 too (#656, #649). $OPENREPOSHAPE_BIN_DIRon Windows.install.shrespells a
Windows-spelled$OPENREPOSHAPE_BIN_DIR, or refuses it, before it fetches
anything;--installrefuses one holding:or a newline, where it
placed the command and printed a line that cannot be used; and the
export PATH=...note single-quotes a directory holding a double quote,
a$, a backquote, a backslash or!(#726, #667).- CI on Linux only (#597), with a result for every landing on main
(#593, #592), and a Git Bash job on Windows (#596, #585). Every test that
runsgit cloneon a just-pushed local remote now clones it in a way git's
background maintenance cannot race (#554, #544), and the test that holds
the doctor's NEXT lines to no#comment tail reads each string whole
(#698, #679).
The test session turns git's automatic maintenance off, in command scope
and in a global config file that includes the person's own, so a test's
copy of a repository it has just committed in or pushed to no longer
meets a repack deleting its loose objects (#713, #645).
The fivegit submodule update --initruns that still cloned a test's
bare remotes by plain path, four of them the runs v1.0.0 listed among its
test-fixture limits, now read them through git's transport too, by the
url.<file URI>.insteadOfrewrite thatclone_just_pusheduses, now
built by one helper, and a pasted line's environment keeps the session's
own git settings (#748, #662). With #554 and #578, that closes v1.0.0's
limit on fixtures that clone a just-pushed remote by plain path.
The tests that need a file name or a branch that is not UTF-8 ask one
pair of helpers whether this system can carry one, and skip on Windows
and on a disk that refuses such a name, as macOS's does; the helpers are
held to Windows' answer on Linux, with itsos.fsencodeemulated (#723,
#663).
Tests now fail ifvalidate-pins.py's workflow scan reads a directory, a
dangling symlink or a looping symlink named.yml, or ifsiblings.py's
headers print the holder's root and folder raw (#706, #664).
scripts/render-cli-reference.pysetsPYTHONDONTWRITEBYTECODEfor the
--helpchildren it runs, so the generator leaves no.pycfiles in the
template tree, which v1.0.0 listed among its test-hygiene limits (#724,
#665). Comments and docstrings inadopt-project.py,shape-doctor.py
and a test say what the code beneath them does (#730, #666). - The docs. README.md and the handbook say how to install from PyPI,
first on native Windows, and that the one line installs git-filter-repo or
prints the command for the machine (#563), and that Git Bash runs the one
line, tested in CI but for a typedyes(#596, #585). Each handbook section
records the hash of every README section it reads, and README's line cap is
a base plus one file per raise intests/readme-cap.d/(#551, #519). The
by-hand setup commands dry-run the scaffold with the--visibilityand
--elected-bysetup passes, andapply --add's five refusals are listed by
their codes (#579, #550, #429). - The doctor's exit 3, listed whole. README.md and the handbook name
every reason the doctor cannot answer: a pin's commit the checkout lacks,
a partial clone missing what is compared, a read that fails, a validator
of its own that crashed or could not be run, and its own naming policy
not loading (#630, #683). - Three sentences made true. README.md and the handbook say that a
source's ownsingle-repository.yamlrecord arrives asleg: drop, that
the adopted Makefile'sCONTRACTS_DIRfollows the spec leg's mount, and
that a second setup run is refused at the plan (#627, #606). validate-pins.py's codes for a neutral-product pin. README.md and the
handbook say again that a digest that recomputes differently is the
findingneutral-pin-digest-mismatch(exit 1), whose absence v1.0.0
listed as a known limit, and that a declared pin file that is not there is
the refusalneutral-pin-missing(exit 2) (#630, #460).
Known limits
v1.0.0's known limits that this release does not
close still hold; this list is not all of them. Two that work on this
release named and left open:
- The README's and the handbook's
--intosentence forfamily.py init
gives its default, the directory you are standing in, but not that
initrefuses that default inside a checkout of the standard
(family-landing-in-standard) (#695). bump-leg.py --dry-runover a pin its own validators already refuse,
such as one with a stray tab line, exits 0 and prints theold -> new
line, where the real run exits 2bump-leg-validators-redand rolls
back. The dry run now says that it runs no validator (#746, #672).
New in v1.1.0:
- A root scaffolded or adopted with
--pin-sourcebefore v1.1.0 records
the source's local path indigest_source:in
contracts/<product>-pin.yaml. Editing it tolocal-cloneremoves the
path from the tip, and git history keeps it. - A NEXT line pasted as
python3 <clone>/adopt-project.py …runs outside
the package, so on a machine whose only git-filter-repo is the package's,
it is refusedfilter-repo-missing. The refusal says the package's
adopt-projecthas it. - The package on Windows: CI runs its tests on Linux only (#597). The
release's install check runs--version,--preflight,--doctor, the
four tools'--helpand, under PowerShell,openRepoShape --version
there, and no test scaffolds through it on Windows. - Git Bash on Windows: the preflight that the one line ends with cannot take
a typedyesthere, so none of its offers can be accepted, and CI runs
only the launcher's andinstall.sh's suites there. Git Bash'sln -s
makes a copy unlessMSYS=winsymlinks:nativestrictis set, and a copy of
the command has noopenRepoShape.refbeside it, so it follows main, not
the installed release: link it with that setting (Windows lets a
non-administrator make a symlink only in Developer Mode), or put
~/.local/binonPATH(#596, #585).
Kept as known limits by the checklist triage of the L4 lines on #457 and
#459, which Brett Heap approved at 2026-10-16T13:36:07Z; each line's text
stays on its issue (#630):
- Windows: adopting a source whose path holds a character NTFS refuses, such
as a TAB or a control character, is unverified there, because such a path
cannot be checked out (#457's L4-05). - Windows: adopting a source whose path is not UTF-8, such as
b'caf\xe9',
is unverified there, because such a path cannot be made (#457's L4-06). - Windows: the submodule plan of a source whose tree names hold a newline, a
TAB or a backslash is unverified there, because such a name is not kept in
a tree (#457's L4-07). - Windows: adopting a source that tracks symlinks is unverified there,
because they check out as plain files unlesscore.symlinks=true
(Developer Mode or an elevated shell) (#457's L4-08). - A root adopted before #471's fix, from a source that kept its own root
.gitattributes, has no line-ending block, so on Windows its pinned
copies still check out CRLF andmake bootstrapreports
shape-copy-drift(#457's L4-17). adopt-project.pycompares anoriginwith the source by Python's
casefold, which is broader than GitHub's ASCII-only folding, so a path
origin.../straße/Atlas.gitand--org strasseare taken for one
repository (#457's L4-43).checksaysplan okfor a source whose own repository is literally
None/<name>,False/<name>or0/<name>, thoughexecutestill
refuses it (#457's L4-45).- A test,
tests/test_adopt_ignored_shape_files.py, feeds
git hash-object --stdina link target as text, not bytes; harmless
today, because no target holds a newline or a non-ASCII byte (#457's L4-54). setup-project.pystrips only C0 control characters and DEL from a value
it prints, so C1 controls and direction-override characters still reach
the terminal (#459's L4-24).- The YAML reader is a subset by design, and still reads, where PyYAML
refuses or reads another thing,k: - a,k: [- ],k: [e?],k: [|a],
a block key that opens with an indicator, and a flow?a(#459's L4-156).
Kept as known limits by the checklist triage of the L1 lines on #457,
which Brett Heap approved at 2026-10-16T11:21:17Z; each line's text stays
on #457 (#457):
- The
--pin-sourcedigest reader runs git with lazy fetch on and the
terminal prompt allowed, so a partial clone can fetch and a private https
remote can prompt, where the help says it uses no network (#457's L1-02). update-shape.py apply --addand the doctor read no git attributes, so a
nested.gitattributesadded after adoption makes CRLF clones drift with
no finding that names the cause (#457's L1-08).- A setting passed only through a parent git's
-c, such as
core.attributesFile, reaches the split'sgit addbut notcheck's
re-encoding question, socheckpasses andexecuterefusesadopt-lost
after both legs exist (#457's L1-10). - A
transfer.hideRefsorreceive.hideRefspattern that covers the split
branch passescheck;executemakes both legs, and the split push is
then denied (#457's L1-13). - A tracking branch the source hides from
ls-remotepassescheck, and
executerefuses itplan-tracking-branch-missingbefore any leg
(#457's L1-14). - With
org:andadopt_branch:both outside the alphabet,check
refusesorgfirst andexecuterefusesadopt_branchfirst
(#457's L1-17). - Two branch names that differ "only by case" are judged with Python's
casefold, which also foldsßtossandſtos; how APFS, NTFS
and GitHub treat such pairs is not established (#457's L1-18). - Under
core.symlinks=false, a link at theshape/copy of a path the
root holds through a link above it gets a finding whose every fix leads on
toadopt-symlinks-off(#457's L1-29). - Under
core.symlinks=false,.github -> ciwithci -> ""reports only
plan-empty-symlinkforciand never names.github, so the fix takes
two rounds (#457's L1-30). - Under
core.symlinks=false,checkpasses a link above a directory the
root keeps (scripts -> ci), andexecutethen refuses
adopt-symlinks-off(#457's L1-31). - A failure of
check's probegit initother than anOSErrorstops it
with "a git or gh command failed" instead of a finding (#457's L1-32). - A url whose organisation is refused is a dead end:
https://example.invalid/café/Atlas.gitwith--org acmeis refused as
not the source, and the suggested--org caféis refusedunsafe-value
(#457's L1-40). - An
originthaturl.<base>.insteadOfrewrites to a local path reads as
a path, so nothing is compared (#457's L1-41). - A url's host is never checked, so a GitLab url is compared as GitHub's
(#457's L1-42). - A drive-relative
originsuch asC:foo/barreads as git'shost:path
form, though git on Windows reads it as a path (#457's L1-44). - Only
planprints a NOTE for anoriginthat is a path;checkand
executeprint none (#457's L1-45). https://github.com/acme//Atlas, with a doubled slash, names no
repository, so nothing is compared andplanexits 0 (#457's L1-46).- A path
originthat is not UTF-8 gets no NOTE (#457's L1-47). plandoes not follow a pathoriginto that repository's ownorigin,
which would catch more mistyped--orgvalues (#457's L1-48).- A submodule url naming an empty bare repository answers
ls-remote, so
checkpasses andexecuteverifies, and the recursive clone then fails
withupload-pack: not our ref(#457's L1-50). - An absolute local-path submodule url that answers on this machine, kept
in a leg pushed to GitHub, works only on machines that have that path, and
nothing notes it (#457's L1-51). - A path on a mounted network filesystem (NFS, a mapped drive) looks like
this disk to git and is asked; its spelling cannot tell it apart
(#457's L1-52). - The NOTE for a submodule url that is not asked gives its network reason
for a bare relative url such asdependency, which names no network
(#457's L1-53). - On Windows, a UNC
--local-remote-dir(\\server\share\remotes) makes
every relative submodule url resolve to a UNC path, which is never asked
and gets no NOTE (#457's L1-54). - A test,
tests/test_adopt_pins.py, writes its stubghwith
Path.write_text, so a CRLF emulation run fails one case; not a product
bug (#457's L1-60). - #618's guard test does not clear
GIT_ALLOW_PROTOCOL, so a caller's
environment could mask a missing guard; not a product bug (#457's L1-62).
Full list
- Merged pull requests: https://github.com/opensoft/openRepoShape/pulls?q=is%3Apr+is%3Amerged
- The
v1.1.0milestone: https://github.com/opensoft/openRepoShape/milestone/2