Skip to content
Merged
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,13 @@ order, and step 4 is the one you must not skip.
it is the human's word, not your convenience.
7. **Read the verification table back to them.** It accounts for every source
path at the source commit, by blob sha, in exactly one of {spec leg, code
leg, root tree} or as `drop`. If it exits non-zero, STOP and relay it: the
leg, root tree} or as `drop`. If it exits non-zero, STOP and relay it, and
take the way out THAT exit prints. Exit 1, a failed verification: the
source is untouched — nothing is deleted, `main` is where it was, the split
is on a branch — so the exit is to fix the plan and re-run into fresh legs.
is on a branch — so fix the plan and re-run into fresh legs. Exit 2 saying
`IS pushed` (GitHub only: `gh pr create` or the topic refused) comes after
both legs and the split are pushed and verified: finish with the command it
prints and never re-run into fresh legs, which abandons that pair.

`execute` opens a pull request, because these organisations are pull-request
only. Never suggest a direct push to the default branch, and never `--admin`.
Expand Down
117 changes: 98 additions & 19 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,25 @@

EXIT CODES: 0 done · 1 findings (a plan that does not check out; a
verification mismatch) · 2 a refusal — the question could not be asked.
`execute` has other exit 2s, once the leg repositories exist (a refused push,
a refused mount). Two of them are GitHub mode's alone, and both come AFTER both
legs and the split branch are pushed and the verification has passed: `gh pr
create` refused (`The branch IS pushed`, printed after the table) and the
topic would not set (`The split IS pushed and the pull request IS open`). Each
prints the command that finishes the job by hand, and that command is the way
out: a re-run into fresh legs abandons the pushed pair for a second. A
verification that fails outranks both, with exit 1 and no command to open the
pull request.
"""

from __future__ import annotations

import argparse
import contextlib
import datetime as _dt
import os
import re
import shlex
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -3283,6 +3294,20 @@ def _refuse_an_unrunnable_plan(plan: Plan, source: Source) -> None:
"goes missing.")


def _flush_stdout() -> None:
"""Flush stdout before a failure is written to stderr (#171, #177, #188).

`execute` announces each leg repository, each pushed leg and the split on
stdout. Piped, stdout is block-buffered and stderr is not, so without
this a log that merges the two (`> log 2>&1`) shows the failure BEFORE the
lines it is about. A stdout nobody reads any more, such as a closed pipe,
must not turn the report into a BrokenPipeError, so that error is
swallowed: the report on stderr is what the human needs.
"""
with contextlib.suppress(OSError):
sys.stdout.flush()


def _create_leg_remotes(plan: Plan, names: dict, repositories: dict,
urls: dict, tracking: str, local: bool) -> None:
"""(a) The two NEW repositories. The assembly root is never created here."""
Expand Down Expand Up @@ -3861,31 +3886,67 @@ def _build_the_legs(source: Source, names: dict, repositories: dict,
return leg_commits, leg_digests


def _pull_request_command(repositories: dict, names: dict, tracking: str,
branch: str, message: str) -> list[str]:
"""The `gh pr create` that opens the split's pull request. It carries its
own title and body, so it runs with no terminal to prompt on: this is what
`_open_the_pull_request` runs, and what `_open_it_by_hand` prints (#304)."""
return ["gh", "pr", "create", "--repo", repositories["assembly"],
"--base", tracking, "--head", branch,
"--title", f"Adopt the three-repository shape: "
f"{names['spec']} and {names['code']}",
"--body", message]


def _open_the_pull_request(repositories: dict, names: dict, tracking: str,
branch: str, message: str) -> bool:
"""(c′) The pull request, because these organisations are pull-request
only.

Returns False when `gh` refused, having printed why and the command that
finishes the job by hand — the branch IS pushed by the time this runs, so
the exit is never "start again". Split out of `cmd_execute` for #138.
Returns False when `gh` refused, having printed why. The branch IS pushed
by the time this runs, so the exit is never "start again". The command
that finishes the job by hand is `_open_it_by_hand`'s, which the caller
prints AFTER the verification and only once it has passed (#304): until
then nothing says the split is the one to open a pull request for. Split
out of `cmd_execute` for #138.
"""
try:
url = run(["gh", "pr", "create", "--repo", repositories["assembly"],
"--base", tracking, "--head", branch,
"--title", f"Adopt the three-repository shape: "
f"{names['spec']} and {names['code']}",
"--body", message])
url = run(_pull_request_command(repositories, names, tracking, branch,
message))
print(f" pull request {url}")
except CommandFailed as exc:
print(exc.loudly("opening the pull request"), file=sys.stderr)
print("The branch IS pushed. Open the pull request by hand:\n"
f" gh pr create --repo {repositories['assembly']} "
f"--base {tracking} --head {branch}", file=sys.stderr)
return False
return True


def _open_it_by_hand(repositories: dict, names: dict, tracking: str,
branch: str, message: str, passed: bool) -> None:
"""What a refused `gh pr create` leaves to do, said once `execute` knows
whether the split it pushed is the one the plan asked for (#304).

A split that PASSED verification gets the command that opens its pull
request, the same command `_open_the_pull_request` ran, with its title and
body, so that it also runs where there is no terminal to prompt on. One
that did not gets the opposite: the refusal printed above names the exact
command that was tried, and a person who sees only that, with findings
under it, may paste it. Stdout is flushed first. The table is on it and a
pipe holds it in a block buffer, so without the flush this would come
before the table it depends on in a merged log; a stdout that is closed,
`| head` and gone, must not take the command down with it (`_flush_stdout`).
"""
_flush_stdout()
if not passed:
print("No pull request was opened, and none should be: the split "
"above did not pass verification.", file=sys.stderr)
return
command = " ".join(shlex.quote(argument) for argument in
_pull_request_command(repositories, names, tracking,
branch, message))
print(f"The branch IS pushed. Open the pull request by hand:\n "
f"{command}", file=sys.stderr)


def _set_the_topic(repositories: dict, topic: str) -> bool:
"""(c″) The topic, on all three.

Expand Down Expand Up @@ -4107,19 +4168,31 @@ def cmd_execute(args) -> int:
return 2
split_commit, message = split

topics_failed = False
# A pull request `gh` refused does not end the run here: the legs and the
# split ARE pushed, and what the person needs to know before they open it
# is whether the split is right (#304). The topic is not tried after a
# refused pull request, because what it says is that the pull request IS
# open.
pull_request_refused = topics_failed = False
if not local:
if not _open_the_pull_request(repositories, names, tracking, branch,
message):
return 2
topics_failed = not _set_the_topic(repositories, topic)
pull_request_refused = not _open_the_pull_request(
repositories, names, tracking, branch, message)
if not pull_request_refused:
topics_failed = not _set_the_topic(repositories, topic)

# ---- (d) verification, by blob sha ------------------------------------
verified = _verify(source, assembly, work_root, names, paths_for,
split_commit, spec_path, code_path, seeded)
if pull_request_refused:
_open_it_by_hand(repositories, names, tracking, branch, message,
not verified)
elif not verified:
_next_steps(names)
if verified:
return verified # a verification mismatch outranks a missing topic
return 2 if topics_failed else 0
# A verification mismatch outranks a refused pull request and a
# missing topic.
return verified
return 2 if pull_request_refused or topics_failed else 0


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -4424,10 +4497,16 @@ def _verify(source: Source, assembly: Path, work_root: Path, names,
"another exit.", file=sys.stderr)
return 1
print("\nadoption verified: every source path is in exactly one place")
return 0


def _next_steps(names: dict) -> None:
"""What a person does once the split is verified AND its pull request is
open. Not said where there is none to review: after a refused `gh pr
create`, the command to open it is the next step (#304)."""
print(f"\nNEXT: review the pull request on {names['assembly']}, then\n"
f" git clone --recurse-submodules <{names['assembly']} url>\n"
" make bootstrap")
return 0


def _account_for(before: dict, after: dict, drops: list[str]) -> tuple[dict, list]:
Expand Down
Loading
Loading