Skip to content
Open
59 changes: 48 additions & 11 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7797,6 +7797,16 @@ def _coverage_findings(entry_paths: list[str], tree_paths: list[str]) -> list[st
return findings


def _unanswered_finding(entry: dict) -> str:
"""`check`'s `plan-unresolved` for one entry whose `leg:` is null: its
path, `_spelled`, and the question it repeats, `_shown` (#328). The one
spelling of it, read by `_leg_findings` and by `_against_its_source`,
which `check` lists it from (#1197)."""
return (f"FINDING plan-unresolved: {_spelled(str(entry.get('path')))} "
"still has `leg: null`. The question was: "
f"{_shown(str(entry.get('question')))}")


def _leg_findings(plan: Plan, tree: list | None,
named_links: frozenset | set = frozenset(),
source: Source | None = None,
Expand Down Expand Up @@ -7855,9 +7865,7 @@ def _leg_findings(plan: Plan, tree: list | None,
leg = entry.get("leg")
path = _spelled(str(entry.get("path")))
if leg is None:
findings.append(
f"FINDING plan-unresolved: {path} still has `leg: null`. The "
f"question was: {_shown(str(entry.get('question')))}")
findings.append(_unanswered_finding(entry))
elif str(leg) not in LEG_VALUES:
findings.append(
f"FINDING plan-bad-leg: {path} declares leg "
Expand Down Expand Up @@ -10592,7 +10600,10 @@ def _entry_findings(plan: Plan, source: Source,
NUL byte among them (#388), which no answer in the plan
fixes: the fix is a commit in the source, and the fresh `plan` after it
takes back every answer given before, so they are listed before the
findings about those answers (`_source_path_findings`). Their
findings about those answers (`_source_path_findings`). (`check` lists
an unanswered entry before every finding here, with the plan's problems
against its source, as `execute` refuses it first: #1197,
`_against_its_source`.) Their
remediation is said once after the findings, with the others. The
same goes for each symlink whose target holds a NUL byte after its
first, or is longer than Linux takes (#908), as
Expand Down Expand Up @@ -14582,8 +14593,9 @@ def cmd_check(args) -> int:
# THE PLAN AGAINST ITS SOURCE, FIRST OF WHAT IT LISTS (#1113): each
# problem of `_against_its_source`, the one list `execute` refuses the
# first of, in its order and in this command's words, before the split
# branch and the entries: another repository, a refused name and a moved
# source. An unanswered entry is listed with the entries, after them.
# branch and the entries: another repository, a refused name, a moved
# source and an unanswered entry, each by its path (#1197), which is not
# listed again with the entries.
names = plan.names()
pins = set(plan.pins)
unmended = list(_against_its_source(plan, source, names, naming, pins))
Expand All @@ -14604,7 +14616,9 @@ def cmd_check(args) -> int:
local_remote_dir = getattr(args, "local_remote_dir", None)
entry_findings, remediation, notes = _entry_findings(plan, source,
local_remote_dir)
findings.extend(entry_findings)
# An unanswered entry the rule listed above is not listed again (#1197).
said = {line for problem in unmended for line in problem.found}
findings.extend(line for line in entry_findings if line not in said)
# What is printed once after the findings, in their order.
remediation = [fix for problem in unmended for fix in problem.fixes] + (
taken_remediation + remediation)
Expand Down Expand Up @@ -14810,10 +14824,29 @@ def _against_its_source(plan: Plan, source: Source, names: dict,
no plan path, and with `_own_name_remediation` saying nothing of legs,
which `check` never makes; a refused name with the rename-first remedy
only where the source is adopted in place under that name (#716); a
moved source as `check` said it; and an unanswered entry not here but
with the entries, each by its path (`_leg_findings`), after these.
moved source as `check` said it; and an unanswered entry each by its
path, as `_leg_findings` says it, which `check` then does not list
again with the entries.
The plan's `org:` and `id:` are asked before all four, by both
(`_checked_org_and_id`, #1020).

AN UNANSWERED ENTRY BEFORE WHAT `check` LISTS AFTER THE RULE (#1197).
`execute` refuses it here, before the plan's values, the split branch
and the entries; `check` listed it with the entries, after the split
branch taken (`plan-split-branch-taken`), the source's own paths
(`plan-unsafe-path`, `plan-empty-symlink`), the coverage
(`plan-uncovered`, `plan-empty-entry`, `plan-duplicate-path`,
`plan-covered-twice`), the leg paths (`plan-bad-leg-path`,
`plan-leg-path-collides`) and a bad `leg:` on an entry before it
(`plan-bad-leg`), so beside any of them the two named different
problems first. Listed here, it is first in both. Each of those is
mended in the plan, or by deleting a branch, and an answer given first
is kept; only a path the source must rename, or a symlink it must
mend, is mended by `plan` run again, which `check` listed first for
that reason (#303), and which `execute` has refused after an
unanswered entry since #168. Where the source has moved on, the moved
source is the rule's problem, and an unanswered entry is listed with
the entries, as before: the fresh plan asks it again.
"""
# The plan names the source, or `execute` acts on another repository
# (#499): said before the names' own findings, as `execute` refuses it.
Expand Down Expand Up @@ -14849,8 +14882,12 @@ def _against_its_source(plan: Plan, source: Source, names: dict,
_refuse_an_unrunnable_plan(plan, source)
except Refusal as exc:
if exc.code != "plan-stale":
# Listed by `check` with the entries, each by its path.
yield _Unmended(exc)
# Listed by `check` here, each entry by its path, as
# `_leg_findings` lists it, and not again with the entries
# (#1197).
yield _Unmended(exc, tuple(
_unanswered_finding(entry) for entry in plan.entries
if entry.get("leg") is None))
else:
# `commit:` is a plan value people and AIs edit: sliced first,
# so the spelling stays bounded, and `_shown`, so a real hash
Expand Down
14 changes: 8 additions & 6 deletions tests/test_adopt_entry_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,10 @@ def test_plan_asks_about_a_path_execute_refuses(adopter, tmp_path, name,
question saying `execute` will not extract the path as it is named, why,
and that `root` or `drop` is the only answer; it prints that question
with the paths still to answer. `check` and `execute` say of the plan as
written what they say of any entry still asked: `plan-unsafe-path`
(`leg: null`, judged so the person knows what `spec` would meet) and
`plan-unresolved`, and `execute` refuses before any leg exists."""
written what they say of any entry still asked: `plan-unresolved`, first
to both (#1197), and then `check`'s `plan-unsafe-path` (`leg: null`,
judged so the person knows what `spec` would meet), and `execute`
refuses before any leg exists."""
text_name = name.decode(UTF8, "surrogateescape")
in_the_file = in_the_file.replace(
"{byte}", adopter._spelled_character("\udce9").replace("\\", "\\\\"))
Expand Down Expand Up @@ -542,11 +543,12 @@ def test_plan_asks_about_a_path_execute_refuses(adopter, tmp_path, name,
found = [line for line in checked.stderr.splitlines()
if line.startswith("FINDING ")]
named = adopter._spelled(text_name)
assert found[0] == (
# The unanswered entry first, as `execute` refuses it (#1197).
assert found[0].startswith(f"FINDING plan-unresolved: {named} still has "
"`leg: null`. The question was: ")
assert found[1] == (
f"FINDING {UNSAFE}: {named} has leg: null, and `execute` will not "
f"extract it into a leg: {adopter.entry_path_problem(text_name)}")
assert found[1].startswith(f"FINDING plan-unresolved: {named} still has "
"`leg: null`. The question was: ")
assert len(found) == 2, found
with pytest.raises(adopter.Refusal) as caught:
adopter.cmd_execute(execute_args(tmp_path, plan))
Expand Down
Loading
Loading