feat: submit pull reviews from mobile My Work (#155)
CI / lint (pull_request) Successful in 11s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-07 02:01:18 +00:00
1 parent 7f5e671f8d
commit 9b81dce8aa
7 files changed
+443 -11

No files matched your search

+41
View File
@@ -14,6 +14,10 @@ REVIEW_DIFF_MAX_LINES = 400
_client: httpx.AsyncClient | None = None
class StaleReviewError(ValueError):
"""Raised before mutation when a pull request head changed during review."""
def _auth() -> dict[str, str]:
headers: dict[str, str] = {"Accept": "application/json"}
if GITEA_TOKEN:
@@ -416,6 +420,43 @@ async def pull_review_detail(repository: str, number: int) -> dict:
}
async def submit_pull_review(
repository: str,
number: int,
expected_head_sha: str,
decision: str,
body: str,
) -> dict:
base = f"repos/{repository}/pulls/{number}"
pull = await fetch(base)
head = pull.get("head") if isinstance(pull, dict) else None
current_sha = head.get("sha") if isinstance(head, dict) else None
if current_sha != expected_head_sha:
raise StaleReviewError("Pull request changed while it was being reviewed")
response = await _get_client().post(
f"/api/v1/{base}/reviews",
headers=_auth(),
json={
"body": body,
"event": {
"comment": "COMMENT",
"approve": "APPROVE",
"request_changes": "REQUEST_CHANGES",
}[decision],
"commit_id": current_sha,
},
)
response.raise_for_status()
review = response.json()
if not isinstance(review, dict):
raise ValueError("Gitea review response was not an object")
return {
"id": review.get("id"),
"state": review.get("state") or "COMMENT",
"url": _safe_web_url(review.get("html_url")),
}
async def activity_events(user: dict | None = None) -> list[dict]:
if user is None:
user = await current_user()
+50
View File
@@ -93,6 +93,19 @@ class NotificationReply(BaseModel):
return value
class PullReviewSubmission(BaseModel):
decision: str
body: str = Field(max_length=10_000)
expected_head_sha: str
@field_validator("decision")
@classmethod
def validate_decision(cls, value: str) -> str:
if value not in {"comment", "approve", "request_changes"}:
raise ValueError("unsupported review decision")
return value
def _context_payload(user_data, repo_data, issues_data, prs_data) -> dict:
user_model = User(
id=user_data["id"],
@@ -680,3 +693,40 @@ async def review_detail(owner: str, repo: str, number: int):
{"error": "Pull request review details are temporarily unavailable"},
status_code=503,
)
@app.post("/api/v1/repos/{owner}/{repo}/pulls/{number}/review", status_code=201)
async def submit_review(
submission: PullReviewSubmission, owner: str, repo: str, number: int
):
repository = f"{owner}/{repo}"
async def submit_requested_review():
if not await is_requested_review(repository, number):
raise HTTPException(status_code=404, detail="Review request not found")
return await gitea_proxy.submit_pull_review(
repository,
number,
submission.expected_head_sha,
submission.decision,
submission.body,
)
try:
result = await asyncio.wait_for(
submit_requested_review(), timeout=REVIEW_DETAIL_TIMEOUT_SECONDS
)
except HTTPException:
raise
except gitea_proxy.StaleReviewError:
return JSONResponse(
{"error": "New commits were pushed. Refresh the review before submitting."},
status_code=409,
)
except Exception:
return JSONResponse(
{"error": "The review could not be submitted. Your draft is safe; please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
return JSONResponse(result, status_code=201)