Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,12 @@ flag goes on the Python entry point —
which, run from a download, still clones the standard into a temporary
directory first, and from a checkout clones nothing. What it leaves behind is
`./Atlas` and three bare repositories in `./rehearsal`, both of which you can
delete. `openRepoShape --help` and `setup.sh --help` list every flag, this one
included; [`docs/cli.md`](docs/cli.md) is every tool's help and every
subcommand's in one file, generated from the tools themselves.
delete. `python3 setup-project.py --help` and, from a checkout, `setup.sh --help`
list every flag, this one included. From a download, `setup.sh --help` clones
nothing and lists none: it names `setup-project.py --help`, which answers
offline from that one file too, and [`docs/cli.md`](docs/cli.md), every
tool's help and every subcommand's in one file, generated from the tools
themselves.

Without installing anything — no fork, no manual clone — one command:

Expand Down Expand Up @@ -516,11 +519,11 @@ confers nothing and stays neutral.

### What setup.sh does

`setup.sh` is a shim — about seventy lines of code (twice that with its comments)
that find a Python 3.9 or newer, clone this standard into a temporary directory when
you are not already in a checkout of it, and hand over to `setup-project.py`, which
IS the flow. These are the commands that flow runs, in order, with no behaviour of
its own (the first only in self-bootstrap mode, from the one-liner above):
`setup.sh` is a shim that finds a Python 3.9 or newer, clones this standard into a
temporary directory when you are not already in a checkout of it, and hands over to
`setup-project.py`, which IS the flow. These are the commands that flow runs, in
order, with no behaviour of its own (the first only in self-bootstrap mode, from the
one-liner above):

```sh
git clone --depth 1 https://github.com/opensoft/openRepoShape.git <tmp-dir>
Expand Down
7 changes: 5 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Every command below is typed at the root of a clone of this repository. `openRep

### `setup.sh`

From anywhere but a checkout of the standard — a download, `curl … | bash` — `setup.sh --help` is the shim's own instead: a few lines that clone nothing, list no flag, and name `setup-project.py --help` and this file as where the flags are.

`./setup.sh --help`:

