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
166 changes: 131 additions & 35 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -1716,7 +1716,7 @@
"`git commit -- "
f"<paths>`, and {effect.format(verb=verb)}. No git command that "
"writes was run and nothing was written.",
"Remediation: " + _operation_exit(found))

Check failure on line 1719 in scripts/family.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Remediation: " 3 times.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaFoezyYteaKSq8rxwGy&open=AaFoezyYteaKSq8rxwGy&pullRequest=1201


def _unparsable_in_an_act(root: Path) -> str:
Expand Down Expand Up @@ -2282,15 +2282,95 @@
return str(here)


def _family_line(verb: str, holder: Path, *words: str) -> str:
"""`<python> <this file> <verb> --family-root <holder> <words>`: ONE line
of this file's own, as `init`'s NEXT STEPS and every remedy that names a
verb print it, each value spelled by `shell_quoted` for the reader's
shell (#486, #1127).

THE ONE SPELLING OF THESE LINES (#1127, RV1117's N2 and N4 on #1117).
`_add_command` printed the interpreter and this file's path from where
the person stands (`_family_script`); the remedies that named `bump`,
`remove` and `add` said a bare `family.py`, which runs only from
`scripts/` and only where the file is executable, and printed a row's
`project:` raw, two words where it held a space. A placeholder
(`<commit>`, `<org>/...`) is the caller's to add after the line, bare:
quoted, it would read as a value.

THE VERB IS A STRING AT EVERY CALL, and
`tests/test_readme_windows_quoting_sentence.py` reads each one: README
lists the verbs whose lines are quoted for the shell they are printed
in, and a call naming a verb it does not list fails there."""
return " ".join([PYTHON, shell_quoted(_family_script()), verb,
"--family-root", shell_quoted(holder),
*(shell_quoted(word) for word in words)])


def _cited(command: str) -> str:
"""`command` between backticks, as a remedy's prose names a command, or
bare where the command holds a backtick of its own (#1127, RV1117's
N6): a holder under a folder named with one closed the quotation inside
its path, so the text between the marks was half the command. Every
shell reads the line the same either way."""
return command if "`" in command else f"`{command}`"


def _off_the_tracking_branch(family: "Family", refuses: str,
suggested: str) -> str:
"""The clause a remedy puts before the line of this file it names, where
the refusal is said before `_checked_branch` asks the holder's branch
(#1127, RV1117's N1): `bump`'s `member-unknown`, and `add`'s
`member-already-present`. On the tracking branch, where a fresh clone
stands, the line that followed was refused `family-on-tracking-branch`.
So the clause says where the line runs and names the `switch -c` that
makes a branch, as the holder's `validate-family.py` does before the
`remove` its `member-duplicate-id` prints; the remedy ends with
`BRANCH_LEFT` (#607). In words true on any branch: the refusal is said
before the branch is read, and reading it here could refuse in its
place. `refuses` names the verb or verbs the branch is refused by and
says so: "`add` refuses", or "`bump` and `remove` refuse"."""
switch = _cited(_switch_command(family.root, suggested))
return ("on a branch other than the holder's tracking branch, which "
f"{refuses} ({switch} makes one), ")


