"""Tests for the git and Mercurial repo sources. Each test builds a tiny synthetic repo via the actual `git` / `hg` CLIs in a tmp_path, ingests it, and asserts the round-trip and supersedes-chain behavior. If the underlying VCS isn't on PATH, the test is skipped. """ from __future__ import annotations import os import shutil import subprocess from pathlib import Path import pytest from arborist.ingest import ingest_source from arborist.sources.vcs import GitRepoSource, MercurialRepoSource from arborist.store import connect # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _git_available() -> bool: return shutil.which("git") is not None def _hg_available() -> bool: return shutil.which("hg") is not None def _mk_git_repo(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) env = { **os.environ, # Detached author so commits are reproducible and tests don't # accidentally pick up the host's git identity. "GIT_AUTHOR_NAME": "test", "GIT_AUTHOR_EMAIL": "t@example.invalid", "GIT_COMMITTER_NAME": "test", "GIT_COMMITTER_EMAIL": "t@example.invalid", "GIT_AUTHOR_DATE": "2026-01-01T00:00:00Z", "GIT_COMMITTER_DATE": "2026-01-01T00:00:00Z", } subprocess.run(["git", "init", "-q", "-b", "main"], cwd=path, check=True) subprocess.run(["git", "config", "user.email", "t@example.invalid"], cwd=path, check=True) subprocess.run(["git", "config", "user.name", "test"], cwd=path, check=True) (path / "README.md").write_text("# Demo\n\nHello world.\n") (path / "src.py").write_text("def hello():\n return 'hi'\n") (path / "logo.bin").write_bytes(b"\x00\x01\x02\x03" * 64) # binary subprocess.run(["git", "add", "-A"], cwd=path, check=True) subprocess.run( ["git", "commit", "-q", "-m", "initial"], cwd=path, check=True, env=env, ) def _commit_file(path: Path, name: str, body: str, msg: str) -> None: env = { **os.environ, "GIT_AUTHOR_NAME": "test", "GIT_AUTHOR_EMAIL": "t@example.invalid", "GIT_COMMITTER_NAME": "test", "GIT_COMMITTER_EMAIL": "t@example.invalid", "GIT_AUTHOR_DATE": "2026-02-01T00:00:00Z", "GIT_COMMITTER_DATE": "2026-02-01T00:00:00Z", } (path / name).write_text(body) subprocess.run(["git", "add", name], cwd=path, check=True) subprocess.run( ["git", "commit", "-q", "-m", msg], cwd=path, check=True, env=env ) def _mk_hg_repo(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) subprocess.run(["hg", "init", "-q"], cwd=path, check=True) (path / ".hg" / "hgrc").write_text( "[ui]\nusername = test \n" ) (path / "README.md").write_text("# Demo\n\nHello hg.\n") (path / "src.py").write_text("def world():\n return 'world'\n") subprocess.run(["hg", "add"], cwd=path, check=True) subprocess.run( ["hg", "commit", "-m", "initial", "-d", "1735689600 0"], cwd=path, check=True, ) # --------------------------------------------------------------------------- # GitRepoSource # --------------------------------------------------------------------------- @pytest.mark.skipif(not _git_available(), reason="git not on PATH") def test_git_source_yields_text_files_and_skips_binary(tmp_path): repo = tmp_path / "demo-repo" _mk_git_repo(repo) src = GitRepoSource(repo) docs = list(src.iter_documents()) titles = sorted(d.title for d in docs) # Binary file (logo.bin) should not appear. assert titles == ["README.md", "src.py"] readme = next(d for d in docs if d.title == "README.md") assert readme.uri == "git://demo-repo/file/README.md" assert "Hello world" in readme.content assert readme.source_type == "git_repo" # Commit metadata is captured (informational; not Merkle-bound). assert "commit_hash" in readme.extra assert len(readme.extra["commit_hash"]) == 40 @pytest.mark.skipif(not _git_available(), reason="git not on PATH") def test_git_source_round_trip_and_supersedes_chain(tmp_path): repo = tmp_path / "demo-repo" _mk_git_repo(repo) db = tmp_path / "arborist.db" # First ingest: 2 docs (README.md, src.py). conn = connect(db) try: first = ingest_source(conn, GitRepoSource(repo)) finally: conn.close() assert first.inserted == 2 # Modify src.py and re-ingest. Same URI, new content -> new document_root # plus an automatic `supersedes` edge to the prior root. _commit_file(repo, "src.py", "def hello():\n return 'updated'\n", "edit src") conn = connect(db) try: second = ingest_source(conn, GitRepoSource(repo)) finally: conn.close() # README.md is unchanged so its content_root collides -> skipped. # src.py changed -> 1 new doc inserted with a supersedes edge. assert second.inserted == 1 assert second.skipped_duplicate >= 1 conn = connect(db) try: edges = conn.execute( "SELECT src_root, dst_root FROM edges WHERE edge_type='supersedes'" ).fetchall() finally: conn.close() assert len(edges) == 1 @pytest.mark.skipif(not _git_available(), reason="git not on PATH") def test_git_source_rejects_non_git_path(tmp_path): with pytest.raises(FileNotFoundError): GitRepoSource(tmp_path) # tmp_path has no .git # --------------------------------------------------------------------------- # MercurialRepoSource # --------------------------------------------------------------------------- @pytest.mark.skipif(not _hg_available(), reason="hg not on PATH") def test_hg_source_yields_text_files(tmp_path): repo = tmp_path / "demo-hg" _mk_hg_repo(repo) src = MercurialRepoSource(repo) docs = list(src.iter_documents()) titles = sorted(d.title for d in docs) assert titles == ["README.md", "src.py"] readme = next(d for d in docs if d.title == "README.md") assert readme.uri == "hg://demo-hg/file/README.md" assert "Hello hg" in readme.content assert readme.source_type == "hg_repo" assert "changeset_hash" in readme.extra @pytest.mark.skipif(not _hg_available(), reason="hg not on PATH") def test_hg_source_rejects_non_hg_path(tmp_path): with pytest.raises(FileNotFoundError): MercurialRepoSource(tmp_path)