Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d6d1df2
conftest.py and four test files: one helper asks whether a system can…
Oct 16, 2026
0b1d1dd
test_bootstrap_branch_bytes.py: the no-fault test's Windows guard is …
Oct 16, 2026
19d126c
shape_materialize.py, scaffold-project.py and family.py: the folder r…
Oct 16, 2026
ab55c29
scaffold-project.py and family.py: `shown_source` joins the import bl…
Oct 16, 2026
326de72
bump-leg.py: the root's branch is read as its full ref and pushed as …
Oct 16, 2026
6ba8c5d
conftest.py and two test files: `skip_message` is held by a test that…
Oct 16, 2026
a5d473e
bump-leg.py: the branch read keeps a trailing no-break space, the dir…
Oct 16, 2026
cd0bf12
Merge lane 5's #731 head ab55c29f (fix/692-raw-folder-paths) as the s…
Oct 17, 2026
4b45c12
Merge lane 5's #746 head a5d473ee (fix/672-bump-leg-branch-and-push-l…
Oct 17, 2026
0e516bb
family.py, scaffold-project.py and bump-leg.py: `init`'s plan rows, s…
Oct 17, 2026
ef6db40
family.py and #766's tests: `family-holder-already-here` and `bump-le…
Oct 17, 2026
4a014a7
Merge #723's head (6ba8c5d2, fix/663-non-utf8-name-tests) into fix/67…
Oct 17, 2026
13f643d
Merge branch 'main' into fix/692-raw-folder-paths
brettheap Oct 17, 2026
4cdb427
Merge branch 'main' into fix/663-non-utf8-name-tests
brettheap Oct 17, 2026
2e3173b
Merge #723's final head 4cdb427c (landed as bad05d2)
Oct 17, 2026
a5139a3
Merge main bad05d21.. after #723 landed as bad05d2
Oct 17, 2026
a1c801f
Merge lane 5's #731 final head 13f643d3 (fix/692-raw-folder-paths), l…
Oct 17, 2026
b1aef06
Merge lane 5's #746 final head a5139a32 (fix/672-bump-leg-branch-and-…
Oct 17, 2026
92aa00b
Merge main fc867207 after #731 landed as d66a9054 and #746 as fc867207
Oct 17, 2026
6a5dc55
Merge main 6f64eea2 after #795 (#686) landed
Oct 17, 2026
b050dc7
family.py: one import of shown_path, now that #688 has landed beside …
Oct 17, 2026
b074e3b
Merge branch 'main' into fix/766-spelled-paths
brettheap Oct 17, 2026
4f6e7f8
Merge branch 'main' into fix/766-spelled-paths
brettheap Oct 18, 2026
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
11 changes: 9 additions & 2 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
from repo_shape import ( # noqa: E402
COMMIT_RE, NEUTRAL_PRODUCT_OWNER, PROJECT_ID_RE, TREE_DIGEST_DEFINITION,
VISIBILITY_CHOICES, NamingPolicy, Refusal, accepts_role, checked_value,
git_out, link_pins_from_trees, repo_basename, shell_line, tree_digest,
git_out, link_pins_from_trees, repo_basename, shell_line, shown_path,
tree_digest,
)
from shape_materialize import ( # noqa: E402
RULESET_HINT, SHAPE_REPOSITORY, CommandFailed,
Expand Down Expand Up @@ -552,6 +553,11 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,
always was, of `remote_is_empty` or `local_remote_is_empty`; the seed is
asked only of a leg that is not empty, and a leg git cannot read is
refused as one with commits, saying so (#542).

A LOCAL REMOTE IS NAMED AS `shown_path` NAMES A PATH (#766), as
`family.py init` names its holder's: it is a path under
`--local-remote-dir`, where `scaffold-unwritable-value` refuses a
control character first. A GitHub `<org>/<name>` is named as it is.
"""
reused: dict[str, str | None] = {}
with tempfile.TemporaryDirectory(prefix="openreposhape-seed-") as scratch:
Expand All @@ -573,7 +579,8 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,
continue
raise Refusal(
"scaffold-remote-exists",
f"{target} already exists" + ("" if local else " on GitHub")
f"{shown_path(target) if local else target} already exists"
+ ("" if local else " on GitHub")
+ ("" if empty else " and has commits"),
_exists_remedy(role, empty, reuse_flag, seed is not None,
unread))
Expand Down
27 changes: 16 additions & 11 deletions scripts/bump-leg.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
sys.path.insert(0, str(SHAPE_ROOT / "scripts"))
from repo_shape import ( # noqa: E402
COMMIT_RE, SAFE_PATH_RE, Refusal, checked_value, git_out, load_yaml,
recorded_gitlink, repo_basename, spelled, tree_digest,
recorded_gitlink, repo_basename, shown_path, spelled, tree_digest,
)
from shape_materialize import ( # noqa: E402
CommandFailed, captured_child_environment, check_program, commit_trailers,
Expand Down Expand Up @@ -1155,11 +1155,13 @@ def _ensure_root_clean(root: Path) -> None:
# `spelled`'s to show, and `strip()` took them off the last name and
# the leading space off the first line's ` M`, which then read as
# `M` (staged). git quotes a name with a control in it or a space at
# its end, so the last newline is all there is to take off.
# its end, so the last newline is all there is to take off. THE ROOT
# ABOVE THEM is named as `shown_path` names a path (#766): its name
# held a C1 control and a bidi override raw on the first line.
shown = dirt.decode("utf-8", "backslashreplace").rstrip("\n")
raise Refusal(
"bump-leg-root-dirty",
f"{root} has uncommitted changes:\n"
f"{shown_path(root)} has uncommitted changes:\n"
+ "\n".join(f" {spelled(line)}" for line in shown.split("\n")),
"Remediation: commit or stash them first. A lockstep commit that "
"cannot be read as exactly one change is the thing the rule "
Expand Down Expand Up @@ -1217,21 +1219,23 @@ def _branch_to_bump_from(root: Path, project: "Project", role: str,
the branch the real run needs (#477); a root on no branch at all is
refused either way. Both refusals name that branch's `switch -c`
through `_switch_command`, quoted (#532), and end with `BRANCH_LEFT`, the
sentence for a root that has that branch already (#647).
sentence for a root that has that branch already (#647). Each detail
names the root as `shown_path` names a path (#766), as the dirty root's
does.
"""
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",
f"{shown_path(root)} is not on a branch, so there is no branch "
"to push and open a pull request from",
f"Remediation: `{_switch_command(root, role, commit)}` and "
f"re-run.\n{BRANCH_LEFT}")
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",
f"{shown_path(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 "
Expand Down Expand Up @@ -1796,9 +1800,10 @@ def cmd_bump(args) -> int:
# LAST, after everything a dry run on a branch prints, so that
# line and the sentence under it are the whole of the difference
# between the two (#477, #647). 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: "
# under `My Projects/` pastes; the root before it is named as
# `shown_path` names a path, as the refusals name it (#766).
print(f"\nthe real run needs a branch: {shown_path(root)} is on "
f"{branch!r}, the tracking branch. Create one first with: "
+ _switch_command(root, role, asked))
print(BRANCH_LEFT)
return 0
Expand Down
98 changes: 69 additions & 29 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,14 @@ def _checked_branch(family: "Family", verb: str, suggested: str) -> str:
(#487). Asked by the real run, after the refusals about the arguments
and the holder and before the member is read or anything is written;
`--dry-run` asks `_branch_said` instead. The remedy's `switch -c` is
followed by `BRANCH_LEFT`, for a holder that has the branch (#607)."""
followed by `BRANCH_LEFT`, for a holder that has the branch (#607).

THE HOLDER IS NAMED AS `shown_path` NAMES A PATH (#766), as #692 names
the folders `init` makes: a holder whose path held an ESC sent it to the
terminal, and one with a line break began a line of its own choosing.
A printable path is named byte for byte. The remedy's `switch -c` is
a command to paste, and still holds the holder's path raw inside its
shell quotes (#459)."""
branch = _holder_branch(family.root)
if branch is not None and branch != family.tracking():
return branch
Expand All @@ -845,16 +852,17 @@ def _checked_branch(family: "Family", verb: str, suggested: str) -> str:
if branch is None:
raise Refusal(
"family-holder-detached",
f"{family.root} is not on a branch, so `{verb}` would write a "
"commit no branch holds, with nothing to push and open a pull "
"request from. No git command that writes was run and nothing "
"was written.", remedy)
f"{shown_path(family.root)} is not on a branch, so `{verb}` "
"would write a commit no branch holds, with nothing to push and "
"open a pull request from. No git command that writes was run "
"and nothing was written.", remedy)
raise Refusal(
"family-on-tracking-branch",
f"{family.root} is on {branch!r}, which {MANIFEST} declares as the "
f"holder's tracking branch, and `{verb}` will not write a commit "
"whose only next step is a push to the default branch. No git "
"command that writes was run and nothing was written.", remedy)
f"{shown_path(family.root)} is on {branch!r}, which {MANIFEST} "
"declares as the holder's tracking branch, and "
f"`{verb}` will not write a commit whose only next step is a push to "
"the default branch. No git command that writes was run and nothing "
"was written.", remedy)


def _branch_said(family: "Family", verb: str, suggested: str) -> list[str]:
Expand Down Expand Up @@ -1894,6 +1902,12 @@ def _refuse_landing_in_standard(into: Path, holder: Path,
checkout the relative path that reaches its parent from there, so the
printed flag lands the family beside the checkout wherever the person
stands. Asked before anything is created, under `--dry-run` alike.

THE THREE PATHS ARE NAMED AS `shown_path` NAMES A PATH (#766): the
holder, the checkout and where `--into ..` lands, each under the
directory the person stands in, whose name can hold an ESC or a line
break. `--into {parent}` is `..` and `..`s, or on Windows a path no
control character can be in, and is named as it is.
"""
checkout = _standard_checkout(into)
if checkout is None:
Expand All @@ -1906,15 +1920,16 @@ def _refuse_landing_in_standard(into: Path, holder: Path,
raise Refusal(
"family-landing-in-standard",
f"with neither --into nor --work-dir the holder lands where you are "
f"standing, at {holder}, which is inside {checkout}, a checkout of "
"openRepoShape itself (it holds scripts/family.py and "
"templates/family-root/). The family would be part of the "
"standard's tree, and `make siblings` would clone every member into "
"it too. Nothing was created, and `--dry-run` refuses the same.",
f"standing, at {shown_path(holder)}, which is inside "
f"{shown_path(checkout)}, a checkout of openRepoShape itself (it "
"holds scripts/family.py and templates/family-root/). The family "
"would be part of the standard's tree, and `make siblings` would "
"clone every member into it too. Nothing was created, and "
"`--dry-run` refuses the same.",
f"Remediation: pass `--into {parent}` to land it beside this "
f"checkout, at {beside / family / family}, or `--into <dir>` for any "
"other parent directory; `--work-dir <dir>` is the override "
"(`<dir>/<Family>`, no family folder). There is no --force.")
f"checkout, at {shown_path(beside / family / family)}, or `--into "
"<dir>` for any other parent directory; `--work-dir <dir>` is the "
"override (`<dir>/<Family>`, no family folder). There is no --force.")


def _landing(args, family: str) -> tuple[Path | None, Path]:
Expand Down Expand Up @@ -1948,12 +1963,15 @@ def _landing(args, family: str) -> tuple[Path | None, Path]:
# `os.path.isfile`, which answers False for a holder this user cannot
# search: `Path.is_file()` raised `PermissionError` there, a traceback
# before the question about the holder (`_refuse_a_taken_holder`) that
# names it (#609's rule: a refusal by name, never a traceback).
# names it (#609's rule: a refusal by name, never a traceback). The two
# paths are named as `shown_path` names a path (#766): `--into`, or the
# directory the person stands in, can hold an ESC or a line break.
if into.name == family and os.path.isfile(into / family / MANIFEST):
raise Refusal(
"family-holder-already-here",
f"{into} is already the {family} family folder and its holder "
f"is at {into / family}, so there is nothing to init",
f"{shown_path(into)} is already the {family} family folder and "
f"its holder is at {shown_path(into / family)}, so there is "
"nothing to init",
f"Remediation: `{_add_command(into / family, args.org)}` grows "
"it, and `make siblings` in the holder puts each member's "
"working clone beside it. There is no --force.")
Expand Down Expand Up @@ -2504,17 +2522,27 @@ def _print_init_plan(values: dict, repository: str, url: str,
and what a real run shows before it does anything.

Split from `cmd_init` for #136.

THE FOLDER AND THE HOLDER ARE NAMED AS `shown_path` NAMES A PATH (#766).
Both lie under `--into`, `--work-dir` or the directory the person
stands in, and the plan is on stdout before any refusal, so a name with
a line break and then `WARNING forged-line: pwned` printed that line as
a line of the plan, on a dry run and on a run refused after it, and an
ESC reached the terminal. A printable path is named byte for byte.
"""
print(f"family {values['FAMILY_NAME']} ({values['FAMILY_ID']})")
print(f"shape {SHAPE_REPOSITORY} @ {shape_commit[:12]} "
f"(tree {shape_tree[:12]}…)")
print(f"created by {values['CREATED_BY']} on {values['CREATED_ON']}")
print(f" holder {repository:<28} -> {url}")
if folder is None:
print(f"landing {holder} (--work-dir: no family folder)")
print(f"landing {shown_path(holder)} (--work-dir: no family "
"folder)")
else:
print(f"folder {folder} a plain folder, NOT a repository")
print(f"landing {holder} the holder clone, inside it")
print(f"folder {shown_path(folder)} a plain folder, NOT a "
"repository")
print(f"landing {shown_path(holder)} the holder clone, inside "
"it")
print(f"members mounted under {MEMBERS_DIR}/; none yet — "
"`family.py add` puts one there")
print("remotes " + ("a bare repository on disk (no network)" if local
Expand Down Expand Up @@ -2610,6 +2638,12 @@ def _checked_reuse(reuse_empty_repo: bool, local: bool, url: str,

Refused when the remote already exists WITH commits, or exists empty
without `--reuse-empty-repo`. Split from `cmd_init` for #136.

A LOCAL REMOTE IS NAMED AS `shown_path` NAMES A PATH (#766): it is a
path under `--local-remote-dir`. `_refuse_an_unwritable_clone_url`
refuses a control character there first, so only a character that is
not printable and not refused (a zero-width space) reaches this from
the command line. A GitHub `<org>/<Family>` is named as it is.
"""
# THE ONE REPOSITORY THAT MAY ALREADY EXIST is the holder, and only with
# --reuse-empty-repo, and only with ZERO commits. `InkRouter` in the
Expand All @@ -2622,7 +2656,8 @@ def _checked_reuse(reuse_empty_repo: bool, local: bool, url: str,
if not (reuse_empty_repo and empty):
raise Refusal(
"family-remote-exists",
f"{target} already exists" + ("" if local else " on GitHub")
f"{shown_path(target) if local else target} already exists"
+ ("" if local else " on GitHub")
+ ("" if empty else " and has commits"),
("Remediation: it has ZERO commits, so it is a reserved name "
"rather than a live repository — re-run with "
Expand Down Expand Up @@ -7765,6 +7800,11 @@ def _checked_index_unlocked(root: Path, path: str) -> None:
`.git/index.lock`. A lock taken after this check is the rollback's to
meet. The dry run does not ask: it writes nothing, and a lock is a fact
about the moment.

THE DETAIL NAMES THE LOCK AS `shown_path` NAMES A PATH (#766): it lies
under the holder, whose path can hold an ESC or a line break. The
remedy's `rm -f` is quoted for a shell, as it was, and still holds
the lock's path raw inside those quotes (#459).
"""
shown = _git_bytes(["rev-parse", "--git-path", "index.lock"],
root).decode("utf-8", "surrogateescape").strip()
Expand All @@ -7773,11 +7813,11 @@ def _checked_index_unlocked(root: Path, path: str) -> None:
return
raise Refusal(
"family-index-locked",
f"the holder's index is locked: {lock.as_posix()} exists. `git "
f"submodule deinit` takes no such lock, so `remove` would empty "
f"{path} and drop its registration and then fail at `git rm`, its "
"first write to the index; it was refused before either, and "
"nothing was written.",
f"the holder's index is locked: {shown_path(lock.as_posix())} "
"exists. `git submodule deinit` takes no such lock, so `remove` "
f"would empty {path} and drop its registration and then fail at "
"`git rm`, its first write to the index; it was refused before "
"either, and nothing was written.",
"Remediation: another git command is running in the holder, or one "
"that stopped left the lock behind. Let it finish and run the same "
"command again; if no git command is running, delete the lock (`rm "
Expand Down
Loading
Loading