def _add_command(holder: Path, org: str) -> str:
"""The `add` that grows the holder at `holder`, as `init` prints it:
ONE line, so it pastes into PowerShell too, with this file's path, the
holder's and the organisation each spelled by `shell_quoted` for the
reader's shell (#486). A holder under `my work/` used to print
`--family-root .../my work/Orbit`, two arguments to argparse.
`<Project>` is left for the person to fill in."""
return (f"{PYTHON} {shell_quoted(_family_script())} add --family-root "
f"{shell_quoted(holder)} --member {shell_quoted(org)}/<Project>")
reader's shell (#486), by `_family_line`, as every remedy's line is
(#1127). A holder under `my work/` used to print `--family-root .../my
work/Orbit`, two arguments to argparse. `<Project>` is left for the
person to fill in."""
return (f"{_family_line('add', holder)} --member "
f"{shell_quoted(org)}/<Project>")


def _unknown_members_repository(family: "Family", typed: str,
project: str) -> str:
"""The `--member` value of the `add` that `bump`'s `member-unknown`
names: `<org>/<Project>` with the owner filled in where it is known
(#1127, RV1117's N3), as ONE word for the reader's shell.

THE OWNER, IN THIS ORDER: the one the person typed, where `bump
--member` was given as `<org>/<Project>`; else `family.yaml`'s `org:`,
the organisation `init` made the holder in and its NEXT STEPS' `add`
names; else the blank `<org>`, which is left only where neither is
there. The blank pasted unfilled is a redirection in every shell, not a
value: `family.py` never runs, and where a file named `org` is in the
working directory the shell tries to create the path after it. A member
may live in another organisation than the family's (`MEMBER_RE`), so
the family's is the likeliest owner, not a certain one; an `add` of a
repository that is not there refuses before it touches the holder.
Either owner is used only where `<owner>/<project>` is a `--member`
`add` takes."""
typed = typed.strip()
org = family.data.get("org")
for owner in (typed.rpartition("/")[0], org):
if isinstance(owner, str) and MEMBER_RE.fullmatch(
f"{owner}/{project}"):
return shell_quoted(f"{owner}/{project}")
return f"<org>/{shell_quoted(project)}"


def _standard_checkout(start: Path) -> Path | None:
Expand Down Expand Up @@ -6460,17 +6540,25 @@
"""
held = family.holding(repository, project)
if held is not None:
# THE `bump` IT NAMES RUNS AS PRINTED (#927): `bump` requires
# `--family-root`, so the line names this holder, spelled by
# `shell_quoted` as `_add_command`'s is.
# THE `bump` AND THE `remove` IT NAMES RUN AS PRINTED (#927, #1127),
# each a `_family_line` with this holder's `--family-root` and the
# row's own name, quoted. Said before `_checked_branch`, so a person
# on the tracking branch is told the branch both verbs need first.
name = str(held.get("project"))
suggested = _suggested_branch("bump", str(held.get("id") or name))
raise Refusal(
"member-already-present",
f"{project} is already a member of {family.data.get('name')}: "
f"the row {held.get('project')} pins {held.get('repository')}",
"Remediation: `family.py bump --family-root "
f"{shell_quoted(family.root)} --member {held.get('project')} "
"--to <commit>` moves an existing member; `remove` takes it "
"out. Adding it twice is not a way to move it.")
"Remediation: "
+ _off_the_tracking_branch(family, "`bump` and `remove` refuse",
suggested)
+ _cited(_family_line("bump", family.root, "--member", name)
+ " --to <commit>")
+ " moves an existing member and "
+ _cited(_family_line("remove", family.root, "--member", name))
+ " takes it out. Adding it twice is not a way to move it. "
+ BRANCH_LEFT)
# Everything the mount's `--force` would otherwise let past git, but the
# cached store, which is asked below (#242).
_checked_mount_path(family.root, project, path,
Expand Down Expand Up @@ -6588,8 +6676,9 @@
if row is not None:
name = row["project"]
# THE `remove` AND THE `bump` IT NAMES RUN AS PRINTED (#1005, #927),
# each with this holder's `--family-root`, which both verbs require,
# as `member-already-present`'s `bump` does.
# each a `_family_line` with this holder's `--family-root`, which
# both verbs require, and the row's own name, quoted (#1127). A real
# `add` asks this after `_checked_branch`, so no branch is named.
raise Refusal(
"member-duplicate-id",
f"{project} is already a member of {family.data.get('name')} "
Expand All @@ -6599,14 +6688,14 @@
"project twice, which `validate-family.py` refuses",
"Remediation: a family pins each project once. If "
f"{repository} is {row.get('repository')} renamed at the forge, "
"`family.py remove --family-root "
f"{shell_quoted(family.root)} --member {name}` takes the old row "
"out and this `add` then pins it under the new name; `family.py "
f"bump --family-root {shell_quoted(family.root)} --member {name} "
"--to <commit>` moves the row that is there instead. Two "
"projects that record one id are a fault in one of their "
"`project.yaml` files, mended in that project, not in the "
"family.")
+ _cited(_family_line("remove", family.root, "--member", name))
+ " takes the old row out and this `add` then pins it under the "
"new name; "
+ _cited(_family_line("bump", family.root, "--member", name)
+ " --to <commit>")
+ " moves the row that is there instead. Two projects that "
"record one id are a fault in one of their `project.yaml` files, "
"mended in that project, not in the family.")


def _probe_member(url: str, protocol: list[str], repository: str,
Expand Down Expand Up @@ -8683,8 +8772,9 @@
return
repository = row.get("repository")
# THE `remove` IT NAMES RUNS AS PRINTED (#1005): `remove` requires
# `--family-root`, so the line names this holder, spelled by
# `shell_quoted` as `member-already-present`'s `bump` is (#927).
# `--family-root`, so the line is a `_family_line` naming this holder and
# the other row's own name, quoted, as every remedy's line is (#1127). A
# real `bump` asks this after `_checked_branch`, so no branch is named.
raise Refusal(
"member-duplicate-id",
f"{repository} at {commit[:12]} is already a member of "
Expand All @@ -8695,11 +8785,12 @@
"Remediation: a family pins each project once. Pass `--to` a commit "
f"of {repository} whose project.yaml records an id no other row "
f"records. If {repository} and {held.get('repository')} are one "
"project, `family.py remove --family-root "
f"{shell_quoted(family.root)} --member {held['project']}` takes the "
"other row out first. Two projects that record one id are a fault in "
"one of their `project.yaml` files, mended in that project, not in "
"the family.")
"project, "
+ _cited(_family_line("remove", family.root, "--member",
held["project"]))
+ " takes the other row out first. Two projects that record one id "
"are a fault in one of their `project.yaml` files, mended in that "
"project, not in the family.")


