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
56 changes: 18 additions & 38 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@
materialize_assembly_root, naming_block, run, write_lf,
)
import shape_advisory # noqa: E402
# #172's shape half, shared with the scaffold since #197 (see that section
# below), under the names this file has always called it by.
from shape_materialize import ( # noqa: E402
MOVE_THE_LEG, first_and_more as _first_and_more, path_overlap as _overlap,
shape_path_collisions,
)

#: The naming policy this tool classifies leg names against. One constant,
#: because three spellings of the same path is how the second one goes stale.
Expand Down Expand Up @@ -1201,10 +1207,13 @@ def _plan_leg_paths(plan: Plan) -> tuple[tuple, tuple]:
# half for the `leg: root` answers it wrote itself, and refuses nothing for
# it: those answers are the person's to change.

#: Said where an overlap holds only once case is ignored.
CASE_ASIDE = " once case is ignored, as a macOS or Windows disk does"
#: The exit every collision has; a root-kept one has a second.
MOVE_THE_LEG = "choose another path for the leg"
# The question about `assembly_root_paths` -- `path_overlap`,
# `first_and_more`, `shape_path_collisions` and the phrases they share --
# moved to `scripts/shape_materialize.py` for #197, because
# `scaffold-project.py` asks it of its own leg paths. `_shape_collisions`
# below asks it with this tool's `COLLISION_DIR` and adds the `shape/` half,
# and `leg_path_collisions` adds the root-kept half: only an adoption has
# either.

#: Why a mount at or under `COLLISION_DIR` is refused where no copy would land
#: there (#172's review, P3-3): the directory is the shape's, whatever this
Expand All @@ -1225,41 +1234,12 @@ def _plan_leg_paths(plan: Plan) -> tuple[tuple, tuple]:
)


def _overlap(leg: str, other: str) -> tuple[str, str] | None:
"""How the mount path `leg` stands to the path `other`: `(relation,
aside)`, the relation `is`, `contains` or `is inside`, and the aside
`CASE_ASIDE` when it holds only once case is ignored. None when the two
do not overlap at all."""
for mine, theirs, aside in ((leg, other, ""),
(leg.casefold(), other.casefold(), CASE_ASIDE)):
if mine == theirs:
return "is", aside
if theirs.startswith(mine + "/"):
return "contains", aside
if mine.startswith(theirs + "/"):
return "is inside", aside
return None


def _first_and_more(paths: list[str]) -> str:
"""`a` or `a` and 2 more: one path named, the rest counted."""
more = f" and {len(paths) - 1} more" if len(paths) > 1 else ""
return f"`{paths[0]}`{more}"


