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
184 changes: 155 additions & 29 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@
whole (#273). Where the source names a repository, `--org` and
`--project` name that one, compared without case, or `plan`
refuses them (`adopt-not-the-source`), and `check` finds and
`execute` refuses a plan edited to name another
(`plan-not-the-source`): in place, the source IS the assembly
root, and its own name is renamed first, on GitHub, never
retyped (#499). An `origin` that is a url names the
`org/repo` it ends in, with or without a trailing `/`; one that
is a path, in any separator, names none, and `plan` says it
`execute` refuses a plan edited to name another repository than
the source names, or, where it names none, than the plan
recorded for it (`plan-not-the-source`, #716): in place, the
source IS the assembly root, and its own name is renamed first,
on GitHub, never retyped (#499). An `origin` that is a url names
the `org/repo` it ends in, with or without a trailing `/`; one
that is a path, in any separator, names none, and `plan` says it
compared nothing (#521). An organisation read from the url and
refused is named by where it came from (#427).
check validates a plan against the source: each entry path bound for
Expand Down Expand Up @@ -10031,15 +10032,30 @@ def cmd_check(args) -> int:
another = _not_the_source(plan, source, names)
if another:
findings.append(f"FINDING {PLAN_NOT_THE_SOURCE}: {another}")
remediation.append(_own_name_remediation(source, naming, pins, ""))
remediation.append(_own_name_remediation(
_held_repository(plan, source), naming, pins, ""))
# THE SOURCE IS HANDED TO `_check_names` WHERE `execute` HANDS IT (#716):
# when the plan does not name another repository, so that a refusal of
# the assembly root's own name says to rename it first, as `execute`
# says it on the same plan. Where the plan does name another,
# `execute` stops at `plan-not-the-source` and never asks the names, and
# a refusal of the plan's name is then about a name that is not the
# source's: `_own_name_remediation` has said what to do.
renamed = (_rename_first(source.repository)
if source.repository is not None and not another else None)
try:
_check_names(naming, names, pins)
_check_names(naming, names, pins, None if another else source)
for role, name in names.items():
found = naming.classify(name, role, pins)
print(f" {role:<9} {name:<28} {found.family}"
+ (f"/{found.role}" if found.role else ""))
except Refusal as exc:
findings.append(f"FINDING {exc.code}: {exc.detail}")
# Only the rename-first remedy is printed, once after the findings:
# it is the one `check` was missing. `--project takes ...` is not,
# and `check` never printed it (#716).
if renamed is not None and exc.remediation == renamed:
remediation.append(renamed)

_print_what_will_happen(plan, source, names,
naming.topic_for(plan.project_id),
Expand Down Expand Up @@ -13248,9 +13264,20 @@ def _check_names(policy: NamingPolicy, names: dict[str, str],
# refuses before it asks the naming policy or any remote, a plan whose `org:`
# names another with its `legs.assembly:`, which is what `execute` acts on, or
# with its `project:`, as `plan-not-the-source`: a hand edit is caught too,
# whatever `org:` then holds, because it is read as `execute` reads it
# (`_plan_org`). A source that names no repository (no `origin`, or one that
# ends in no `org/repo`) is asked nothing, as before.
# because `org:` is read as `execute` reads it (`_plan_org`), as `str()` of
# what the file holds. One that `checked_value` refuses (a list, a mapping,
# an empty value, a space, a leading `-`, a character outside ASCII) is, in
# `execute`, `plan-not-the-source` where the plan is held to a repository
# (`_held_repository`, and the `org:` is not that repository's own) and
# `unsafe-value` where it is held to none. `check` asks `checked_value` of
# `org:` first (`_manifest_values`, and `_submodule_findings` where a kept
# submodule url is relative) and ends in `unsafe-value`, exit 2, before it
# compares anything; only where the plan's own names are refused, and no
# kept url is relative, does it reach the comparison with one (#716,
# L4-48). A source that names no repository (no `origin`, or one that ends
# in no `org/repo`) is asked nothing by `plan`, as before; `check` and
# `execute` hold the plan's names to what the plan recorded for it,
# `source: repository:`, where it recorded one (#716).
#
# AN `origin` THAT IS A PATH NAMES NO REPOSITORY (#521), in any separator
# (`url_repository`). It was read as its last two directories, so a clone of
Expand Down Expand Up @@ -13330,13 +13357,14 @@ def _path_origin_notes(args, source: Source) -> list[str]:
"repository on GitHub."]


def _own_name_remediation(source: Source, naming: NamingPolicy,
def _own_name_remediation(repository: str, naming: NamingPolicy,
pins: set[str], done: str) -> str:
"""What `adopt-not-the-source` and `plan-not-the-source` say to do:
`plan` again under the source's own name, or, where the naming policy
refuses that name, `_rename_first`. `done` is what the refusing command
left as it was, said first."""
repository = str(source.repository)
refuses that name, `_rename_first`. `repository` is the one the source
is held to (`_held_repository`), as text: where the live source names
none, the plan's record of it, and `str(None)` is no name (#716). `done`
is what the refusing command left as it was, said first."""
org, _, name = repository.partition("/")
try:
_check_names(naming, _names(name), pins)
Expand Down Expand Up @@ -13373,24 +13401,116 @@ def _refuse_another_repository(args, source: Source, naming: NamingPolicy,
"an in-place adoption keeps the source's own name, and `execute` "
"pushes the split to the repository they name, opens its pull "
"request there and sets its topic on it",
_own_name_remediation(source, naming, pins, "no plan was written. "))
_own_name_remediation(repository, naming, pins,
"no plan was written. "))


def _last_two_directories(path: str) -> str | None:
"""`<directory>/<name>` for a filesystem path, in either separator and
after a `:`, with one `.git` dropped from the name and the case kept;
None where the path has fewer than two (#716).

A SUPERSET OF WHAT v1.0.0 AND v1.0.1 READ. Their `_remote_repository`
took `[:/]([^/:]+/[^/]+?)(?:\\.git)?$` of a path `origin` (until #521),
which is its last two segments, split at `/` or `:`, one `.git` dropped
from the second: `/srv/a:b/Atlas.git` was `b/Atlas`, and
`/srv/clones/Atlas.git.git` was `clones/Atlas.git`. It read none for a
trailing `/`, a backslash path, or a relative one that starts with no
separator; this reads each as its last two directories too, which only
holds a plan to nothing more often, and that is what main does."""
segments = [part for part in re.split(r"[\\/:]", path) if part]
if segments and segments[-1].endswith(".git") \
and len(segments[-1]) > len(".git"):
segments[-1] = segments[-1][:-len(".git")]
return "/".join(segments[-2:]) if len(segments) >= 2 else None


def _held_repository(plan: Plan, source: Source) -> str | None:
"""The repository an in-place adoption by `plan` is held to, or None
where there is none (#716): the one the live source names
(`source.repository`), and where it names none, the one the plan
recorded for it, `source: repository:`.

A SOURCE WITH NO `origin` ONCE HELD A PLAN TO NOTHING. `plan` records
`repository: null` for it and compares nothing, which is right; but a
plan for acme/Atlas whose names were edited to `Atlsa`, with `origin`
then removed from the source, or given a copy of it that has none by
`--source`, checked `plan ok`, and `execute` went on to ask GitHub about
`acme/Atlsa`. The record is read as `_github_name` reads a name, and
never raises: one that is no text, or names no `owner/repo`, holds the
plan to nothing, as a null does, and `Plan.open_source` is the reader
that refuses one it must clone (`_recorded_repository`).

A RECORD THAT IS THE PATH'S OWN LAST TWO DIRECTORIES HOLDS NOTHING. v1.0.0
and v1.0.1 read a path `origin` as those directories (#521), so `plan
--org acme --project Atlas` on a clone of `/srv/clones/Atlas.git` was
refused with "`--org clones`" and, where the plan was written all the
same, recorded `repository: clones/Atlas`. A person who put `org: acme`
back by hand holds a correct plan, and holding it to that record
refused it, with a remedy that is the advice #521 took out. Where the
source's `origin` is a path (`_origin_path`), a record that is its last
two directories, as `_last_two_directories` reads them without case, in
either separator, is the old reader's and not the source's name: the
plan is held to nothing. Any other record is held, so a `git clone` of
the source, whose `origin` is a path, still holds a plan edited to
`Atlsa` to `acme/Atlas`."""
if source.repository is not None:
return source.repository
recorded = plan.source.get("repository")
held = _github_name(recorded) if isinstance(recorded, str) else None
path = _origin_path(source) if held is not None else None
if path is not None:
# Read as `held` was, so the two spellings are one name: v1.0.1
# records `clones/Atlas.git` for `/srv/clones/Atlas.git.git`.
old = _last_two_directories(path)
old = None if old is None else _github_name(old)
if old is not None and old.casefold() == held.casefold():
return None
return held


def _why_the_source_names_none(source: Source) -> str:
"""Why the live source names no repository, as `_not_the_source` says it
beside the plan's record (#716): it has no `origin`, or its `origin`
names none (a path, a url that ends in no `org/repo`, or one that is not
UTF-8: `_remote_repository`)."""
try:
git_out(["remote", "get-url", "origin"], cwd=source.path, binary=True)
except Refusal:
return "it has no `origin`"
return "its `origin` names no repository"


def _not_the_source(plan: Plan, source: Source,
names: dict[str, str]) -> str | None:
"""`plan-not-the-source`'s detail, or None: the plan's `org:` with its
`legs.assembly:` (`names`, read by `Plan.names`) or with its `project:`
names another repository than the one the source names (#499).

`org:` is read as `execute` reads it (`_plan_org`), whatever the file
holds. While it was compared only when it was text, a number, a
boolean, a null or a deleted `org:` line checked `plan ok`, and
`execute` acted on `2026/Atlas`, `True/Atlas` or `None/Atlas` (#513's
review, SHOULD-1). Where it is not text, the detail says what the file
holds beside how `execute` spells it. `project:` is read only as text:
a misread one is not this question's to answer, and `execute` acts on
`legs.assembly:`."""
repository = source.repository
names another repository than the one the source is held to
(`_held_repository`): the one it names (#499), or, where it names none,
the one the plan records for it (#716).

`org:` is read as `execute` reads it (`_plan_org`), as whatever text
`str()` makes of what the file holds. While it was compared only when
it was text, a number, a boolean, a null or a deleted `org:` line
checked `plan ok`, and `execute` acted on `2026/Atlas`, `True/Atlas` or
`None/Atlas` (#513's review, SHOULD-1). Where it is not text, the detail
says what the file holds beside how `execute` spells it. `project:` is
read only as text: a misread one is not this question's to answer, and
`execute` acts on `legs.assembly:`.

AN `org:` THAT `checked_value` REFUSES IS CAUGHT, BUT NOT ALWAYS AS
THIS (#716, L4-48): a list, a mapping, an empty value, one with a space,
a leading `-` or a character outside ASCII. `execute` asks this first,
so it refuses such an `org:` as `plan-not-the-source` where the plan is
held to a repository (`_held_repository`, and the `org:` is not that
repository's own), and as `unsafe-value`, from `_repository_urls`,
where it is held to none. `check` asks
`checked_value` of `org:` first (`_manifest_values`, and
`_submodule_findings` where a kept submodule url is relative) and ends
in `unsafe-value`, exit 2, before it compares anything; only where the
plan's own names are refused, and no kept url is relative, does it
reach this with one."""
repository = _held_repository(plan, source)
if repository is None:
return None
org, held = _plan_org(plan), plan.get("org")
Expand All @@ -13407,8 +13527,14 @@ def _not_the_source(plan: Plan, source: Source,
keys = " and ".join(f"`{key}: {_shown(value)}`" for key, value in wrong)
named = _and_listed(sorted({f"{_shown(org)}/{_shown(value)}"
for _, value in wrong}))
# Where the live source names none, the plan's own record is what it is
# held to, and the detail says so: the source did not say it (#716).
is_the = (f"the source is {_shown(repository)}"
if source.repository is not None else
f"the plan records the source as {_shown(repository)} "
f"({_why_the_source_names_none(source)})")
return (f"the plan's {said} with its {keys} names {named}, "
f"and the source is {_shown(repository)}: an in-place adoption "
f"and {is_the}: an in-place adoption "
"keeps the source's own name, and `execute` acts on the "
"repository `org:` and `legs.assembly:` name")

Expand All @@ -13426,7 +13552,7 @@ def _refuse_a_plan_for_another_repository(
raise Refusal(
PLAN_NOT_THE_SOURCE, f"{plan.path}: {detail}",
_own_name_remediation(
source, naming, pins,
_held_repository(plan, source), naming, pins,
"no leg repository was created and nothing was pushed. "))


Expand Down
Loading
Loading