feat: show pull reviewer decisions (Closes #1334)
This commit is contained in:
1 parent
f8ab6977ed
commit
bd96c3caa3
7 files changed
+275
-10
No files matched your search
+63
-2
@@ -2771,6 +2771,55 @@ async def pull_completion_detail(repository: str, number: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _normalize_reviewer_statuses(pull: dict, reviews: object, head_sha: str) -> list[dict]:
|
||||
"""Return one bounded, current decision per reviewer without exposing raw review data."""
|
||||
latest: dict[str, dict] = {}
|
||||
for review in (reviews if isinstance(reviews, list) else [])[:100]:
|
||||
if not isinstance(review, dict):
|
||||
continue
|
||||
user = review.get("user") if isinstance(review.get("user"), dict) else {}
|
||||
login = user.get("login")
|
||||
state = review.get("state")
|
||||
commit_id = review.get("commit_id")
|
||||
if (
|
||||
not isinstance(login, str)
|
||||
or not login
|
||||
or state not in {"APPROVED", "REQUEST_CHANGES", "COMMENT"}
|
||||
or not isinstance(commit_id, str)
|
||||
or not commit_id
|
||||
):
|
||||
continue
|
||||
previous = latest.get(login.casefold())
|
||||
review_id = review.get("id") if isinstance(review.get("id"), int) else 0
|
||||
if previous is None or review_id >= previous["id"]:
|
||||
latest[login.casefold()] = {
|
||||
"id": review_id, "login": login, "state": state, "commit_id": commit_id,
|
||||
}
|
||||
|
||||
statuses: dict[str, dict] = {}
|
||||
state_names = {
|
||||
"APPROVED": "approved", "REQUEST_CHANGES": "changes_requested", "COMMENT": "commented",
|
||||
}
|
||||
for key, review in latest.items():
|
||||
current = review["commit_id"] == head_sha
|
||||
status = state_names[review["state"]] if current else "outdated"
|
||||
statuses[key] = {
|
||||
"login": review["login"],
|
||||
"status": status,
|
||||
"head_sha": review["commit_id"],
|
||||
"blocking": status in {"changes_requested", "outdated"},
|
||||
}
|
||||
|
||||
requested = pull.get("requested_reviewers") if isinstance(pull, dict) else []
|
||||
for reviewer in (requested if isinstance(requested, list) else [])[:25]:
|
||||
login = reviewer.get("login") if isinstance(reviewer, dict) else None
|
||||
if isinstance(login, str) and login:
|
||||
statuses[login.casefold()] = {
|
||||
"login": login, "status": "waiting", "head_sha": head_sha, "blocking": True,
|
||||
}
|
||||
return sorted(statuses.values(), key=lambda item: item["login"].casefold())
|
||||
|
||||
|
||||
async def pull_completion_review(repository: str, number: int) -> dict:
|
||||
base = f"repos/{repository}/pulls/{number}"
|
||||
pull = await fetch(base)
|
||||
@@ -2780,12 +2829,13 @@ async def pull_completion_review(repository: str, number: int) -> dict:
|
||||
head: dict = head_value if isinstance(head_value, dict) else {}
|
||||
sha_value = head.get("sha")
|
||||
sha = sha_value if isinstance(sha_value, str) else ""
|
||||
files, status, diff_result = await asyncio.gather(
|
||||
files, status, diff_result, reviews = await asyncio.gather(
|
||||
fetch(f"{base}/files"),
|
||||
fetch(f"repos/{repository}/commits/{sha}/status"),
|
||||
fetch_text(
|
||||
f"repos/{repository}/pulls/{number}.diff", REVIEW_DIFF_MAX_BYTES
|
||||
),
|
||||
fetch(f"{base}/reviews?limit=100"),
|
||||
)
|
||||
diff, diff_truncated = diff_result
|
||||
previews = _diff_previews(diff, diff_truncated)
|
||||
@@ -2799,6 +2849,7 @@ async def pull_completion_review(repository: str, number: int) -> dict:
|
||||
"merged": pull.get("merged") is True,
|
||||
"ci_state": status.get("state", "unknown") if isinstance(status, dict) else "unknown",
|
||||
"checks": _normalize_commit_checks(status),
|
||||
"reviewers": _normalize_reviewer_statuses(pull, reviews, sha),
|
||||
"files": [
|
||||
{
|
||||
"filename": item.get("filename", ""),
|
||||
@@ -2873,7 +2924,10 @@ async def pull_check_status(repository: str, number: int) -> dict:
|
||||
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")
|
||||
status, reviews = await asyncio.gather(
|
||||
fetch(f"repos/{repository}/commits/{sha}/status"),
|
||||
fetch(f"{base}/reviews?limit=100"),
|
||||
)
|
||||
return {
|
||||
"repository": repository,
|
||||
"number": number,
|
||||
@@ -2884,6 +2938,7 @@ async def pull_check_status(repository: str, number: int) -> dict:
|
||||
"merged": pull.get("merged") is True,
|
||||
"ci_state": status.get("state", "unknown") if isinstance(status, dict) else "unknown",
|
||||
"checks": _normalize_commit_checks(status),
|
||||
"reviewers": _normalize_reviewer_statuses(pull, reviews, sha),
|
||||
}
|
||||
|
||||
|
||||
@@ -2924,6 +2979,12 @@ async def merge_assigned_pull(
|
||||
or ci_state != "success"
|
||||
):
|
||||
raise PullNotMergeableError("Pull request is not currently safe to merge")
|
||||
reviews = await fetch(f"{base}/reviews?limit=100")
|
||||
if any(
|
||||
reviewer["blocking"]
|
||||
for reviewer in _normalize_reviewer_statuses(pull, reviews, current_sha)
|
||||
):
|
||||
raise PullNotMergeableError("Pull request review is not currently resolved")
|
||||
response = await _get_client().post(
|
||||
f"/api/v1/{base}/merge",
|
||||
headers=_auth(),
|
||||
|
||||
Reference in new issue
Block a user