def _shape_collisions(what: str, value: str) -> list[str]:
"""Where the mount path `value` overlaps what the shape writes."""
out: list[str] = []
files = [path for path in assembly_root_paths() if _overlap(value, path)]
if files:
relation, aside = _overlap(value, files[0])
them = "them" if len(files) > 1 else "it"
out.append(
f"{what} {value!r} {relation} {_first_and_more(files)}{aside}. "
f"The shape writes {them} into the assembly root, and `execute` "
f"would write {them} into the mounted leg or beside it under "
f"`{COLLISION_DIR}/`, leaving the root without {them}: "
f"{MOVE_THE_LEG}.")
"""Where the mount path `value` overlaps what the shape writes: the
paths of `assembly_root_paths`, asked through the definition this tool
shares with `scaffold-project.py` (#197), then this tool's own
`COLLISION_DIR`, which only an adoption writes into."""
out = shape_path_collisions(what, value, COLLISION_DIR)
beside = _overlap(value, COLLISION_DIR)
if beside:
relation, aside = beside
Expand Down
93 changes: 91 additions & 2 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
1. Validates the three names — `<Project>`, `<Project>-spec`,
`<Project>-code` — against `contracts/repository-naming.yaml` BEFORE it
creates anything, so a naming mistake costs a message rather than three
repositories and a rename.
repositories and a rename. A leg path that is, holds or lies inside a
path the shape writes into the assembly root (`contracts`,
`project.yaml`, a `--pin`'s `contracts/<product>-pin.yaml`) is refused
just as early, for the same reason (#197).
2. Creates the three remotes: `gh repo create` normally, or three BARE
repositories in a directory with `--local-remote-dir`, which is the test
path and touches no network.
Expand Down Expand Up @@ -60,6 +63,12 @@
stage_written,
)
from shape_advisory import scaffold_lines # noqa: E402
# The shape half of the leg-path collision rule, which `adopt-project.py`
# asks of its own mount paths (#172), imported as one unit (#197), with the
# one spelling of the file a `--pin` adds to what the shape writes.
from shape_materialize import ( # noqa: E402
assembly_root_paths, neutral_pin_path, shape_path_collisions,
)

#: The pin argument: `openGlass@<40 hex>`, optionally organisation-qualified.
PIN_ARG_RE = re.compile(r"^(?P<name>[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?)@(?P<commit>[0-9a-fA-F]{40})$")
Expand Down Expand Up @@ -340,6 +349,20 @@ def declared_pin_values(args) -> dict[str, dict[str, str]]:
return pins


def declared_pin_products(args) -> dict[str, None]:
"""The products `declared_pin_values` keys its pins by, in its order and
read by the same `parse_pin`, with no digest: so no `git` and no `gh`.

#197: `_build_assembly_root` hands those pins to the materializer, which
writes `neutral_pin_path(product)` for each, and the leg paths are asked
about that file before `_identity` and `_declaration` run, which is
before any digest is read. A malformed `--pin` is therefore refused
here, by `parse_pin`'s own `pin-malformed`, rather than in
`_declaration`; it was refused before anything was created either way.
"""
return dict.fromkeys(parse_pin(raw, args.pin_owner)[0] for raw in args.pin)


def _reusable_remotes(names: dict, urls: dict, repositories: dict,
local: bool, reuse_flag: bool) -> set[str]:
"""Which remotes already exist, and whether that is allowed.
Expand Down Expand Up @@ -392,6 +415,63 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool) -> str:
"--force.")


def _leg_collision_remediation(neutral_pins=()) -> str:
"""What a leg-path collision tells the person to do. The paths are
`assembly_root_paths(neutral_pins)`, the materializer's own list for this
run, never a copy of it: each `--pin` adds the file it writes."""
return (
"Remediation: nothing was created, and nothing here picks a path for "
"you, so pass another one yourself. A leg path neither is, holds nor "
"lies inside, whatever the case, a path the shape writes into the "
"assembly root ("
+ ", ".join(f"`{path}`" for path in assembly_root_paths(neutral_pins))
+ "). The defaults `spec` and `code` pass."
)


def _refuse_colliding_leg_paths(spec_path: str, code_path: str,
neutral_pins=()) -> None:
"""Neither leg is mounted over a path the shape writes into the assembly
root, or this refuses (#197).

`_build_assembly_root` writes the shape into the root FIRST and mounts
the legs after it, so a leg path the shape had just written was met only
by `git submodule add`. On main through 20e13f0, `--spec-path contracts`
and `--code-path project.yaml` planned under `--dry-run`, then created
all three repositories, pushed both legs, left the root empty and exited
2 (`'contracts' already exists and is not a valid git repo`).
`--code-path Makefile/code` died the same way, and `--spec-path
Contracts` exited 0 with a gitlink beside the shape's `contracts/`,
which a macOS or Windows disk keeps as one path. Against GitHub, each
left three repositories for a run that made no project.

THE QUESTION IS `adopt-project.py`'s (#172), asked through the one
definition both tools import: `shape_path_collisions` over
`assembly_root_paths()`, so a file the shape gains is refused here
without anybody editing this tool. It is asked with no collision
directory, because the scaffold hands the materializer none and writes
nothing under adopt's `shape/`.

A `--pin` IS ONE MORE PATH. `_build_assembly_root` hands the materializer
`neutral_pins=pins`, which writes `neutral_pin_path(product)` for each,
so `neutral_pins` is those products (`declared_pin_products`) and they
are asked about with the rest. Without them, `--pin openGlass@<sha>
--spec-path contracts/openglass-pin.yaml` planned and then made three
repositories, and `Contracts/OpenGlass-pin.yaml` exited 0 with a gitlink
beside the pin, one path on a macOS or Windows disk. With no `--pin`,
`contracts/openglass-pin.yaml` lies inside a directory the shape writes
into, not a path it writes, and it mounts.
"""
collisions = [sentence
for what, value in (("--spec-path", spec_path),
("--code-path", code_path))
for sentence in shape_path_collisions(
what, value, neutral_pins=neutral_pins)]
if collisions:
raise Refusal("scaffold-leg-path-collides", " ".join(collisions),
_leg_collision_remediation(neutral_pins))


def _elector(args) -> str:
"""(a) Whose act electing this shape was, or a refusal.

Expand Down Expand Up @@ -629,7 +709,7 @@ def _print_plan(args, values: dict, names: dict, repositories: dict,
print(f"pin {pin_values['PIN_REPOSITORY']} @ "
f"{pin_values['PIN_COMMIT'][:12]} tree "
f"{pin_values['PIN_TREE_SHA256'][:12]}… -> "
f"contracts/{product.lower()}-pin.yaml ({pin_values['PIN_DIGEST_SOURCE']})")
f"{neutral_pin_path(product)} ({pin_values['PIN_DIGEST_SOURCE']})")
_print_declared_classification(policy, names, declared_pins, chain,
link_pins, pins)
_print_plan_actions(args, values["TOPIC"], local)
Expand Down Expand Up @@ -861,6 +941,15 @@ def _scaffold(args) -> int:
args.code_path = checked_value("--code-path", args.code_path)
args.org = checked_value("--org", args.org)
args.pin_owner = checked_value("--pin-owner", args.pin_owner)
# THE LEG PATHS AGAINST THE SHAPE'S OWN, before anything else runs
# (#197): `_identity` may read git's config, `_declaration` asks `gh api`
# for a `--pin` with no `--pin-source`, `_leg_visibility` asks `gh repo
# view` about a reused root, and `--dry-run` returns only after all of
# them. Refused here, a dry run refuses exactly as the real run would,
# and no process has been started. Each `--pin`'s file is among the
# shape's paths, its product read off the argument by `parse_pin` alone.
_refuse_colliding_leg_paths(args.spec_path, args.code_path,
declared_pin_products(args))
project_id, display, elected_on, reference, elected_by = _identity(
args, project)

Expand Down
Loading
Loading