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
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,25 +137,25 @@ py setup-project.py <Project> --org <your-org>
```

`python setup-project.py <Project> --org <your-org>` is the same command on a
machine with no `py`: Python from the Microsoft Store installs `python` and no
launcher at all.

machine with no `py`: Microsoft Store Python installs `python`, no launcher.
Two commands rather than one pipe, on purpose. Windows PowerShell 5.1 — still
the default shell on a stock install — re-encodes piped text as ASCII by
default (that is its `$OutputEncoding`), and writes UTF-16 when `>` redirects
to a file, so no byte above 0x7F survives either route and a script sent down
one is not the script that arrives; and a script arriving on stdin has an
exhausted stdin, so it cannot ask for the one `yes` before three repositories
exist — it refuses instead. Downloading the file first leaves stdin attached
to the terminal you are typing at.
one is not the script that arrives; and a script arriving on stdin leaves it
exhausted, so it cannot ask for the one `yes` before three repositories exist
and refuses. Downloading the file first leaves stdin on your terminal.

`setup-project.py` is the flow itself, and `setup.sh` a shim over it: the same
preflight, the same naming check, the same plan, the same one `yes`, the same
self-bootstrap outside a checkout. It never needs `make` and never looks for a
`python3` — it runs the interpreter that started it. That two-liner IS the
Windows way in; the `openRepoShape` command in step 3 below is a macOS, Linux
and WSL2 convenience with no Windows twin, because on Windows there is nothing
to install.
to install. Printed commands are quoted for a POSIX shell, so paste them into
Git Bash. Only `shape-doctor.py`'s lines, `setup-project.py`'s family lines
and `family.py`'s `NEXT`, `switch -c`, `add` and `git rm -r --cached` lines
are quoted for PowerShell, on Windows.

</details>

Expand Down
21 changes: 13 additions & 8 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@

<!-- reads README.md § preamble: blob ddadd23a162129f7e64fefce899105661440c05c -->

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<!-- reads README.md § The three legs: blob 6ebed796b7b135151ff3ca2c5280fb501e001acc -->

Expand Down Expand Up @@ -609,7 +609,7 @@ <h4>The one thing to read before electing it</h4>

<section id="start">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<p class="eyebrow">Setup</p>
<h2>Start here</h2>
Expand Down Expand Up @@ -722,7 +722,12 @@ <h4>The tool offers the rest</h4>
for <code>git</code>: a run refuses a missing <code>git</code> before its preflight starts.
There is no <code>openRepoShape</code> command to install here: that two-liner IS the
Windows way in, and the <code>openRepoShape</code> command is a macOS, Linux and WSL2
convenience with no Windows twin, because on Windows there is nothing to install.</p>
convenience with no Windows twin, because on Windows there is nothing to install.
Printed commands are quoted for a POSIX shell, so paste them into Git Bash. Only
<code>shape-doctor.py</code>&rsquo;s lines, <code>setup-project.py</code>&rsquo;s family
lines and <code>family.py</code>&rsquo;s <code>NEXT</code>, <code>switch -c</code>,
<code>add</code> and <code>git rm -r --cached</code> lines are quoted for PowerShell, on
Windows.</p>
</div>

</div>
Expand Down Expand Up @@ -1022,7 +1027,7 @@ <h4>What you have afterwards &mdash; the same on every platform</h4>

<section id="partway">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<p class="eyebrow">Setup</p>
<h2>If it fails part-way</h2>
Expand Down Expand Up @@ -1057,7 +1062,7 @@ <h2>If it fails part-way</h2>

<section id="ci">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<!-- reads README.md § The three legs: blob 6ebed796b7b135151ff3ca2c5280fb501e001acc -->

Expand Down Expand Up @@ -1115,7 +1120,7 @@ <h2>Reading private legs in CI</h2>

<section id="legs">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<!-- reads README.md § The three legs: blob 6ebed796b7b135151ff3ca2c5280fb501e001acc -->

Expand Down Expand Up @@ -1351,7 +1356,7 @@ <h3>The InkRouter example</h3>

<section id="doublepin">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<!-- reads README.md § The double pin, and the lockstep invariant: blob 57fffa08ffad5476f8eae73dd3e8494a02e050c9 -->

Expand Down Expand Up @@ -1648,7 +1653,7 @@ <h2>Is this repository compliant? <code>openRepoShape --doctor</code></h2>

<section id="park">

<!-- reads README.md § Starting a project in a new organisation: blob acdf65726ad6433d1d70386eb17f21b015673da0 -->
<!-- reads README.md § Starting a project in a new organisation: blob 95f06438cf8088e29bba6f56991898f304405f3b -->

<!-- reads README.md § Carrying in-flight work to another workstation: blob b84512ad39c09085dfe6bf7ea694017f5e28c1dd -->

Expand Down
43 changes: 34 additions & 9 deletions scripts/bump-leg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,25 @@ def _ensure_root_clean(root: Path) -> None:
"exists to keep readable.")


def _switch_command(root: Path, role: str, commit: str) -> str:
"""`git -C <root> switch -c bump/<role>-<commit[:12]>` as a line a person
pastes, every word quoted for a POSIX shell (#532).

THE ONE PLACE THE THREE `switch -c` LINES ARE SPELLED: the two refusals
below and the dry run's closing hint (#477) all name the same branch, and
the dry run's rule is that it names the branch the real run's refusal
names. Two of them concatenated `str(root)` bare, so a root under `My
Projects/` printed a line that ran in the wrong directory or did not
parse, and one under `it's here/` a quotation nothing closes; a Windows
root, which always holds a backslash, prints in single quotes, as
`shlex` spells it. They are lines for a POSIX shell (Git Bash, or WSL2
on Windows), as `update-shape.py`'s are, and README's "Windows,
natively" says so.
"""
return shlex.join(["git", "-C", str(root), "switch", "-c",
f"bump/{role}-{commit[:12]}"])


def _branch_to_bump_from(root: Path, project: "Project", role: str,
commit: str, *, dry_run: bool = False) -> str:
"""The branch on which the bump commits, refused if there is none or
Expand All @@ -1130,25 +1149,26 @@ def _branch_to_bump_from(root: Path, project: "Project", role: str,
next step would be a push to the default branch. A `--dry-run` makes no
commit, so on the tracking branch it is let through and ends by naming
the branch the real run needs (#477); a root on no branch at all is
refused either way.
refused either way. Both refusals name that branch's `switch -c`
through `_switch_command`, quoted (#532).
"""
branch = current_branch(root)
if branch is None:
raise Refusal(
"bump-leg-root-detached",
f"{root} is not on a branch, so there is no branch to push and "
"open a pull request from",
"Remediation: `git -C " + str(root) + " switch -c bump/"
+ f"{role}-{commit[:12]}` and re-run.")
f"Remediation: `{_switch_command(root, role, commit)}` and "
"re-run.")
if branch == project.tracking and not dry_run:
raise Refusal(
"bump-leg-on-tracking-branch",
f"{root} is on {branch!r}, which {MANIFEST} declares as this "
"project's tracking branch",
"Remediation: these organisations are pull-request only, so this "
"will not make a commit whose only next step is a push to the "
"default branch. Run:\n git -C " + str(root)
+ f" switch -c bump/{role}-{commit[:12]}\nand re-run.")
"default branch. Run:\n "
f"{_switch_command(root, role, commit)}\nand re-run.")
return branch


Expand Down Expand Up @@ -1357,10 +1377,12 @@ def _restore_leg_if_moved(submodule: Path, placement: str | None,
cwd=submodule).lower() == was.lower()):
restore_leg(submodule, placement, was)
if _leg_status(submodule) != status:
# QUOTED (#532): the leg's path is a root's, and a person pastes it.
where = shlex.join(["git", "-C", str(submodule), "status"])
raise LegNotAsFound(
"its `git status` no longer reads as it did before the bump, "
"which is what a checkout cut off part way leaves behind. Read "
f"`git -C {submodule} status` before anything else, and remove "
f"`{where}` before anything else, and remove "
"only what you did not put there yourself.")


Expand Down Expand Up @@ -1639,8 +1661,7 @@ def cmd_bump(args) -> int:
# The command is quoted, so a root under `My Projects/` pastes.
print(f"\nthe real run needs a branch: {root} is on {branch!r}, "
"the tracking branch. Create one first with: "
+ shlex.join(["git", "-C", str(root), "switch", "-c",
f"bump/{role}-{asked[:12]}"]))
+ _switch_command(root, role, asked))
return 0

# ---- the rewrite and the commit, remembered byte for byte so a failure
Expand Down Expand Up @@ -1669,7 +1690,11 @@ def cmd_bump(args) -> int:
print(f" {path}/ is left DETACHED at {commit[:12]}; `make bootstrap` in "
"the root puts it on its tracking branch AT the new pin, or says "
"why not")
print(f"\nNEXT git -C {root} push -u origin {branch}")
# QUOTED WHOLE (#532), the root and the branch: a root under `My
# Projects/` pasted as two arguments, and the line is the one this run
# ends on, the one a person runs next.
print("\nNEXT " + shlex.join(["git", "-C", str(root), "push", "-u",
"origin", branch]))
print(f" then open a pull request against {project.tracking}; never "
"push the bump to the default branch directly")
return 0
Expand Down
17 changes: 15 additions & 2 deletions shape-doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,16 +701,29 @@ def quote_arg(value, platform: str | None = None, *,
itself is still this function's alone -- one place that knows the
apostrophe is doubled, rather than a second copy of the rule beside the
one that needs it.

A WORD THAT BEGINS WITH `=` IS QUOTED IN `sh`'S DIALECT (#532). zsh, the
login shell macOS ships, expands a bare `=word` to the path of the
command `word` (its EQUALS option, on by default): `zsh -f -c 'echo =ls'`
prints `/usr/bin/ls`, and `=x` stops the line with `x not found`. bash
and dash read it as itself, and `=` anywhere but the front is itself to
every shell, so only a leading one is quoted. It stood in
`UNQUOTED_POSIX` as an ordinary character, and reached a line only as a
value that starts with one (a branch named `=x`), which is the gap #460
named. `repo_shape.shell_quoted` quotes it already (#486, #507), and
`tests/test_family_printed_quoting.py` holds the two quoters to one
answer. PowerShell's dialect is NOT changed: it expands no `=word`, so
`UNQUOTED_NT` keeps its bare `=`, as in any other place in a word.
"""
# ASCII first, so the alphabet check and the quoting below it both see
# what `Row.__init__` will (Copilot, PR #102); see the docstring above.
text = ascii_text(str(value))
windows = (os.name if platform is None else platform) == "nt"
# `.fullmatch`, not `.match`: a pattern ending in `$` still matches just
# before a trailing newline, so `.match` alone would call "Atlas\n" bare
# (Copilot, PR #102).
# (Copilot, PR #102). And a leading `=` is never bare for `sh` (#532).
if not always and (UNQUOTED_NT if windows else UNQUOTED_POSIX).fullmatch(
text):
text) and (windows or not text.startswith("=")):
return text
if windows:
# PowerShell's verbatim string: nothing inside is expanded, and the
Expand Down
8 changes: 5 additions & 3 deletions tests/test_bump_leg_dry_run_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ def test_the_real_run_on_the_tracking_branch_is_refused_byte_for_byte(
project, scaffolded, advanced):
"""The refusal's every byte, spelled out, and nothing on stdout: this is
the text the run had before #477, and a change to it is a change to what
a lane that scripts around it reads."""
a lane that scripts around it reads. The `switch -c` on its last line is
spelled as the dry run's hint is, by `shlex` (#532): a plain root prints
the bytes it always did, and one that needs quoting prints quoted."""
root = project.resolve()
before = snapshot(project)

Expand All @@ -198,8 +200,8 @@ def test_the_real_run_on_the_tracking_branch_is_refused_byte_for_byte(
"project.yaml declares as this project's tracking branch\n"
"Remediation: these organisations are pull-request only, so this "
"will not make a commit whose only next step is a push to the "
f"default branch. Run:\n git -C {root} switch -c "
f"bump/spec-{advanced[:12]}\nand re-run.\n")
f"default branch. Run:\n {switch_command(project, advanced)}\n"
"and re-run.\n")
assert_nothing_changed(project, before)


Expand Down
Loading
Loading