feat: add pagination metadata to gitea_list_prs (#340)
Return wrapped {prs, pagination} from gitea_list_prs with has_more,
inventory_complete, and page traversal via api_fetch_page. Route
gitea_review_pr inventory through gitea_list_prs so trust gates can
prove pagination finality. Add assess_list_prs_pagination_proof verifier.
Closes #340
This commit is contained in:
+130
-28
@@ -461,6 +461,7 @@ from gitea_auth import ( # noqa: E402
|
||||
get_auth_header,
|
||||
api_request,
|
||||
api_get_all,
|
||||
api_fetch_page,
|
||||
repo_api_url,
|
||||
get_profile,
|
||||
gitea_url,
|
||||
@@ -1118,6 +1119,56 @@ def gitea_create_pr(
|
||||
return _with_optional_url({"number": data["number"]}, data.get("html_url"))
|
||||
|
||||
|
||||
def _format_list_pr_entry(pr: dict) -> dict:
|
||||
head_obj = pr.get("head") or {}
|
||||
base_obj = pr.get("base") or {}
|
||||
entry = {
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"state": pr["state"],
|
||||
"head": head_obj.get("ref") if isinstance(head_obj, dict) else head_obj,
|
||||
"base": base_obj.get("ref") if isinstance(base_obj, dict) else base_obj,
|
||||
"mergeable": pr.get("mergeable"),
|
||||
"updated_at": pr.get("updated_at"),
|
||||
"head_sha": head_obj.get("sha") if isinstance(head_obj, dict) else None,
|
||||
"author": (pr.get("user") or {}).get("login"),
|
||||
"labels": [
|
||||
label["name"]
|
||||
for label in pr.get("labels", [])
|
||||
if isinstance(label, dict) and label.get("name")
|
||||
],
|
||||
"body": pr.get("body") or "",
|
||||
}
|
||||
return _with_optional_url(entry, pr.get("html_url"))
|
||||
|
||||
|
||||
def _list_prs_pagination_envelope(
|
||||
prs: list[dict],
|
||||
*,
|
||||
page: int,
|
||||
per_page: int,
|
||||
pages_fetched: int,
|
||||
is_final_page: bool,
|
||||
has_more: bool,
|
||||
next_page: int | None,
|
||||
inventory_complete: bool,
|
||||
) -> dict:
|
||||
return {
|
||||
"prs": prs,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"returned_count": len(prs),
|
||||
"has_more": has_more,
|
||||
"next_page": next_page,
|
||||
"is_final_page": is_final_page,
|
||||
"pages_fetched": pages_fetched,
|
||||
"inventory_complete": inventory_complete,
|
||||
"total_count": len(prs) if inventory_complete else None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_prs(
|
||||
state: str = "open",
|
||||
@@ -1125,7 +1176,9 @@ def gitea_list_prs(
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> list[dict]:
|
||||
page: int | None = None,
|
||||
per_page: int = 50,
|
||||
) -> dict:
|
||||
"""List pull requests on a Gitea repository.
|
||||
|
||||
Args:
|
||||
@@ -1134,11 +1187,16 @@ def gitea_list_prs(
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
page: Optional 1-based page for explicit single-page fetch. When
|
||||
omitted, all pages are traversed and ``inventory_complete`` is set.
|
||||
per_page: Page size (1–50; Gitea caps at 50).
|
||||
|
||||
Returns:
|
||||
List of dicts with 'number', 'title', 'state', 'head', 'base',
|
||||
'mergeable', 'updated_at' ('url' only with the reveal opt-in).
|
||||
The additional 'updated_at' aids stale/conflicting queue detection.
|
||||
Dict with ``prs`` (list of PR summaries) and ``pagination`` metadata
|
||||
(``has_more``, ``next_page``, ``is_final_page``, ``inventory_complete``,
|
||||
``pages_fetched``, ``total_count`` when complete). Each PR entry carries
|
||||
``number``, ``title``, ``state``, ``head``, ``base``, ``mergeable``,
|
||||
``updated_at`` ('url' only with the reveal opt-in).
|
||||
"""
|
||||
allowed, block_reasons = capability_stop_terminal.check_reviewer_queue_tool(
|
||||
"list_prs"
|
||||
@@ -1148,20 +1206,48 @@ def gitea_list_prs(
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/pulls?state={state}"
|
||||
prs = api_get_all(url, auth)
|
||||
results = []
|
||||
for pr in prs:
|
||||
entry = {
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"state": pr["state"],
|
||||
"head": pr["head"]["ref"],
|
||||
"base": pr["base"]["ref"],
|
||||
"mergeable": pr.get("mergeable"),
|
||||
"updated_at": pr.get("updated_at"),
|
||||
}
|
||||
results.append(_with_optional_url(entry, pr.get("html_url")))
|
||||
return results
|
||||
|
||||
if page is not None:
|
||||
raw_page, pagination = api_fetch_page(
|
||||
url, auth, page=page, limit=per_page
|
||||
)
|
||||
prs = [_format_list_pr_entry(pr) for pr in raw_page]
|
||||
return _list_prs_pagination_envelope(
|
||||
prs,
|
||||
page=pagination["page"],
|
||||
per_page=pagination["per_page"],
|
||||
pages_fetched=1,
|
||||
is_final_page=pagination["is_final_page"],
|
||||
has_more=pagination["has_more"],
|
||||
next_page=pagination["next_page"],
|
||||
inventory_complete=False,
|
||||
)
|
||||
|
||||
all_raw: list[dict] = []
|
||||
current_page = 1
|
||||
pages_fetched = 0
|
||||
last_meta: dict = {}
|
||||
while pages_fetched < 100:
|
||||
raw_page, last_meta = api_fetch_page(
|
||||
url, auth, page=current_page, limit=per_page
|
||||
)
|
||||
pages_fetched += 1
|
||||
all_raw.extend(raw_page)
|
||||
if last_meta["is_final_page"]:
|
||||
break
|
||||
current_page += 1
|
||||
|
||||
prs = [_format_list_pr_entry(pr) for pr in all_raw]
|
||||
return _list_prs_pagination_envelope(
|
||||
prs,
|
||||
page=1,
|
||||
per_page=per_page,
|
||||
pages_fetched=pages_fetched,
|
||||
is_final_page=bool(last_meta.get("is_final_page")),
|
||||
has_more=False,
|
||||
next_page=None,
|
||||
inventory_complete=bool(last_meta.get("is_final_page")),
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -2928,19 +3014,25 @@ def gitea_review_pr(
|
||||
if can_read and auth:
|
||||
inventory_attempted = True
|
||||
try:
|
||||
url = f"{repo_api_url(h, o, r)}/pulls?state=open"
|
||||
prs = api_get_all(url, auth)
|
||||
prs_found_count = len(prs)
|
||||
for pr in prs:
|
||||
labels = [l["name"] for l in pr.get("labels", [])]
|
||||
prs = gitea_list_prs(
|
||||
state="open",
|
||||
remote=remote,
|
||||
host=h,
|
||||
org=o,
|
||||
repo=r,
|
||||
)
|
||||
prs_list = prs.get("prs") or []
|
||||
prs_found_count = len(prs_list)
|
||||
for pr in prs_list:
|
||||
labels = list(pr.get("labels") or [])
|
||||
body_text = pr.get("body") or ""
|
||||
linked = []
|
||||
# Simple extraction of linked issues from body
|
||||
for match in re.findall(r'#(\d+)', body_text):
|
||||
linked.append(f"#{match}")
|
||||
|
||||
pr_head_sha = (pr.get("head") or {}).get("sha")
|
||||
pr_author = (pr.get("user") or {}).get("login")
|
||||
pr_head_sha = pr.get("head_sha")
|
||||
pr_author = pr.get("author")
|
||||
|
||||
# Determine review block status
|
||||
req_non_author = False
|
||||
@@ -2951,8 +3043,8 @@ def gitea_review_pr(
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"author": pr_author,
|
||||
"base": pr["base"]["ref"],
|
||||
"head": pr["head"]["ref"],
|
||||
"base": pr["base"],
|
||||
"head": pr["head"],
|
||||
"mergeable": pr.get("mergeable"),
|
||||
"linked_issues": list(set(linked)),
|
||||
"labels": labels,
|
||||
@@ -2993,6 +3085,16 @@ def gitea_review_pr(
|
||||
if inventory_msg:
|
||||
report_lines.append(inventory_msg)
|
||||
else:
|
||||
pagination = (
|
||||
prs.get("pagination") if isinstance(prs, dict) else {}
|
||||
) or {}
|
||||
has_finality_metadata = bool(
|
||||
pagination.get("inventory_complete")
|
||||
or (
|
||||
pagination.get("is_final_page")
|
||||
and not pagination.get("has_more")
|
||||
)
|
||||
)
|
||||
inventory_trust_gate = review_proofs.pr_inventory_trust_gate(
|
||||
prs if inventory_attempted else None,
|
||||
remote=remote,
|
||||
@@ -3001,7 +3103,7 @@ def gitea_review_pr(
|
||||
state="open",
|
||||
authenticated_profile=profile,
|
||||
local_remote_url=_local_git_remote_url(remote),
|
||||
has_finality_metadata=True,
|
||||
has_finality_metadata=has_finality_metadata,
|
||||
)
|
||||
report_lines.extend(
|
||||
review_proofs.format_pr_inventory_trust_gate_report(
|
||||
|
||||
Reference in New Issue
Block a user