nisse-review: repository patterns, and the wall checked every round
At bring-up, nisse-review read no repositories: Gitea's /user/repos leaves out a public repository's Read collaborators, so dev/nisse never showed up. `repositories` now holds patterns (dev/nisse, dev/*, */*) matched against every repository the account can read, public ones included (an organization's or user's list, or the instance's search); unset, it is /user/repos as before. A configured repository it can't read is left out with an oplog line. The read-only wall (R3) is checked every round, not only at startup: an account granted push or admin while running stops (exit 3) rather than review on. nisse-core's Gitea client gains GiteaNotFound for 404s, owner_repositories and visible_repositories. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
52c6704b17
commit
2139ddd05c
12 files changed
+283
-44
No files matched your search
@@ -28,6 +28,10 @@ class GiteaError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class GiteaNotFound(GiteaError):
|
||||
"""A 404: no such thing, or nothing this account may see."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PullRequest:
|
||||
number: int
|
||||
@@ -123,7 +127,8 @@ class GiteaClient:
|
||||
except httpx.HTTPError as exc:
|
||||
raise GiteaError(f"gitea request failed: {exc}") from exc
|
||||
if response.status_code >= 400:
|
||||
raise GiteaError(
|
||||
error = GiteaNotFound if response.status_code == 404 else GiteaError
|
||||
raise error(
|
||||
f"gitea {method} {path} returned {response.status_code}: "
|
||||
f"{response.text[:300]}"
|
||||
)
|
||||
@@ -416,6 +421,30 @@ class GiteaClient:
|
||||
return found
|
||||
raise GiteaError("too many repositories to list")
|
||||
|
||||
def owner_repositories(self, owner: str) -> list[str]:
|
||||
"""Every repository of an organization (or a user) this account can
|
||||
read, as "owner/name"."""
|
||||
try:
|
||||
return self._paged_names(f"/orgs/{owner}/repos")
|
||||
except GiteaNotFound:
|
||||
return self._paged_names(f"/users/{owner}/repos")
|
||||
|
||||
def visible_repositories(self) -> list[str]:
|
||||
"""Every repository this account can read, public ones included."""
|
||||
return self._paged_names("/repos/search", key="data")
|
||||
|
||||
def _paged_names(self, path: str, key: str | None = None) -> list[str]:
|
||||
found: list[str] = []
|
||||
for page in range(1, MAX_PR_PAGES + 1):
|
||||
answer = self._request(
|
||||
"GET", path, params={"limit": PR_PAGE_SIZE, "page": page}
|
||||
)
|
||||
items = (answer or {}).get(key, []) if key else (answer or [])
|
||||
found += [str(item["full_name"]) for item in items]
|
||||
if len(items) < PR_PAGE_SIZE:
|
||||
return found
|
||||
raise GiteaError("too many repositories to list")
|
||||
|
||||
def open_pull_requests(self, repo: str) -> list[PullRequest]:
|
||||
found: list[PullRequest] = []
|
||||
for page in range(1, MAX_PR_PAGES + 1):
|
||||
|
||||
@@ -8,6 +8,7 @@ from nisse_core.gitea import (
|
||||
Comment,
|
||||
GiteaClient,
|
||||
GiteaError,
|
||||
GiteaNotFound,
|
||||
pull_request_reconciler,
|
||||
)
|
||||
from nisse_core.state.store import Intent
|
||||
@@ -360,6 +361,33 @@ def test_readable_repositories_page_to_the_end() -> None:
|
||||
assert len(found) == PR_PAGE_SIZE + 2 and found[-1] == "o/r2-1"
|
||||
|
||||
|
||||
def test_owner_repositories_try_the_organization_then_the_user() -> None:
|
||||
asked: list[str] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
asked.append(request.url.path)
|
||||
if request.url.path.startswith("/api/v1/orgs/"):
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
return httpx.Response(200, json=[{"full_name": "don/r"}])
|
||||
|
||||
assert client(handler).owner_repositories("don") == ["don/r"]
|
||||
assert asked == ["/api/v1/orgs/don/repos", "/api/v1/users/don/repos"]
|
||||
|
||||
|
||||
def test_visible_repositories_read_the_search_and_a_404_is_not_found() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.url.path.endswith("/repos/search"):
|
||||
return httpx.Response(
|
||||
200, json={"ok": True, "data": [{"full_name": "d/n"}]}
|
||||
)
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
|
||||
gitea = client(handler)
|
||||
assert gitea.visible_repositories() == ["d/n"]
|
||||
with pytest.raises(GiteaNotFound):
|
||||
gitea.repository("d/gone")
|
||||
|
||||
|
||||
def test_a_pull_request_carries_what_a_review_needs() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.url.path.endswith(".diff"):
|
||||
|
||||
Reference in new issue
Block a user