feat: request teammate pull reviews (Closes #1330)
This commit is contained in:
1 parent
505a40d077
commit
1f950f93dd
8 files changed
+373
-3
No files matched your search
@@ -1962,6 +1962,72 @@ async def pull_handoff_candidates(repository: str) -> list[dict]:
|
||||
][:25]
|
||||
|
||||
|
||||
async def pull_review_candidates(repository: str, number: int) -> list[dict]:
|
||||
pull, candidates = await asyncio.gather(
|
||||
fetch(f"repos/{repository}/pulls/{number}"),
|
||||
pull_handoff_candidates(repository),
|
||||
)
|
||||
if not isinstance(pull, dict):
|
||||
raise ValueError("Gitea pull request response was not an object")
|
||||
excluded = {
|
||||
item["login"].casefold()
|
||||
for item in (pull.get("requested_reviewers") or [])
|
||||
if isinstance(item, dict) and isinstance(item.get("login"), str)
|
||||
}
|
||||
author = pull.get("user") if isinstance(pull.get("user"), dict) else {}
|
||||
if isinstance(author.get("login"), str):
|
||||
excluded.add(author["login"].casefold())
|
||||
return [
|
||||
item for item in candidates
|
||||
if item["login"].casefold() not in excluded
|
||||
][:25]
|
||||
|
||||
|
||||
async def request_assigned_pull_review(
|
||||
repository: str, number: int, reviewer: str, expected_head_sha: str
|
||||
) -> dict:
|
||||
login, pull = await _current_login_and_target(
|
||||
f"repos/{repository}/pulls/{number}"
|
||||
)
|
||||
head = pull.get("head") if isinstance(pull.get("head"), dict) else {}
|
||||
if (
|
||||
pull.get("state") != "open"
|
||||
or pull.get("merged") is True
|
||||
or not _login_in_users(login, pull.get("assignees"))
|
||||
or head.get("sha") != expected_head_sha
|
||||
):
|
||||
raise IssueNotAvailableError("Pull request is no longer eligible for review request")
|
||||
eligible = {
|
||||
item["login"] for item in await pull_review_candidates(repository, number)
|
||||
}
|
||||
if reviewer not in eligible:
|
||||
raise IssueNotAvailableError("Reviewer is not eligible")
|
||||
response = await _get_client().post(
|
||||
f"/api/v1/repos/{repository}/pulls/{number}/requested_reviewers",
|
||||
headers=_auth(),
|
||||
json={"reviewers": [reviewer]},
|
||||
)
|
||||
response.raise_for_status()
|
||||
confirmed_response = await _get_client().get(
|
||||
f"/api/v1/repos/{repository}/pulls/{number}", headers=_auth()
|
||||
)
|
||||
confirmed_response.raise_for_status()
|
||||
confirmed = confirmed_response.json()
|
||||
requested = [
|
||||
item["login"] for item in (confirmed.get("requested_reviewers") or [])
|
||||
if isinstance(item, dict) and isinstance(item.get("login"), str)
|
||||
] if isinstance(confirmed, dict) else []
|
||||
if reviewer not in requested:
|
||||
raise ValueError("Gitea did not confirm the requested reviewer")
|
||||
return {
|
||||
"repository": repository,
|
||||
"number": number,
|
||||
"head_sha": expected_head_sha,
|
||||
"requested_reviewers": requested,
|
||||
"reviewer": reviewer,
|
||||
}
|
||||
|
||||
|
||||
async def _change_assigned_pull_owners(
|
||||
repository: str, number: int, recipient: str | None
|
||||
) -> dict:
|
||||
|
||||
+60
@@ -1108,6 +1108,15 @@ class IssueHandoff(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class PullReviewRequest(BaseModel):
|
||||
reviewer: str = Field(
|
||||
min_length=1, max_length=255, pattern=r"^[A-Za-z0-9_.-]+$"
|
||||
)
|
||||
expected_head_sha: str = Field(
|
||||
min_length=7, max_length=64, pattern=r"^[A-Fa-f0-9]+$"
|
||||
)
|
||||
|
||||
|
||||
class IssueReassignment(IssueHandoff):
|
||||
expected_assignees: list[str] = Field(min_length=1, max_length=10)
|
||||
|
||||
@@ -6662,6 +6671,57 @@ async def handoff_assigned_pull(
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review-candidates")
|
||||
async def pull_review_candidates(
|
||||
owner: str, repo: str, number: int = PathParam(gt=0)
|
||||
) -> JSONResponse:
|
||||
repository = f"{owner}/{repo}"
|
||||
try:
|
||||
if not await gitea_proxy.is_assigned_pull(repository, number):
|
||||
raise HTTPException(status_code=404, detail="Assigned pull request not found")
|
||||
result = await asyncio.wait_for(
|
||||
gitea_proxy.pull_review_candidates(repository, number),
|
||||
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "Reviewers could not be loaded. Please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
return JSONResponse(result, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@app.post("/api/v1/repos/{owner}/{repo}/pulls/{number}/request-review")
|
||||
async def request_assigned_pull_review(
|
||||
request: PullReviewRequest,
|
||||
owner: str,
|
||||
repo: str,
|
||||
number: int = PathParam(gt=0),
|
||||
) -> JSONResponse:
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
gitea_proxy.request_assigned_pull_review(
|
||||
f"{owner}/{repo}", number, request.reviewer, request.expected_head_sha
|
||||
),
|
||||
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
|
||||
)
|
||||
except gitea_proxy.IssueNotAvailableError:
|
||||
return JSONResponse(
|
||||
{"error": "The pull request or reviewer changed. Reload before requesting review."},
|
||||
status_code=409,
|
||||
)
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "The review request could not be confirmed. Please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.patch("/api/v1/repos/{owner}/{repo}/pulls/{number}/release")
|
||||
async def release_assigned_pull(
|
||||
owner: str, repo: str, number: int = PathParam(gt=0)
|
||||
|
||||
Reference in new issue
Block a user