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
20 changes: 15 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ options:
folder, create no remote and push nothing
--local-remote-dir LOCAL_REMOTE_DIR
create bare repositories here instead of calling `gh
repo create` (the TEST path; no network)
repo create` (the TEST path; no network). An empty
value is refused as `local-remote-dir-no-path`, and
`.` means the directory the run stands in.
--no-push
--reuse-empty-repo if <org>/<Project>, <Project>-spec or <Project>-code
already exists and has ZERO commits, use it instead of
Expand Down Expand Up @@ -392,7 +394,9 @@ options:
top level, or a bare clone's: any other path is
refused as `pin-source-not-a-clone`.
--work-dir WORK_DIR where the working trees are built (default: a
temporary directory)
temporary directory). An empty value is refused as
`work-dir-no-path`, and `.` means the directory the
run stands in.
--no-next-steps leave out the closing NEXT STEPS (the clone, the cd
and `make bootstrap`): setup-project.py passes it,
because it runs those steps itself next. `Working
Expand Down Expand Up @@ -923,16 +927,22 @@ options:
slot.
--local-remote-dir LOCAL_REMOTE_DIR
create a bare repository here instead of calling `gh
repo create` (the TEST path; no network)
repo create` (the TEST path; no network). An empty
value is refused as `local-remote-dir-no-path`, and
`.` means the directory the run stands in.
--into DIR the PARENT directory the family folder is made in
(default: the directory you are standing in, which is
refused inside a checkout of openRepoShape: from its
root, pass `--into ..`). The holder lands at
<into>/<Family>/<Family>, beside where `make siblings`
clones the members.
clones the members. An empty value is refused as
`into-no-path`, and `.` means the directory the run
stands in.
--work-dir WORK_DIR THE OVERRIDE: land the holder at <work-dir>/<Family>
and make no family folder at all. Mutually exclusive
with --into.
with --into. An empty value is refused as `work-dir-
no-path`, and `.` means the directory the run stands
in.
--no-push
--dry-run print the plan and ask, as the real run does, whether
each folder it makes can be made (the family folder or
Expand Down
115 changes: 92 additions & 23 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@
)
# The leg-path rule `adopt-project.py` asks of its own paths (#179).
from shape_materialize import refuse_bad_leg_paths # noqa: E402
# And its question of one segment, asked of `--project`: the one function
# `family.py init` asks of `--family` (#688, #989).
from shape_materialize import refuse_an_unholdable_name # noqa: E402
# The root's visibility and the Free-plan secrets note, asked as `family.py`
# asks them of its holder (#660).
from shape_materialize import ( # noqa: E402
Expand Down Expand Up @@ -377,7 +380,10 @@ def main(argv: list[str] | None = None) -> int:
# parsed (`folder_flag_path`).
parser.add_argument("--local-remote-dir", default=None,
help="create bare repositories here instead of calling "
"`gh repo create` (the TEST path; no network)")
"`gh repo create` (the TEST path; no network). "
"An empty value is refused as "
"`local-remote-dir-no-path`, and `.` means the "
"directory the run stands in.")
parser.add_argument("--no-push", action="store_true")
parser.add_argument("--reuse-empty-repo", action="store_true",
help="if <org>/<Project>, <Project>-spec or "
Expand Down Expand Up @@ -430,7 +436,9 @@ def main(argv: list[str] | None = None) -> int:
"`pin-source-not-a-clone`.")
parser.add_argument("--work-dir", default=None,
help="where the working trees are built "
"(default: a temporary directory)")
"(default: a temporary directory). An empty "
"value is refused as `work-dir-no-path`, and `.` "
"means the directory the run stands in.")
parser.add_argument("--no-next-steps", action="store_true",
help="leave out the closing NEXT STEPS (the clone, "
"the cd and `make bootstrap`): setup-project.py "
Expand Down Expand Up @@ -972,31 +980,17 @@ def _refuse_bad_id(project_id: str, given: str | None, policy,
north_wind` was refused for its id, and the remedy's retype then met
`naming-unclassified`, which no `--id` mends. So a passed id is refused
only once `--project` is a name the naming policy holds as an assembly
root; one it does not is refused here as `_check_one_name` refuses the
assembly root's name, which `_check_declared_names` asks later, after
`_declaration`. The question is the policy's answer for the name in the
`assembly` role with nothing declared, and NOTHING DECLARED TURNS ITS NO
INTO A YES (#972's review, M1): `classify` answers None only when
`matches` finds no pattern; a form decided by its characters alone
(`UNAMBIGUOUS_FORMS`) carries the same role whatever is pinned; and a
pin makes a descendant form the assembly root only where the name also
satisfies the `project-leg/assembly` form, which it answers with no pin
too. So `Foo-Install` is refused here, and so are `Atlas-spec` and
`Atlas-code`, a leg's form with a pin or without: `--project Atlas-spec
--id foo_bar` was told to retype the id, and `--id foo-bar` then met
`naming-role-mismatch`. A name a pin can make the root, `MedxGlass`,
passes here, and every answer is asked with no process started. A
derived id keeps its own refusal: its remedy is already the naming
rule."""
root (`_refuse_a_name_no_root_can_have`), and then one a Windows disk
can make a folder of (`_refuse_an_unholdable_project_name`, #989):
`--project NUL --id foo_bar` is told the name, which no `--id` mends
either. A derived id keeps its own refusal: its remedy is already the
naming rule."""
if PROJECT_ID_RE.match(project_id):
return
pattern = PROJECT_ID_RE.pattern
if given is not None:
found = policy.classify(project, "assembly")
if found is None:
raise _unclassified(project)
if not accepts_role(found, "assembly"):
raise _role_mismatch(project, found, "assembly")
_refuse_a_name_no_root_can_have(policy, project)
_refuse_an_unholdable_project_name(policy, project)
raise Refusal("scaffold-bad-id",
f"--id {project_id!r} must match {pattern}",
PASSED_ID_REMEDIATION)
Expand All @@ -1006,6 +1000,77 @@ def _refuse_bad_id(project_id: str, given: str | None, policy,
DERIVED_ID_REMEDIATION)


def _refuse_a_name_no_root_can_have(policy, project: str) -> None:
"""`--project` is a name the naming policy holds as an assembly root, or
this refuses it as `_check_one_name` refuses the assembly root's name,
which `_check_declared_names` asks later, after `_declaration`. Asked
early only where another refusal would speak first otherwise: a passed
id that is no id (`_refuse_bad_id`, #911), or a name no Windows disk can
make a folder of (`_refuse_an_unholdable_project_name`, #989).

The question is the policy's answer for the name in the `assembly` role
with nothing declared, and NOTHING DECLARED TURNS ITS NO INTO A YES
(#972's review, M1): `classify` answers None only when `matches` finds
no pattern; a form decided by its characters alone (`UNAMBIGUOUS_FORMS`)
carries the same role whatever is pinned; and a pin makes a descendant
form the assembly root only where the name also satisfies the
`project-leg/assembly` form, which it answers with no pin too. So
`Foo-Install` is refused here, and so are `Atlas-spec` and `Atlas-code`,
a leg's form with a pin or without: `--project Atlas-spec --id foo_bar`
was told to retype the id, and `--id foo-bar` then met
`naming-role-mismatch`. A name a pin can make the root, `MedxGlass`,
passes here, and every answer is asked with no process started."""
found = policy.classify(project, "assembly")
if found is None:
raise _unclassified(project)
if not accepts_role(found, "assembly"):
raise _role_mismatch(project, found, "assembly")


#: What `scaffold-bad-project-name` is told to do (#989), in the words
#: `family.py init` gives `family-bad-holder-name` (#688), of what
#: `--project` names.
PROJECT_NAME_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. "
"Choose another --project. It names the assembly root: its repository, "
"the folder it is cloned into, its working tree and, under "
"--local-remote-dir, its bare repository `<Project>.git`; and a "
"root is cloned on Windows too, where no file or folder can be called a "
"device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`-`COM9`, `LPT1`-`LPT9`, "
"in any case, with or without an extension), and a name ending in a dot "
"or a space is made under another name. There is no --force.")


