feat: add mobile review readiness inspector (#121)
This commit is contained in:
1 parent
329591ebd6
commit
83788c6b82
7 files changed
+391
-9
No files matched your search
@@ -55,6 +55,70 @@ async def pull_requests() -> list[dict]:
|
||||
return list(merged.values())
|
||||
|
||||
|
||||
async def is_requested_review(repository: str, number: int) -> bool:
|
||||
pulls = await pull_requests()
|
||||
return any(
|
||||
isinstance(pull, dict)
|
||||
and pull.get("number") == number
|
||||
and "review_requested" in (pull.get("work_reasons") or [])
|
||||
and isinstance(pull.get("repository"), dict)
|
||||
and pull["repository"].get("full_name") == repository
|
||||
for pull in pulls
|
||||
)
|
||||
|
||||
|
||||
async def pull_review_detail(repository: str, number: int) -> dict:
|
||||
base = f"repos/{repository}/pulls/{number}"
|
||||
pull = await fetch(base)
|
||||
if not isinstance(pull, dict):
|
||||
raise ValueError("Gitea pull request response was not an object")
|
||||
files = await fetch(f"{base}/files")
|
||||
head_value = pull.get("head")
|
||||
head: dict = head_value if isinstance(head_value, dict) else {}
|
||||
sha_value = head.get("sha")
|
||||
sha = sha_value if isinstance(sha_value, str) else ""
|
||||
status = await fetch(f"repos/{repository}/commits/{sha}/status")
|
||||
reviews = await fetch(f"{base}/reviews")
|
||||
user_value = pull.get("user")
|
||||
user: dict = user_value if isinstance(user_value, dict) else {}
|
||||
normalized_files = [
|
||||
{
|
||||
"filename": file["filename"],
|
||||
"status": file.get("status") or "changed",
|
||||
"additions": file.get("additions") or 0,
|
||||
"deletions": file.get("deletions") or 0,
|
||||
}
|
||||
for file in (files if isinstance(files, list) else [])[:100]
|
||||
if isinstance(file, dict) and isinstance(file.get("filename"), str)
|
||||
]
|
||||
normalized_reviews = [
|
||||
{
|
||||
"user": {
|
||||
"login": (
|
||||
review["user"].get("login", "")
|
||||
if isinstance(review.get("user"), dict)
|
||||
else ""
|
||||
)
|
||||
},
|
||||
"state": review.get("state") or "COMMENT",
|
||||
"body": review.get("body") or "",
|
||||
}
|
||||
for review in (reviews if isinstance(reviews, list) else [])[:50]
|
||||
if isinstance(review, dict)
|
||||
]
|
||||
return {
|
||||
"repository": repository,
|
||||
"number": number,
|
||||
"title": pull.get("title", ""),
|
||||
"body": pull.get("body") or "",
|
||||
"url": pull.get("html_url", ""),
|
||||
"author": user.get("login", ""),
|
||||
"ci_state": status.get("state", "unknown") if isinstance(status, dict) else "unknown",
|
||||
"files": normalized_files,
|
||||
"reviews": normalized_reviews,
|
||||
}
|
||||
|
||||
|
||||
async def activity_events() -> list[dict]:
|
||||
user = await current_user()
|
||||
events = await fetch(f"users/{user['login']}/activities/feeds?limit=20")
|
||||
|
||||
+37
-4
@@ -2,13 +2,21 @@ import asyncio
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from src.gitea_proxy import activity_events, current_user, repos, issues, pull_requests
|
||||
from src.models import User, Repo, Issue, PullRequest
|
||||
from src.gitea_proxy import (
|
||||
activity_events,
|
||||
current_user,
|
||||
is_requested_review,
|
||||
issues,
|
||||
pull_requests,
|
||||
pull_review_detail,
|
||||
repos,
|
||||
)
|
||||
from src.models import Issue, PullRequest, Repo, User
|
||||
from src.suggestion_engine import compute
|
||||
from src.views import router as frontend_router
|
||||
|
||||
@@ -41,7 +49,10 @@ app.include_router(frontend_router)
|
||||
@app.middleware("http")
|
||||
async def prevent_live_api_caching(request, call_next):
|
||||
response = await call_next(request)
|
||||
if request.url.path in {"/api/v1/context", "/api/v1/events"}:
|
||||
if request.url.path in {"/api/v1/context", "/api/v1/events"} or (
|
||||
request.url.path.startswith("/api/v1/repos/")
|
||||
and request.url.path.endswith("/review")
|
||||
):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
@@ -247,3 +258,25 @@ async def event_stream():
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review")
|
||||
async def review_detail(owner: str, repo: str, number: int):
|
||||
try:
|
||||
repository = f"{owner}/{repo}"
|
||||
if not await asyncio.wait_for(
|
||||
is_requested_review(repository, number),
|
||||
timeout=CONTEXT_TIMEOUT_SECONDS,
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Review request not found")
|
||||
return await asyncio.wait_for(
|
||||
pull_review_detail(repository, number),
|
||||
timeout=CONTEXT_TIMEOUT_SECONDS,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "Pull request review details are temporarily unavailable"},
|
||||
status_code=503,
|
||||
)
|
||||
Reference in new issue
Block a user