```
Expand Down Expand Up @@ -133,8 +135,9 @@ and setup.sh asks, if a terminal is attached. `--org` comes from that flag,
else $OPENREPOSHAPE_ORG, else a prompt — it is NEVER defaulted. Every other
argument reaches setup.sh unchanged: --visibility, --elected-by, --family,
--into, --yes, --local-remote-dir, and anything after `--`, which setup.sh
hands on to scaffold-project.py. `./setup.sh --help` prints setup-project.py's
usage, which lists them all.
hands on to scaffold-project.py. They are setup-project.py's flags:
`python3 setup-project.py --help` lists them all, and so does
`./setup.sh --help` in a checkout of this standard.

`--install` places ONE command, and it is this one file:

Expand Down
27 changes: 16 additions & 11 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -864,9 +864,12 @@ <h3>Rehearse first, creating nothing</h3>
instead and nothing touches the network at all.</p>
<p data-platform="win" class="aside">Run from a download it still clones this standard into a temporary
directory first; run from a checkout it clones nothing.
<code>openRepoShape --help</code> and <code>setup.sh --help</code> list every flag, this
one included; <code>docs/cli.md</code> is every tool&rsquo;s help and every
subcommand&rsquo;s in one file, generated from the tools themselves.</p>
<code>python3 setup-project.py --help</code> and, from a checkout,
<code>setup.sh --help</code> list every flag, this one included. From a download,
<code>setup.sh --help</code> clones nothing and lists none: it names
<code>setup-project.py --help</code>, which answers offline from that one file too,
and <code>docs/cli.md</code>, every tool&rsquo;s help and every subcommand&rsquo;s in
one file, generated from the tools themselves.</p>
</div></li>

<li><div>
Expand Down Expand Up @@ -929,11 +932,11 @@ <h4>Consent rules, on every platform</h4>

<div class="stack">
<h4>What <code>setup.sh</code> actually runs</h4>
<p>It is a shim &mdash; about seventy lines of code (twice that with its comments) that find
a Python 3.9 or newer, clone this standard into a temporary directory when you are not
already in a checkout of it, and hand over to <code>setup-project.py</code>, which IS the
flow. These are the commands that flow runs, in order, with no behaviour of its own (the
first only in self-bootstrap mode, from the one-liner):</p>
<p>It is a shim that finds a Python 3.9 or newer, clones this standard into a temporary
directory when you are not already in a checkout of it, and hands over to
<code>setup-project.py</code>, which IS the flow. These are the commands that flow runs,
in order, with no behaviour of its own (the first only in self-bootstrap mode, from the
one-liner):</p>
<div class="code" data-lang="sh"><pre><code>git clone --depth 1 https://github.com/opensoft/openRepoShape.git &lt;tmp-dir&gt;
python3 scripts/validate-repository-naming.py --explain Atlas Atlas-spec Atlas-code
python3 scaffold-project.py --org &lt;your-org&gt; --project Atlas --dry-run
Expand Down Expand Up @@ -1794,9 +1797,11 @@ <h2>For AI assistants</h2>
saying that some scaffold refusals name a value to change, added (#288);
<code>bump-leg.py</code> refusing a workflow file or the leg&rsquo;s pin that is not UTF-8, by
name, added (#289); <code>bump-leg.py</code> refusing a workflow file, the workflows directory
or the leg&rsquo;s pin that this user cannot open, by name, added (#383) &mdash; a page cannot
name the commit that contains it, and the blob below is the fact a test checks.
README.md blob e8da93817b0c1a195300800373a2895c0e0280e5. Every command, flag, exit code
or the leg&rsquo;s pin that this user cannot open, by name, added (#383); what
<code>setup.sh --help</code> does from a download, which clones nothing, and which commands
list every flag, added (#341) &mdash; a page cannot name the commit that contains it, and
the blob below is the fact a test checks.
README.md blob 5465d135ca7263010ee354424041f90c4553d2e7. Every command, flag, exit code
and file name on this page is quoted from that README and from AGENTS.md.</p>
</footer>
</main>
Expand Down
5 changes: 3 additions & 2 deletions openRepoShape
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ and setup.sh asks, if a terminal is attached. `--org` comes from that flag,
else $OPENREPOSHAPE_ORG, else a prompt — it is NEVER defaulted. Every other
argument reaches setup.sh unchanged: --visibility, --elected-by, --family,
--into, --yes, --local-remote-dir, and anything after `--`, which setup.sh
hands on to scaffold-project.py. `./setup.sh --help` prints setup-project.py's
usage, which lists them all.
hands on to scaffold-project.py. They are setup-project.py's flags:
`python3 setup-project.py --help` lists them all, and so does
`./setup.sh --help` in a checkout of this standard.

`--install` places ONE command, and it is this one file:

Expand Down
118 changes: 116 additions & 2 deletions scripts/render-cli-reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@
as in `tests/test_repo_hygiene.py`, because this is the one file in the
repository whose bytes come from running programs on somebody's machine.

A CHECKOUT OF THE STANDARD, AND NOTHING ELSE, IS RENDERED (#341). `setup.sh
--help` is `setup-project.py`'s usage only where the shim finds itself in a
checkout; anywhere else it answers with a short text of its own, and before
#341 it cloned the public upstream to answer at all. So a rendering made in a
download, a `git archive` or a release extraction reached GitHub and then
documented UPSTREAM's flags under this tree's heading, `--check` included.
`require_a_checkout` asks the shim's own question first and refuses, naming
the tree; and every child runs under `GIT_ALLOW_PROTOCOL=file`, so that if
the question ever stops matching the shim's, the clone fails closed instead
of reaching the network. A checkout clones nothing, so neither changes a
byte of `docs/cli.md`.

THE PROJECT-SIDE COPIES CANNOT PRINT THEIR HELP AS THEY STAND from a checkout
of this standard. Each of the six does `sys.path.insert(0, <its own
directory>)` and then `from repo_shape import …`, and `repo_shape.py` is beside
Expand Down Expand Up @@ -108,7 +120,12 @@ class Tool(NamedTuple):
Tool("setup.sh", "bash", "./setup.sh",
"The front door for a NEW project: preflight, the naming check, the "
"plan, one typed `yes`, then three repositories. A SHIM over "
"`setup-project.py`, so the help below is that tool's own."),
"`setup-project.py`, so the help below is that tool's own.",
note="From anywhere but a checkout of the standard — a download, "
"`curl … | bash` — `setup.sh --help` is the shim's own instead: "
"a few lines that clone nothing, list no flag, and name "
"`setup-project.py --help` and this file as where the flags "
"are."),
Tool("openRepoShape", "bash", "openRepoShape",
"The same front door as an installed command, plus `--install` "
"(itself, into `~/.local/bin`), `--preflight` (this MACHINE) and "
Expand Down Expand Up @@ -261,6 +278,13 @@ def child_env() -> dict:
to depend on which locales happen to be installed on whichever machine
runs this script next, which is exactly the noise this capture must not
have on one platform and not another.

`GIT_ALLOW_PROTOCOL=file` (#341), replacing whatever the caller had:
asking a tool for its help is never a network act, and no child here
clones anything from a checkout, which `require_a_checkout` insists on.
If that ever stops being true, git refuses the transport, the tool's
`--help` exits non-zero, and `capture_help` turns that into a refusal
naming the tool — the regression fails closed, offline.
"""
env = dict(os.environ)
env.pop("LINES", None)
Expand All @@ -275,10 +299,85 @@ def child_env() -> dict:
"NO_COLOR": "1",
"TERM": "dumb",
"LC_ALL": "C",
"GIT_ALLOW_PROTOCOL": "file",
})
return env


#: WHAT `setup.sh`'s `is_shape_checkout` ASKS FOR, read from the shim rather
#: than guessed: git's top level for the directory the shim sits in must hold
#: both of these files. `tests/test_cli_reference.py` reads the shim's probe
#: and fails if the two lists ever part.
SHAPE_CHECKOUT_FILES = ("scaffold-project.py",
"contracts/repository-naming.yaml")

#: The second half of every refusal `require_a_checkout` makes.
RENDER_FROM_A_CLONE = (
"Outside a checkout `setup.sh --help` is the shim's own short text, not "
"`setup-project.py`'s usage, so a rendering here would document the "
"wrong thing. Run this script in a `git clone` of the standard instead.")


def require_a_checkout(tree: Path = REPO) -> None:
"""Refuse, naming `tree`, unless it is the top level of a checkout.

THE SHIM'S OWN QUESTION, asked first: `git -C <tree> rev-parse
--show-toplevel` answers, and that top level holds every one of
`SHAPE_CHECKOUT_FILES`. Where it does not, `setup.sh --help` is not
`setup-project.py`'s usage, which is the text this script renders.

AND ONE QUESTION THE SHIM DOES NOT ASK: that the top level IS `tree`. A
copy of the standard nested inside another checkout of it passes the
shim's probe, and its `setup.sh --help` then prints the OUTER checkout's
`setup-project.py` usage — somebody else's flags under this tree's
heading. `os.path.samefile`, because git spells the path its own way
(forward slashes on Windows, a symlink resolved).
"""
try:
proc = subprocess.run(
["git", "-C", str(tree), "rev-parse", "--show-toplevel"],
env=child_env(), stdin=subprocess.DEVNULL, capture_output=True,
check=False)
except OSError as error:
raise Refusal(
f"cannot tell whether {tree} is a checkout of openRepoShape: git "
f"could not be run ({error}). {RENDER_FROM_A_CLONE}") from error
# THE PATH, BYTE FOR BYTE. Bash's `$(...)` in the shim's
# `is_shape_checkout` drops the trailing newlines and keeps every other
# byte, so a checkout whose directory name ends in a space, or is not
# UTF-8, is a checkout to the shim and has to be one here. `.strip()`
# would eat the space that belongs to the name, and a `replace` decode
# would turn a byte that is not UTF-8 into U+FFFD, naming a directory that
# is not there. `os.fsdecode` is how this process spells such a name to
# the file system; the `\r` goes with the newline, as `.strip()` took it,
# for a git that ends its lines the Windows way.
toplevel = os.fsdecode(proc.stdout).rstrip("\r\n")
if proc.returncode != 0 or not toplevel:
said = proc.stderr.decode("utf-8", "replace").strip().splitlines()
raise Refusal(
f"{tree} is not a checkout of openRepoShape: git finds no work "
f"tree there (a download, or a `git archive` or release "
f"extraction, has none){'; git said: ' + said[0] if said else ''}"
f". {RENDER_FROM_A_CLONE}")
missing = [name for name in SHAPE_CHECKOUT_FILES
if not (Path(toplevel) / name).is_file()]
if missing:
raise Refusal(
f"{tree} is not a checkout of openRepoShape: it lies in the git "
f"work tree {toplevel}, whose top level has no "
f"{' and no '.join(missing)}. {RENDER_FROM_A_CLONE}")
try:
same = os.path.samefile(toplevel, tree)
except OSError:
same = False
if not same:
raise Refusal(
f"{tree} is not the top level of a checkout of openRepoShape: it "
f"lies inside the checkout {toplevel}, so its `setup.sh --help` "
f"would print THAT checkout's `setup-project.py` usage. "
f"{RENDER_FROM_A_CLONE}")


def capture_help(tool: Tool, *subcommand: str) -> str:
"""One tool's `--help`, normalised.

Expand Down Expand Up @@ -548,7 +647,13 @@ def _refuse_host_paths(text: str) -> None:


def render(skip_bash: bool = False) -> str:
"""The whole document."""
"""The whole document.

`require_a_checkout` first, here as well as in `main`: the guard in
`tests/test_cli_reference.py` calls this directly, and a test suite run
in an extraction is a rendering made in one.
"""
require_a_checkout()
tools = [(title, _active_tools(group, skip_bash), blurb)
for title, group, blurb in GROUPS]

Expand Down Expand Up @@ -617,6 +722,15 @@ def main(argv: list | None = None) -> int:
"there and write nothing; exit 1 if they differ")
args = parser.parse_args(argv)

# A TREE THAT IS NOT A CHECKOUT IS REFUSED FIRST (#341), before the
# questions about this machine below: it is the same refusal on every
# machine, and a windows runner should hear it rather than the bash one.
try:
require_a_checkout()
except Refusal as refusal:
print(f"[!!] {refusal}", file=sys.stderr)
return 2

# `os.name` BEFORE `shutil.which`, and the order is the point:
# `tests/conftest.py::WINDOWS_SKIP` says at length that the `bash` on a
# stock Windows PATH is `C:\Windows\System32\bash.exe`, the WSL
Expand Down
10 changes: 5 additions & 5 deletions setup-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@

Two entry points that refuse different things in different words are two
standards, so #50 settled which of them is the flow. `setup.sh` is a shim now:
about seventy lines of code (twice that with its comments) that find an
interpreter, clone this standard when the person has no checkout of it, and
hand over here. There is ONE parser, one usage banner and one set of refusal
wordings, and `tests/test_setup_sh.py` drives them through bash where
`tests/test_setup_project_py.py` drives them through an interpreter alone.
it finds an interpreter, clones this standard when the person has no checkout
of it, and hands over here. There is ONE parser, one usage banner and one set
of refusal wordings, and `tests/test_setup_sh.py` drives them through bash
where `tests/test_setup_project_py.py` drives them through an interpreter
alone.

THE `setup.sh:NNN` CITATIONS BELOW NAME setup.sh AS IT STOOD BEFORE #50, the
500-line implementation this file was transcribed from. They are kept because
Expand Down
Loading
Loading