def _refuse_an_unholdable_project_name(policy, project: str) -> None:
"""`scaffold-bad-project-name`: a `--project` no Windows disk can make a
folder of (#989), before `_declaration`, the first place a `--pin` can
reach `gh`, so before anything exists and under `--dry-run` alike.

`--project NUL` and `Con`, each one CamelCase token, which is all the
naming policy asks, made the three repositories, the working trees and,
under `--local-remote-dir`, the bare `NUL.git`: a root no Windows disk
can clone, into a folder of its name. `family.py init` refused the same
`--family` as `family-bad-holder-name` (#688); the question is the one
function both ask (`shape_materialize.refuse_an_unholdable_name`). The
legs' names are not asked: a device name is the whole name, and
`NUL-spec` is a folder Windows makes.

THE NAMING REFUSAL SPEAKS FIRST, as `init` asks `_classify_family_name`
before its own: a name this refuses that the policy refuses too
(`NUL.git`, `Nul.`) is refused as the policy refuses it, whatever the
id, and only a name the policy holds as an assembly root is told it is
a device name."""
try:
refuse_an_unholdable_name(
"scaffold-bad-project-name", "--project", project,
names="the assembly root and the folders the scaffold makes "
"for it",
remediation=PROJECT_NAME_REMEDIATION)
except Refusal:
_refuse_a_name_no_root_can_have(policy, project)
raise