def _head_oid(root: Path, path: str) -> str | None:
Expand Down Expand Up @@ -9383,15 +9474,20 @@
row = family.member(project)
if row is None:
# THE `add` IT NAMES RUNS AS PRINTED (#1005): `add` requires
# `--family-root`, so the line names this holder, spelled by
# `shell_quoted` as `_add_command`'s is (#486). `<org>` is left for
# the person to fill in: nothing here says whose repository it is.
# `--family-root`, so the line is a `_family_line` naming this
# holder (#1127), with the member's owner where it is known
# (`_unknown_members_repository`). Said before `_checked_branch`, so
# a person on the tracking branch is told the branch `add` needs.
raise Refusal(
"member-unknown",
f"{project} is not a member of {family.data.get('name')}",
"Remediation: `family.py add --family-root "
f"{shell_quoted(family.root)} --member <org>/{project}` puts it "
"in the family first.")
"Remediation: "
+ _off_the_tracking_branch(family, "`add` refuses",
_suggested_branch("add", project))
+ _cited(_family_line("add", family.root) + " --member "
+ _unknown_members_repository(family, args.member,
project))
+ " puts it in the family first. " + BRANCH_LEFT)
project = str(row["project"])
commit = checked_value("--to", args.to).lower()
if not COMMIT_RE.match(commit):
Expand Down
18 changes: 10 additions & 8 deletions tests/test_bump_remedies_family_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from test_family import FAMILY, family, manifest # noqa: F401
from test_family_add_member_id import PINNED, RENAMED, renamed_copy
from test_family_add_refusals import offline # noqa: F401
from test_family_printed_quoting import copied_holder, host_words
from test_family_printed_quoting import copied_holder, family_lead, host_words

#: A commit id to put where a line says `<commit>` or `<sha>` when nothing is
#: run with it. It is NOT a fixture's `advanced`: that one pushes a branch to
Expand Down Expand Up @@ -108,12 +108,14 @@ def refused_as(code, family, holder, offline, tmp_path):
return result.stderr


def the_bump_line(stderr: str, lead: str = "") -> str:
"""The one `family.py bump ...` a remedy prints, from `bump` to the
closing backtick, with `lead` before `family.py` (a path, for
`bump-leg.py`'s)."""
found = re.findall(re.escape(f"`{lead}family.py ") + r"(bump [^`]*)`",
stderr)
def the_bump_line(stderr: str, lead: str | None = None) -> str:
"""The one `<lead>bump ...` a remedy prints, from `bump` to the closing
backtick. `lead` is what comes before `bump`: `bump-leg.py`'s
`scripts/family.py `, or by default `family.py`'s own, the interpreter
and the path to the file from where the run stood (`family_lead`,
#1127)."""
lead = family_lead() if lead is None else lead
found = re.findall(re.escape(f"`{lead}") + r"(bump [^`]*)`", stderr)
assert len(found) == 1, stderr
return found[0]

