Populate worktree list probe columns

This commit is contained in:
Himanshu Dongre 2026-05-04 12:52:35 +05:30
parent 432502932a
commit 1bab89cdea
7 changed files with 208 additions and 10 deletions

View file

@ -28,6 +28,7 @@ from sqlalchemy.orm import Session
from app.db.database import get_db
from app.db.models import CommitModel, RepoModel, WorkTree
from app.services.worktree_probe import _probe_worktree
router = APIRouter(prefix="/worktrees", tags=["worktrees-v5"])
@ -180,6 +181,18 @@ class WorkTreeResponse(BaseModel):
model_config = {"from_attributes": True}
class WorkTreeProbe(BaseModel):
dirty_files: int
ahead: int
behind: int
last_commit_sha: str | None = None
last_commit_relative: str | None = None
class WorkTreeListEntry(WorkTreeResponse):
probe: WorkTreeProbe | None = None
# -- Endpoints ----------------------------------------------------------------
@ -249,7 +262,7 @@ def create_worktree(
return worktree
@router.get("", response_model=list[WorkTreeResponse])
@router.get("", response_model=list[WorkTreeListEntry])
def list_worktrees(
space_id: uuid.UUID = Query(..., description="Space UUID"),
include_closed: bool = Query(False, description="Include closed worktrees"),
@ -264,7 +277,23 @@ def list_worktrees(
)
if not include_closed:
stmt = stmt.where(WorkTree.status == "active")
return list(db.scalars(stmt).all())
entries = []
for worktree in db.scalars(stmt).all():
probe = None
if worktree.status == "active":
probed = _probe_worktree(
str(worktree.id),
worktree.path,
worktree.branch_name,
)
if probed:
probe = WorkTreeProbe(**probed)
entries.append(
WorkTreeListEntry.model_validate(worktree).model_copy(
update={"probe": probe},
)
)
return entries
@router.get("/{worktree_id}", response_model=WorkTreeResponse)

View file

@ -173,6 +173,66 @@ def test_list_show_and_close_clean_worktree(client, tmp_path):
assert len(all_r.json()) == 1
def test_list_includes_probe_data_for_active_worktrees(client, tmp_path, monkeypatch):
git_repo = _init_git_repo(tmp_path)
space_id = _create_project_with_root(client, git_repo)
target = tmp_path / "probe-worktree"
created = _create_worktree(client, space_id, base_path=str(target)).json()
def fake_probe(worktree_id, path, branch):
assert worktree_id == created["id"]
assert path == str(target.resolve())
assert branch == created["branch_name"]
return {
"id": worktree_id,
"path": path,
"branch": branch,
"dirty_files": 3,
"ahead": 1,
"behind": 0,
"last_commit_sha": "abc1234",
"last_commit_relative": "5 minutes ago",
}
monkeypatch.setattr(worktrees, "_probe_worktree", fake_probe)
r = client.get(f"/api/v5/worktrees?space_id={space_id}")
assert r.status_code == 200, r.text
data = r.json()
assert data[0]["id"] == created["id"]
assert data[0]["probe"] == {
"dirty_files": 3,
"ahead": 1,
"behind": 0,
"last_commit_sha": "abc1234",
"last_commit_relative": "5 minutes ago",
}
def test_list_probe_null_for_closed_worktrees(client, tmp_path, monkeypatch):
git_repo = _init_git_repo(tmp_path)
space_id = _create_project_with_root(client, git_repo)
created = _create_worktree(
client,
space_id,
base_path=str(tmp_path / "closed-probe"),
).json()
close_r = client.delete(f"/api/v5/worktrees/{created['id']}")
assert close_r.status_code == 200, close_r.text
def fail_probe(worktree_id, path, branch):
raise AssertionError("closed worktrees should not be probed")
monkeypatch.setattr(worktrees, "_probe_worktree", fail_probe)
r = client.get(f"/api/v5/worktrees?space_id={space_id}&include_closed=true")
assert r.status_code == 200, r.text
assert r.json()[0]["id"] == created["id"]
assert r.json()[0]["probe"] is None
def test_show_nonexistent_worktree_returns_404(client):
r = client.get(f"/api/v5/worktrees/{uuid.uuid4()}")
assert r.status_code == 404

View file

@ -26,6 +26,32 @@ def _pretty_path(path: str | None) -> str | None:
return path
def format_worktree_dirty(worktree: dict) -> str:
"""Render the worktree dirty-file count, preserving dash on unknown rows."""
if worktree.get("status") == "closed":
return ""
probe = worktree.get("probe")
if not probe:
return ""
return str(probe.get("dirty_files", ""))
def format_worktree_ahead(worktree: dict) -> str:
"""Render compact ahead/behind drift for the existing AHEAD column."""
if worktree.get("status") == "closed":
return ""
probe = worktree.get("probe")
if not probe:
return ""
ahead = int(probe.get("ahead") or 0)
behind = int(probe.get("behind") or 0)
if ahead > 0:
return f"+{ahead}"
if behind > 0:
return f"-{behind}"
return "0"
def _relative_time(iso_ts: str) -> str:
"""Format an ISO-8601 UTC timestamp as a relative-time string."""
try:

View file

@ -62,6 +62,8 @@ from .formatters import (
format_review,
format_space_list,
format_state_brief,
format_worktree_ahead,
format_worktree_dirty,
)
@ -832,8 +834,8 @@ def _print_worktree_table(worktrees: list[dict]) -> None:
_short_id(str(w.get("id", ""))),
str(w.get("agent", "")),
str(w.get("branch_name", "")),
"",
"",
format_worktree_dirty(w),
format_worktree_ahead(w),
_display_path(str(w.get("path", ""))),
]
for w in worktrees
@ -1262,7 +1264,15 @@ def _build_parser() -> argparse.ArgumentParser:
wt_open.add_argument("--json", action="store_true")
wt_open.set_defaults(func=cmd_worktree_open)
wt_list = worktree_sub.add_parser("list", help="List worktrees for a space")
wt_list = worktree_sub.add_parser(
"list",
help="List worktrees for a space",
description=(
"List worktrees for a space. DIRTY shows dirty file count when "
"available. AHEAD shows +N when ahead of origin/main, -N when "
"behind, 0 when even, or — when unknown."
),
)
wt_list.add_argument("space", help="Space name or UUID")
wt_list.add_argument("--include-closed", action="store_true", help="Include closed worktrees")
wt_list.add_argument("--json", action="store_true")

View file

@ -29,6 +29,8 @@ from .formatters import (
format_review,
format_space_list,
format_state_brief,
format_worktree_ahead,
format_worktree_dirty,
)
@ -645,8 +647,8 @@ def _format_worktree_list(worktrees: list[dict]) -> str:
f"{short_id} | "
f"{worktree.get('agent', '')} | "
f"{worktree.get('branch_name', '')} | "
" | "
" | "
f"{format_worktree_dirty(worktree)} | "
f"{format_worktree_ahead(worktree)} | "
f"{worktree.get('path', '')} |"
)
return "\n".join(lines)
@ -700,9 +702,9 @@ def smriti_worktree_open(
def smriti_worktree_list(space: str, include_closed: bool = False) -> str:
"""List worktrees for a Smriti space.
Use this to see active agent worktree directories. Dirty/ahead columns
are placeholders in V1 and always render as unknown until a future git
status enrichment pass lands.
Use this to see active agent worktree directories. Dirty shows dirty
file count when available. Ahead shows +N when ahead of origin/main,
-N when behind, 0 when even, or when unknown.
Args:
space: Space name or UUID.

View file

@ -126,6 +126,65 @@ def test_cmd_worktree_list_calls_client(capsys: pytest.CaptureFixture[str]):
client.list_worktrees.assert_called_once_with("space-uuid", include_closed=False)
def test_list_renders_probe_data(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = {"id": "space-uuid", "name": "my-project"}
client.list_worktrees.return_value = [
_worktree_dict(
probe={
"dirty_files": 3,
"ahead": 1,
"behind": 0,
"last_commit_sha": "abc1234",
"last_commit_relative": "5 minutes ago",
},
)
]
args = argparse.Namespace(space="my-project", include_closed=False, json=False)
cli_main.cmd_worktree_list(client, args)
out = capsys.readouterr().out
assert "DIRTY" in out
assert "AHEAD" in out
assert "3" in out
assert "+1" in out
def test_list_renders_dash_when_probe_null(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = {"id": "space-uuid", "name": "my-project"}
client.list_worktrees.return_value = [_worktree_dict(probe=None)]
args = argparse.Namespace(space="my-project", include_closed=False, json=False)
cli_main.cmd_worktree_list(client, args)
out = capsys.readouterr().out
assert "" in out
def test_list_renders_negative_ahead_for_behind_only(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = {"id": "space-uuid", "name": "my-project"}
client.list_worktrees.return_value = [
_worktree_dict(
probe={
"dirty_files": 0,
"ahead": 0,
"behind": 2,
"last_commit_sha": "abc1234",
"last_commit_relative": "5 minutes ago",
},
)
]
args = argparse.Namespace(space="my-project", include_closed=False, json=False)
cli_main.cmd_worktree_list(client, args)
out = capsys.readouterr().out
assert "-2" in out
def test_cmd_worktree_show_calls_client(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.get_worktree.return_value = _worktree_dict()

View file

@ -59,6 +59,18 @@ def test_mcp_worktree_list_calls_client(mock_client):
)
def test_mcp_worktree_list_renders_probe_data(mock_client):
mock_client.resolve_space.return_value = {"id": "space-uuid", "name": "my-project"}
mock_client.list_worktrees.return_value = [
_worktree_dict(probe={"dirty_files": 3, "ahead": 0, "behind": 2})
]
result = mcp_server.smriti_worktree_list(space="my-project")
assert "3" in result
assert "-2" in result
def test_mcp_worktree_show_calls_client(mock_client):
mock_client.get_worktree.return_value = _worktree_dict()