#: How `scaffold-topic-too-long` and `scaffold-org-too-long` are told to go
#: on (#838). A `--id` the person passed is theirs to shorten. One
#: `_identity` derived is `--project` lowercased, and an explicit `--id` is
Expand Down Expand Up @@ -2248,6 +2313,10 @@ def _scaffold(args) -> int:
_refuse_an_unwritable_clone_url(local, urls["assembly"])
policy = NamingPolicy.load(SHAPE_ROOT / "contracts" / "repository-naming.yaml")
_refuse_bad_id(project_id, args.id, policy, project)
# A NAME NO WINDOWS DISK CAN MAKE A FOLDER OF (#989), as `init` asks it
# of `--family`: before `_declaration` and before a passed id's alphabet,
# so the name speaks before the id, as it does in `_refuse_bad_id`.
_refuse_an_unholdable_project_name(policy, project)
# AND THE ALPHABET A COMMAND LINE'S VALUE IS HELD TO, after the id's own
# rule (#911), so `--id ''` is named for the id it is not; this still
# refuses what `PROJECT_ID_RE.match` lets through, a trailing line break.
Expand Down
43 changes: 25 additions & 18 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@
from shape_materialize import unwritable_reason # noqa: E402
# Whether `--family` names a folder a Windows disk can make (#688): the rule
# the scaffold and `adopt-project.py` hold a leg's mount path to, asked of a
# path of one segment.
from shape_materialize import _unmountable_reason # noqa: E402
# path of one segment, by the one function the scaffold asks of `--project`
# too (#989).
from shape_materialize import refuse_an_unholdable_name # noqa: E402
# How a refusal quotes the words git echoes of a value it refused (#688): as
# a path is quoted (#610), as it is when printable and as Python's `repr`
# spells it when it is not.
Expand Down Expand Up @@ -1027,20 +1028,19 @@ def _refuse_an_unholdable_holder_name(family: str) -> None:

