Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions tools/fossil/src/magpie_fossil/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from __future__ import annotations

import contextlib
import os
import sqlite3
import subprocess
Expand Down Expand Up @@ -67,13 +68,12 @@ def find_repo_db(start_dir: Path) -> Path | None:
db_path = d / marker
if db_path.exists():
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT value FROM vvar WHERE name = 'repository'")
row = cursor.fetchone()
conn.close()
if row:
return Path(row[0])
with contextlib.closing(sqlite3.connect(db_path)) as conn:
cursor = conn.cursor()
cursor.execute("SELECT value FROM vvar WHERE name = 'repository'")
row = cursor.fetchone()
if row:
return Path(row[0])
except sqlite3.Error:
pass
return None
Expand All @@ -82,12 +82,11 @@ def find_repo_db(start_dir: Path) -> Path | None:
def query_db(repo_path: Path, query: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]:
"""Execute a read-only SQL query against the Fossil SQLite database."""
try:
conn = sqlite3.connect(repo_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
rows = cursor.fetchall()
conn.close()
return [dict(r) for r in rows]
with contextlib.closing(sqlite3.connect(repo_path)) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
rows = cursor.fetchall()
return [dict(r) for r in rows]
except sqlite3.Error as exc:
raise FossilError(f"Failed to query Fossil SQLite database: {exc}") from exc
18 changes: 14 additions & 4 deletions tools/fossil/src/magpie_fossil/forum.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,13 @@ def list_forum_threads(repo_path: Path) -> list[dict[str, Any]]:
"date": parsed["date"],
}
)
except Exception:
pass
except Exception as exc:
import sys

print(
f"Warning: skipped corrupt or missing forum post artifact '{root_uuid}': {exc}",
file=sys.stderr,
)
return threads


Expand Down Expand Up @@ -129,6 +134,11 @@ def read_forum_thread(repo_path: Path, thread_uuid: str) -> list[dict[str, Any]]
"body": parsed["body"],
}
)
except Exception:
pass
except Exception as exc:
import sys

print(
f"Warning: skipped corrupt or missing forum post artifact '{post_uuid}': {exc}",
file=sys.stderr,
)
return posts
13 changes: 6 additions & 7 deletions tools/fossil/src/magpie_fossil/ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ def get_ticket(repo_path: Path, tkt_uuid: str) -> dict[str, Any]:
mtime = chng.get("tkt_mtime") or chng.get("mtime")
if c_text:
comments.append({"id": idx + 1, "body": c_text, "author": user, "date": mtime})
except Exception:
# If ticket_chng is missing/different, we still return the ticket without history comments
pass
except FossilError as exc:
import sys

print(f"Warning: failed to query ticket comments (schema might be custom): {exc}", file=sys.stderr)

# If the main ticket body has a comment, and no change comments were found,
# we can treat that as the initial comment
Expand Down Expand Up @@ -81,11 +82,10 @@ def list_tickets(repo_path: Path) -> list[dict[str, Any]]:

def submit_ticket(repo_path: Path, title: str, body: str, extra_fields: dict[str, str] | None = None) -> str:
"""Create a new ticket using Fossil CLI."""
args = ["ticket", "add", "title", title, "comment", body]
args = ["ticket", "add", "-R", str(repo_path), "--", "title", title, "comment", body]
if extra_fields:
for k, v in extra_fields.items():
args.extend([k, v])
args.extend(["-R", str(repo_path)])

# Fossil ticket add outputs: "Created new ticket <UUID>"
out = run_fossil(args)
Expand All @@ -102,10 +102,9 @@ def update_ticket_fields(repo_path: Path, tkt_uuid: str, fields: dict[str, str])
tkt = get_ticket(repo_path, tkt_uuid)
full_uuid = tkt["tkt_uuid"]

args = ["ticket", "set", full_uuid]
args = ["ticket", "set", full_uuid, "-R", str(repo_path), "--"]
for k, v in fields.items():
args.extend([k, v])
args.extend(["-R", str(repo_path)])

run_fossil(args)
return full_uuid
Expand Down
4 changes: 2 additions & 2 deletions tools/fossil/tests/test_fossil.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def test_submit_ticket(mock_run_fossil: MagicMock) -> None:
uuid = submit_ticket(Path("repo.fossil"), "T1", "Body", {"type": "Bug"})
assert uuid == "1234567890abcdef12"
mock_run_fossil.assert_called_once_with(
["ticket", "add", "title", "T1", "comment", "Body", "type", "Bug", "-R", "repo.fossil"]
["ticket", "add", "-R", "repo.fossil", "--", "title", "T1", "comment", "Body", "type", "Bug"]
)


Expand All @@ -123,7 +123,7 @@ def test_submit_comment(mock_query_db: MagicMock, mock_run_fossil: MagicMock) ->
uuid = submit_comment(Path("repo.fossil"), "abc", "My comment")
assert uuid == "abcdef123456"
mock_run_fossil.assert_called_once_with(
["ticket", "set", "abcdef123456", "+comment", "My comment", "-R", "repo.fossil"]
["ticket", "set", "abcdef123456", "-R", "repo.fossil", "--", "+comment", "My comment"]
)


Expand Down
55 changes: 55 additions & 0 deletions tools/vcs/tests/test_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,58 @@ def test_cli_unimplemented_backend_errors(tmp_path: Path, capsys: pytest.Capture
def test_cli_cat(git_repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
assert main(["-C", str(git_repo), "cat", "HEAD", "file.txt"]) == 0
assert capsys.readouterr().out == "hello\n"


def test_fossil_parser_status(monkeypatch: pytest.MonkeyPatch) -> None:
def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str:
if "changes" in args:
return "EDITED file1.txt\nADDED file2.txt\nDELETED file3.txt\n"
if "extras" in args:
return "untracked1.txt\nuntracked2.txt\n"
return ""

monkeypatch.setattr("magpie_vcs._run", mock_run)
backend = FossilBackend(Path("/tmp/repo"))
status_out = backend.status()
assert "M file1.txt" in status_out
assert "A file2.txt" in status_out
assert "D file3.txt" in status_out
assert "? untracked1.txt" in status_out
assert "? untracked2.txt" in status_out


def test_fossil_parser_status_error(monkeypatch: pytest.MonkeyPatch) -> None:
def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str:
raise VCSError("command failed")

monkeypatch.setattr("magpie_vcs._run", mock_run)
backend = FossilBackend(Path("/tmp/repo"))
assert backend.status() == ""


def test_fossil_parser_log(monkeypatch: pytest.MonkeyPatch) -> None:
sample_timeline = """
=== 2026-07-08 ===
17:00:00 [a1b2c3d4e5f6] Initial commit (user: arnav tags: trunk)
16:30:00 [f6e5d4c3b2a1] Fix a bug in forum post rendering (user: jsmith tags: trunk)
16:00:00 [1234567890ab] Add a new feature [options] (user: alice tags: trunk)
"""

def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str:
if "timeline" in args:
return sample_timeline
return ""

monkeypatch.setattr("magpie_vcs._run", mock_run)
backend = FossilBackend(Path("/tmp/repo"))

# Test basic log
log_out = backend.log()
assert "a1b2c3d4e5f6 Initial commit" in log_out
assert "f6e5d4c3b2a1 Fix a bug in forum post rendering" in log_out
assert "1234567890ab Add a new feature [options]" in log_out

# Test grep search
grep_out = backend.log(grep="bug")
assert "f6e5d4c3b2a1 Fix a bug in forum post rendering" in grep_out
assert "a1b2c3d4e5f6" not in grep_out