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:
2026-07-07 04:25:02 -04:00
parent fd396df334
commit 8929286276
7 changed files with 520 additions and 107 deletions
+130 -28
View File
@@ -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 (150; 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(