nisse-review: read code from a git clone kept between runs
Archives were downloaded and unpacked afresh for every run, which doesn't scale to very large repositories. Each repository is now cloned once, with its whole history, under scratch/repos/<owner>/<name>/ (head/ the clone, base/ a worktree when read). A run fetches every branch and the PR's refs/pull/N/head, checks out the head exactly, and fails if that head is gone. Only the latest repository is kept for now. Git runs walled off: none of the host's git configuration, links checked out as plain files, no hooks, submodules or LFS, only the Gitea URL's protocol, and the token as an http.extraHeader for that URL in git's environment only. A stop ends a git command; a cut-short first clone is removed. The reader never shows .git. GiteaClient.archive is gone. 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
a85e855fc2
commit
3813ad55d6
11 files changed
+642
-139
No files matched your search
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from review_fakes import FakeGitea, Scripted, call, pr
|
||||
from review_fakes import FakeGitea, FakeSources, Scripted, call, pr
|
||||
|
||||
from nisse_core.gitea import GiteaError
|
||||
from nisse_core.knowledge.handlers import Knowledge
|
||||
@@ -51,17 +51,22 @@ def world(
|
||||
oplog=oplog,
|
||||
knowledge=Knowledge(vault, Memory(vault, "memory/nisse-review"), oplog),
|
||||
)
|
||||
scratch = tmp_path / "scratch"
|
||||
scratch.mkdir()
|
||||
sources = FakeSources(gitea, tmp_path / "trees")
|
||||
scheduler = Scheduler(
|
||||
store,
|
||||
gitea, # type: ignore[arg-type]
|
||||
reviewer,
|
||||
oplog=oplog,
|
||||
scratch=scratch,
|
||||
sources=sources,
|
||||
**more,
|
||||
)
|
||||
return {"gitea": gitea, "store": store, "scheduler": scheduler, "tmp": tmp_path}
|
||||
return {
|
||||
"gitea": gitea,
|
||||
"store": store,
|
||||
"scheduler": scheduler,
|
||||
"sources": sources,
|
||||
"tmp": tmp_path,
|
||||
}
|
||||
|
||||
|
||||
def item(w: dict[str, Any], number: int = 7):
|
||||
@@ -212,7 +217,7 @@ def test_reads_stay_in_the_repository(tmp_path: Path) -> None:
|
||||
assert results[1] == ("ref is head or base, not 'other'", False)
|
||||
assert "count.py:2: return len(text.split())" in results[2][0]
|
||||
assert results[3][0].startswith("[o/demo#3, by don]")
|
||||
assert list((tmp_path / "scratch").iterdir()) == [] # the trees are gone
|
||||
assert w["sources"].opened == [(REPO, 7, "abc123def456", "main")]
|
||||
|
||||
|
||||
def test_memory_notes_go_under_the_repositorys_folder(tmp_path: Path) -> None:
|
||||
@@ -509,30 +514,6 @@ def test_gitea_errors_while_posting_count_as_a_failed_run(tmp_path: Path) -> Non
|
||||
assert w["store"].pending_intents() == []
|
||||
|
||||
|
||||
def test_an_archive_unpacks_only_plain_files_inside(tmp_path: Path) -> None:
|
||||
import tarfile
|
||||
|
||||
from nisse_review.reader import _unpack
|
||||
|
||||
raw = io.BytesIO()
|
||||
with tarfile.open(fileobj=raw, mode="w:gz") as tar:
|
||||
for name, kind in [
|
||||
("demo/ok.py", tarfile.REGTYPE),
|
||||
("demo/../../evil.py", tarfile.REGTYPE),
|
||||
("demo/link", tarfile.SYMTYPE),
|
||||
]:
|
||||
info = tarfile.TarInfo(name)
|
||||
info.type = kind
|
||||
info.linkname = "/etc/passwd" if kind == tarfile.SYMTYPE else ""
|
||||
info.size = 0 if kind == tarfile.SYMTYPE else 2
|
||||
tar.addfile(info, io.BytesIO(b"x\n") if info.size else None)
|
||||
root = tmp_path / "tree"
|
||||
root.mkdir()
|
||||
_unpack(raw.getvalue(), root)
|
||||
assert sorted(p.name for p in root.rglob("*")) == ["ok.py"]
|
||||
assert not (tmp_path / "evil.py").exists()
|
||||
|
||||
|
||||
def test_the_config_names_every_problem(tmp_path: Path) -> None:
|
||||
from nisse_core.config import ConfigError
|
||||
from nisse_review.config import ReviewConfig
|
||||
@@ -564,7 +545,8 @@ def test_a_long_file_is_read_in_parts_to_its_end(tmp_path: Path) -> None:
|
||||
|
||||
body = "".join(f"x{n} = {'y' * 60}\n" for n in range(1, 1001))
|
||||
gitea = FakeGitea(files={"long.py": body})
|
||||
repo = Repository(gitea, REPO, {"head": "abc"}, tmp_path) # type: ignore[arg-type]
|
||||
trees = FakeSources(gitea, tmp_path).open(REPO, 7, "abc", "main")
|
||||
repo = Repository(gitea, REPO, trees) # type: ignore[arg-type]
|
||||
first = repo.dispatch("read_file", {"path": "long.py"}).result
|
||||
assert len(first) < MAX_FILE_CHARS + 200
|
||||
assert "lines 1-" in first and "of 1000" in first
|
||||
@@ -619,3 +601,23 @@ def test_out_of_turns_with_nothing_drafted_is_a_failed_run(tmp_path: Path) -> No
|
||||
w["scheduler"].round()
|
||||
assert w["gitea"].posted == []
|
||||
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 1)
|
||||
|
||||
|
||||
def test_code_git_cannot_give_fails_the_run(tmp_path: Path) -> None:
|
||||
from nisse_review.clones import CloneError
|
||||
|
||||
class Broken(FakeSources):
|
||||
def open(self, repo: str, number: int, head: str, base: str): # type: ignore[no-untyped-def]
|
||||
trees = super().open(repo, number, head, base)
|
||||
|
||||
def tree(ref: str) -> Path:
|
||||
raise CloneError("git fetch failed: no route to host")
|
||||
|
||||
trees.tree = tree # type: ignore[method-assign]
|
||||
return trees
|
||||
|
||||
w = world(tmp_path, Scripted(*REVIEW))
|
||||
w["scheduler"]._sources = Broken(w["gitea"], tmp_path / "trees")
|
||||
w["scheduler"].round()
|
||||
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 1)
|
||||
assert w["gitea"].posted == []
|
||||
Reference in new issue
Block a user