nisse-review: repository patterns, and the wall checked every round
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m56s
CI / e2e (pull_request) Successful in 9m52s

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:
dongarandClaude Opus 5.5 committed 2026-10-16 21:41:53 +00:00
1 parent 52c6704b17
commit 2139ddd05c
12 files changed
+283 -44

No files matched your search

+30 -1
View File
@@ -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):