THE QUESTION IS THE LEG PATH RULE'S (`_unmountable_reason`), the one the
scaffold and `adopt-project.py` ask of a leg's mount path, asked of a
path of one segment, in its own words. It is asked after
`_classify_family_name` on purpose: the policy's token holds no `.`, no
`~` and no space, so a device name is the only answer the rule can give
here, and the names it also refuses (a trailing dot or space, `.git`,
`git~1`, `.gitmodules`) are refused before it, as `unsafe-value`,
`family-bad-id` or `naming-unclassified`, as they always were.
path of one segment, in its own words, by
`shape_materialize.refuse_an_unholdable_name`, which the scaffold asks of
`--project` too (#989). It is asked after `_classify_family_name` on
purpose: the policy's token holds no `.`, no `~` and no space, so a
device name is the only answer the rule can give here, and the names it
also refuses (a trailing dot or space, `.git`, `git~1`, `.gitmodules`)
are refused before it, as `unsafe-value`, `family-bad-id` or
`naming-unclassified`, as they always were.
"""
reason = _unmountable_reason(family)
if reason:
raise Refusal(
"family-bad-holder-name",
f"--family is {family!r}, which names the holder and the folders "
f"`init` makes for it, and {reason}.",
HOLDER_NAME_REMEDIATION)
refuse_an_unholdable_name(
"family-bad-holder-name", "--family", family,
names="the holder and the folders `init` makes for it",
remediation=HOLDER_NAME_REMEDIATION)


def _read_member_project(path: Path, project: str, repository: str,
Expand Down Expand Up @@ -10520,18 +10520,25 @@ def build_parser() -> argparse.ArgumentParser:
# `_folder_flags` makes each a `Path`, refusing an empty one first.
init.add_argument("--local-remote-dir", default=None,
help="create a bare repository here instead of calling "
"`gh repo create` (the TEST path; no network)")
"`gh repo create` (the TEST path; no network). An "
"empty value is refused as "
"`local-remote-dir-no-path`, and `.` means the "
"directory the run stands in.")
init.add_argument("--into", default=None, metavar="DIR",
help="the PARENT directory the family folder is made "
"in (default: the directory you are standing in, "
"which is refused inside a checkout of "
"openRepoShape: from its root, pass `--into ..`). "
"The holder lands at <into>/<Family>/<Family>, "
"beside where `make siblings` clones the members.")
"beside where `make siblings` clones the members. "
"An empty value is refused as `into-no-path`, and "
"`.` means the directory the run stands in.")
init.add_argument("--work-dir", default=None,
help="THE OVERRIDE: land the holder at "
"<work-dir>/<Family> and make no family folder at "
"all. Mutually exclusive with --into.")
"all. Mutually exclusive with --into. An empty "
"value is refused as `work-dir-no-path`, and `.` "
"means the directory the run stands in.")
init.add_argument("--no-push", action="store_true")
init.add_argument("--dry-run", action="store_true",
help="print the plan and ask, as the real run does, "
Expand Down
32 changes: 32 additions & 0 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -4618,6 +4618,38 @@ def _unmountable_reason(text: str) -> str | None:
return None


def refuse_an_unholdable_name(refusal: str, flag: str, name: str, *,
names: str, remediation: str) -> None:
"""`name`, typed as `flag`, is a name a Windows disk can make a folder
of, or this refuses it as `refusal`: before anything exists, under
`--dry-run` alike, and with no process started.

ONE QUESTION FOR EVERY NAME A TOOL MAKES A FOLDER OF (#989). `family.py
init` asked it of `--family` since #688 (`family-bad-holder-name`), and
the scaffold never asked it of `--project`: `--project NUL` and `Con`,
each one CamelCase token, which is all the naming policy asks, made the
three repositories, the working trees and, under `--local-remote-dir`,
the bare `NUL.git`, a root no Windows disk can clone. Each tool asks it
here now, so the two cannot part again.

THE QUESTION IS THE LEG PATH RULE'S (`_unmountable_reason`), asked of a
path of one segment: a Windows device name, with or without an
extension, in any case (`CON`, `Nul`, `com1`, `NUL.git`); a name ending
in a dot or a space; one of Git's own names. Each tool asks it after its
naming policy has said what the name may hold, so in practice a device
name is the answer. A device name is the WHOLE name: `Nullable` and
`NUL-spec` are folders Windows makes. As `refuse_bad_leg_paths` does,
the caller gives its own code, the flag, what the name names (`names`,
the sentence's middle: "which names <names>, and <reason>.") and its own
remediation, so each tool refuses in its own voice."""
reason = _unmountable_reason(name)
if reason:
raise Refusal(refusal,
f"{flag} is {name!r}, which names {names}, and "
f"{reason}.",
remediation)


#: The templates the rule below reads its lines from: the shape's own
#: `templates/`, which both tools render a project from (`SHAPE_ROOT /
#: "templates"`, and this file is `SHAPE_ROOT/scripts/`). A test points it
Expand Down
15 changes: 15 additions & 0 deletions tests/test_derived_id_refusals.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,21 @@ def test_a_name_no_holder_can_have_speaks_before_a_passed_id(tmp_path, name,
assert "family-bad-id" not in stderr


@pytest.mark.parametrize("passed", ["foo_bar", ""])
def test_a_name_no_root_can_have_on_windows_speaks_before_a_passed_id(
tmp_path, passed):
"""The scaffold's half of the row above (#989): `--project NUL`, which
no Windows disk can make a folder of, is refused as
`scaffold-bad-project-name` before a passed id that is no id, as `init`
refuses `--family NUL` before its own; no `--id` mends it."""
stderr = run_tool(tmp_path, "scaffold", "NUL", f"--id={passed}")
assert stderr.startswith(
"REFUSED scaffold-bad-project-name: --project is 'NUL', which names "
"the assembly root and the folders the scaffold makes for it, and "), \
stderr
assert "scaffold-bad-id" not in stderr


def test_init_s_derived_remedy_rules_out_what_naming_not_a_family_refuses(
tmp_path):
"""#911 (U162): `--family open_glass` was told to change `--family` to
Expand Down
Loading
Loading