Slice 3: the PR flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -376,6 +376,200 @@ class Bot:
|
||||
|
||||
# ----- Per-RFC repo: seeding (test/dev fixtures, future graduation) -----
|
||||
|
||||
# ----- Per-RFC repo: PRs (§10) -----
|
||||
|
||||
async def open_branch_pr(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
head_branch: str,
|
||||
title: str,
|
||||
description: str,
|
||||
slug: str,
|
||||
supersedes_pr_number: int | None = None,
|
||||
) -> dict:
|
||||
"""Per §10.1: open a PR from a branch against main.
|
||||
|
||||
The PR's body carries the contributor's description, optional
|
||||
`Supersedes:` trailer for the §10.9 replay path, and the
|
||||
standard `On-behalf-of:` trailer per §6.5.
|
||||
"""
|
||||
body_lines = [description.strip()]
|
||||
if supersedes_pr_number is not None:
|
||||
body_lines += ["", f"Supersedes: #{supersedes_pr_number}"]
|
||||
body_lines += ["", _trailer(actor)]
|
||||
body = "\n".join(body_lines).strip()
|
||||
pr = await self._gitea.create_pull(
|
||||
owner,
|
||||
repo,
|
||||
title=title,
|
||||
body=body,
|
||||
head=head_branch,
|
||||
base="main",
|
||||
)
|
||||
_log(
|
||||
actor,
|
||||
"open_branch_pr",
|
||||
rfc_slug=slug,
|
||||
branch_name=head_branch,
|
||||
pr_number=pr["number"],
|
||||
details={
|
||||
"title": title,
|
||||
"supersedes": supersedes_pr_number,
|
||||
"repo": f"{owner}/{repo}",
|
||||
},
|
||||
)
|
||||
return pr
|
||||
|
||||
async def merge_branch_pr(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pr_number: int,
|
||||
head_branch: str,
|
||||
slug: str,
|
||||
) -> None:
|
||||
"""Per §10.5: no-fast-forward merge.
|
||||
|
||||
Gitea's `style='merge'` produces a merge commit; the
|
||||
per-acceptance commits from §8.6 remain individually reachable
|
||||
in main's history. The merge commit's body records the merging
|
||||
user via the `On-behalf-of:` trailer — the merge commit's
|
||||
author stays the bot (the bot is the only Git writer per §1)
|
||||
but the trailer carries the human accountability.
|
||||
"""
|
||||
subject = f"Merge branch '{head_branch}'"
|
||||
body = _trailer(actor)
|
||||
await self._gitea.merge_pull(
|
||||
owner,
|
||||
repo,
|
||||
pr_number,
|
||||
merge_message_title=subject,
|
||||
merge_message_body=body,
|
||||
style="merge",
|
||||
)
|
||||
_log(
|
||||
actor,
|
||||
"merge_branch_pr",
|
||||
rfc_slug=slug,
|
||||
branch_name=head_branch,
|
||||
pr_number=pr_number,
|
||||
details={"repo": f"{owner}/{repo}"},
|
||||
)
|
||||
|
||||
async def withdraw_branch_pr(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pr_number: int,
|
||||
head_branch: str,
|
||||
slug: str,
|
||||
reason: str = "withdraw",
|
||||
) -> None:
|
||||
"""Per §10.8: close the PR; do not delete the branch."""
|
||||
await self._gitea.close_pull(owner, repo, pr_number)
|
||||
_log(
|
||||
actor,
|
||||
"withdraw_branch_pr" if reason == "withdraw" else "supersede_branch_pr",
|
||||
rfc_slug=slug,
|
||||
branch_name=head_branch,
|
||||
pr_number=pr_number,
|
||||
details={"repo": f"{owner}/{repo}", "reason": reason},
|
||||
)
|
||||
|
||||
async def cut_resolution_branch(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
original_branch: str,
|
||||
resolution_branch: str,
|
||||
slug: str,
|
||||
) -> dict:
|
||||
"""Per §10.9: cut a fresh branch off main's tip into which the
|
||||
original branch's changes will be replayed. The bot owns the
|
||||
cut; the replay itself is a sequence of commit_accepted_change
|
||||
/ manual flush operations driven by the API layer."""
|
||||
created = await self._gitea.create_branch(
|
||||
owner, repo, resolution_branch, from_branch="main"
|
||||
)
|
||||
_log(
|
||||
actor,
|
||||
"create_resolution_branch",
|
||||
rfc_slug=slug,
|
||||
branch_name=resolution_branch,
|
||||
details={
|
||||
"repo": f"{owner}/{repo}",
|
||||
"original_branch": original_branch,
|
||||
},
|
||||
)
|
||||
return created
|
||||
|
||||
async def commit_replay_change(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
branch: str,
|
||||
file_path: str,
|
||||
new_content: str,
|
||||
prior_sha: str,
|
||||
original_change_id: int,
|
||||
original: str,
|
||||
proposed: str,
|
||||
reason: str,
|
||||
slug: str,
|
||||
) -> str:
|
||||
"""Per §10.9: a single replayed accept lands as its own commit on
|
||||
the resolution branch, so the §8.6 evidence shape is preserved.
|
||||
The subject mirrors `commit_accepted_change`'s but the body
|
||||
records the original change id so the resolution PR's
|
||||
conversation can stitch back to the original branch's chat."""
|
||||
subject = _subject_from_reason(reason, fallback="Replay change")
|
||||
body_lines = [
|
||||
"**Original:**",
|
||||
original.strip(),
|
||||
"",
|
||||
"**Proposed:**",
|
||||
proposed.strip(),
|
||||
]
|
||||
if reason and reason.strip():
|
||||
body_lines += ["", "**Reason:**", reason.strip()]
|
||||
body_lines += ["", f"Replayed-Change-Id: {original_change_id}"]
|
||||
body_lines += [_trailer(actor)]
|
||||
message = subject + "\n\n" + "\n".join(body_lines).strip()
|
||||
result = await self._gitea.update_file(
|
||||
owner,
|
||||
repo,
|
||||
file_path,
|
||||
content=new_content,
|
||||
sha=prior_sha,
|
||||
message=message,
|
||||
branch=branch,
|
||||
author_name=actor.display_name,
|
||||
author_email=actor.email or f"{actor.gitea_login}@users.noreply",
|
||||
)
|
||||
sha = result.get("commit", {}).get("sha") or result.get("content", {}).get("sha") or ""
|
||||
_log(
|
||||
actor,
|
||||
"replay_change",
|
||||
rfc_slug=slug,
|
||||
branch_name=branch,
|
||||
bot_commit_sha=sha,
|
||||
details={"original_change_id": original_change_id, "file_path": file_path},
|
||||
)
|
||||
return sha
|
||||
|
||||
# ----- Per-RFC repo: seeding (test/dev fixtures, future graduation) -----
|
||||
|
||||
async def ensure_rfc_repo_seed(
|
||||
self,
|
||||
actor: Actor,
|
||||
|
||||
Reference in New Issue
Block a user