Expand Down Expand Up @@ -218,7 +220,7 @@ def test_a_bump_leg_remedy_that_names_family_bump_names_the_holder(
result = BUMP_LEG_CASES[code](project, scaffolded, tmp_path)
assert result.returncode == 2, result.stdout + result.stderr
assert f"REFUSED {code}: " in result.stderr
line = the_bump_line(result.stderr, "scripts/")
line = the_bump_line(result.stderr, "scripts/family.py ")
assert line == "bump --family-root <holder> --member <Project> --to <sha>"
# The holder as a shell is handed it: quoted, so that a basetemp whose
# path holds a space (`--basetemp="my work/bt"`) is still one word.
Expand Down
8 changes: 6 additions & 2 deletions tests/test_family.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

sys.path.insert(0, str(REPO / "scripts"))
from repo_shape import ( # noqa: E402
Refusal, load_yaml, shell_quoted, tree_digest,
PYTHON, Refusal, load_yaml, shell_quoted, tree_digest,
)
from shape_materialize import ( # noqa: E402
SHAPE_REPOSITORY, readme_shape_lines, readme_trailing_shape_line,
Expand Down Expand Up @@ -540,7 +540,11 @@ def test_add_refuses_a_spelling_of_a_member_the_family_already_pins(
assert result.returncode == 2, result.stderr + result.stdout
assert "REFUSED member-already-present" in result.stderr
assert "the row IRRS pins InkRouter/IRRS" in result.stderr
assert ("`family.py bump --family-root "
# The line as every remedy prints one of `family.py`'s own: the
# interpreter and the path that reaches the file from where the run
# stood, which is this process's (#1127).
script = shell_quoted(os.path.relpath(FAMILY.resolve()))
assert (f"`{PYTHON} {script} bump --family-root "
f"{shell_quoted(holder.resolve())} --member IRRS --to "
"<commit>`") in result.stderr
assert "mounting" not in result.stdout
Expand Down
15 changes: 9 additions & 6 deletions tests/test_family_add_member_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
)
#: The shell's reading of a printed line on this host: the holder `bump`
#: names is one quoted word of it, whatever its path holds (#927).
from test_family_printed_quoting import host_words
from test_family_printed_quoting import family_lead, host_words

#: The project the renamed copy is of, and the name it is added under.
PINNED = "IRRS"
Expand All @@ -71,10 +71,10 @@

DUPLICATE_ID_REMEDIATION = (
f"Remediation: a family pins each project once. If {ORG}/{{who}} is "
f"{ORG}/{PINNED} renamed at the forge, `family.py remove --family-root "
f"{ORG}/{PINNED} renamed at the forge, `{{lead}}remove --family-root "
"{holder} --member "
f"{PINNED}` takes the old row out and this `add` then pins it under the "
"new name; `family.py bump --family-root {holder} --member "
"new name; `{lead}bump --family-root {holder} --member "
f"{PINNED} --to <commit>` moves the row that is there instead. Two "
"projects that record one id are a fault in one of their "
"`project.yaml` files, mended in that project, not in the family.")
Expand All @@ -83,9 +83,11 @@
def duplicate_id_remediation(who: str, holder: Path) -> str:
"""What `add` tells the human to do about `member-duplicate-id`: the
`remove` and the `bump` it names each carry `--family-root` and the
holder, as both verbs require (#927, #1005)."""
holder, as both verbs require (#927, #1005), and each begins with the
interpreter and the path to `family.py` from where the run stood
(#1127)."""
return DUPLICATE_ID_REMEDIATION.format(
who=who, holder=shell_quoted(holder.resolve()))
who=who, holder=shell_quoted(holder.resolve()), lead=family_lead())


def renamed_copy(family, tmp_path: Path) -> Path:
Expand Down Expand Up @@ -174,7 +176,8 @@ def test_each_command_the_refusal_names_runs_on_the_row_it_names(
remotes = renamed_copy(family, tmp_path)
refused = offline.add(holder, RENAMED, remotes=remotes)
assert refused.returncode == 2, refused.stdout + refused.stderr
commands = re.findall(rf"`family\.py ({verb} [^`]*)`", refused.stderr)
commands = re.findall(rf"`{re.escape(family_lead())}({verb} [^`]*)`",
refused.stderr)
assert len(commands) == 1, refused.stderr
older = git("rev-parse", "main~1",
cwd=family["remotes"] / f"{PINNED}.git").stdout.strip()
Expand Down
8 changes: 5 additions & 3 deletions tests/test_family_bump_to_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from test_family_bump_to_a_tag import annotated, both_runs, peeled_line
from test_family_member_path import MEMBER, OTHER, older, raw
#: The shell's reading of a printed line, and of a refusal's `switch`.
from test_family_printed_quoting import host_words
from test_family_printed_quoting import family_lead, host_words
from test_family_tracking_branch import remedy_switch

PATH = f"members/{MEMBER}"
Expand All @@ -84,7 +84,8 @@ def duplicate_id_refusal(to: str, holder: Path) -> tuple[str, str]:
"""The two lines `bump --to <to>` is refused with when the member's
`project.yaml` at `to` records the neighbour's id. The `remove` the
remedy names carries `--family-root` and the holder, as `remove`
requires (#1005)."""
requires (#1005), after the interpreter and the path to `family.py`
from where the run stood (#1127)."""
return (
f"REFUSED member-duplicate-id: {ORG}/{MEMBER} at {to[:12]} is "
f"already a member of {NAME} under another name: its project.yaml "
Expand All @@ -94,7 +95,8 @@ def duplicate_id_refusal(to: str, holder: Path) -> tuple[str, str]:
"Remediation: a family pins each project once. Pass `--to` a commit "
f"of {ORG}/{MEMBER} whose project.yaml records an id no other row "
f"records. If {ORG}/{MEMBER} and {ORG}/{OTHER} are one project, "
f"`family.py remove --family-root {shell_quoted(holder.resolve())} "
f"`{family_lead()}remove --family-root "
f"{shell_quoted(holder.resolve())} "
f"--member {OTHER}` takes the other row out first. "
"Two projects that record one id are a fault in one of their "
"`project.yaml` files, mended in that project, not in the family.")
Expand Down
Loading
Loading