Log in
scripts/oak_field_map.py 161 lines · 5.6 KB · blame Source
88e86b3faccd Rebuild benchmarks as a clean single-root reposi 2 months ago
1
#!/usr/bin/env python3
2
"""Render a compact Oak branch field map from local isolated clones.
3
4
The tool is intentionally read-only: it shells out to `oak status`, remote
5
branch listing, and local log only. It does not pull, switch, close, or merge.
6
Use fresh/disposable worker clones when the exact latest main log matters.
7
"""
8
9
from __future__ import annotations
10
11
import argparse
12
import json
13
import subprocess
14
from pathlib import Path
15
from typing import Any, Optional
16
17
18
def _run_json(repo: Path, command: list[str]) -> tuple[Optional[Any], Optional[str]]:
19
    try:
20
        proc = subprocess.run(
21
            command,
22
            cwd=repo,
23
            check=False,
24
            text=True,
25
            stdout=subprocess.PIPE,
26
            stderr=subprocess.PIPE,
27
        )
28
    except OSError as exc:
29
        return None, str(exc)
30
    if proc.returncode != 0:
31
        return None, (proc.stderr or proc.stdout or f"returncode={proc.returncode}")[-2000:]
32
    try:
33
        return json.loads(proc.stdout or "null"), None
34
    except json.JSONDecodeError as exc:
35
        return None, f"invalid_json:{exc}"
36
37
38
def _short_hash(value: Any) -> str:
39
    text = str(value or "")
40
    return text[:12] if text else ""
41
42
43
def _desc_line(value: Any) -> str:
44
    if not isinstance(value, str):
45
        return ""
46
    for line in value.splitlines():
47
        stripped = line.strip()
48
        if stripped:
49
            return stripped
50
    return ""
51
52
53
def inspect_repo(repo: Path) -> dict[str, Any]:
54
    repo = repo.resolve()
55
    status, status_error = _run_json(repo, ["oak", "status", "--json"])
56
    open_branches, branch_error = _run_json(
57
        repo, ["oak", "branch", "list", "--remote", "--status", "open", "--json"]
58
    )
59
    log, log_error = _run_json(repo, ["oak", "log", "-n", "8", "--json"])
60
    open_branch_rows = open_branches if isinstance(open_branches, list) else []
61
    log_rows = log if isinstance(log, list) else []
62
    non_main_open = [
63
        row for row in open_branch_rows
64
        if isinstance(row, dict) and row.get("name") != "main"
65
    ]
66
    main = next(
67
        (
68
            row for row in open_branch_rows
69
            if isinstance(row, dict) and row.get("name") == "main"
70
        ),
71
        None,
72
    )
73
    return {
74
        "path": str(repo),
75
        "label": repo.name,
76
        "status": status if isinstance(status, dict) else None,
77
        "status_error": status_error,
78
        "branch_error": branch_error,
79
        "log_error": log_error,
80
        "remote_main": main,
81
        "remote_open_branches": open_branch_rows,
82
        "remote_non_main_open_branches": non_main_open,
83
        "recent_log": log_rows,
84
    }
85
86
87
def inspect_repos(paths: list[Path]) -> list[dict[str, Any]]:
88
    return [inspect_repo(path) for path in paths]
89
90
91
def render_markdown(repos: list[dict[str, Any]]) -> str:
92
    lines = ["# Oak Field Map", ""]
93
    for repo in repos:
94
        status = repo.get("status") or {}
95
        remote_main = repo.get("remote_main") or {}
96
        non_main = repo.get("remote_non_main_open_branches") or []
97
        lines.append(f"## {repo['label']}")
98
        lines.append("")
99
        lines.append(f"- path: `{repo['path']}`")
100
        lines.append(f"- local branch: `{status.get('branch') or 'unknown'}`")
101
        lines.append(f"- local head: `{_short_hash(status.get('head')) or 'unknown'}`")
102
        lines.append(f"- local unmerged commits: `{status.get('unmerged_commit_count', 'unknown')}`")
103
        lines.append(f"- local dirty files: `{len(status.get('changes') or [])}`")
104
        lines.append(f"- remote main: `{_short_hash(remote_main.get('head')) or 'unknown'}`")
105
        if repo.get("status_error"):
106
            lines.append(f"- status error: `{repo['status_error']}`")
107
        if repo.get("branch_error"):
108
            lines.append(f"- branch-list error: `{repo['branch_error']}`")
109
        if repo.get("log_error"):
110
            lines.append(f"- log error: `{repo['log_error']}`")
111
        lines.append("")
112
        lines.append("Open non-main remote branches:")
113
        if non_main:
114
            for branch in non_main:
115
                desc = _desc_line(branch.get("description"))
116
                desc_suffix = f" - {desc}" if desc else ""
117
                lines.append(
118
                    f"- `{branch.get('name')}` `{_short_hash(branch.get('head'))}`{desc_suffix}"
119
                )
120
        else:
121
            lines.append("- none")
122
        lines.append("")
123
        lines.append("Recent local log:")
124
        recent = repo.get("recent_log") or []
125
        if recent:
126
            for entry in recent[:8]:
127
                desc = entry.get("description_or_subject") or ""
128
                lines.append(
129
                    f"- `{_short_hash(entry.get('hash'))}` `{entry.get('branch')}` {desc}"
130
                )
131
        else:
132
            lines.append("- unavailable")
133
        lines.append("")
134
    return "\n".join(lines).rstrip() + "\n"
135
136
137
def main(argv: Optional[list[str]] = None) -> int:
138
    parser = argparse.ArgumentParser(description="Read-only Oak remote/local field map")
139
    parser.add_argument(
140
        "--repo",
141
        action="append",
142
        required=True,
143
        help="Path to an existing local Oak clone. Pass once per repo.",
144
    )
145
    parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
146
    args = parser.parse_args(argv)
147
148
    repos = inspect_repos([Path(path) for path in args.repo])
149
    if args.json:
150
        print(json.dumps({"schema_version": 1, "repos": repos}, indent=2, sort_keys=True))
151
    else:
152
        print(render_markdown(repos), end="")
153
    errors = [
154
        repo for repo in repos
155
        if repo.get("status_error") or repo.get("branch_error") or repo.get("log_error")
156
    ]
157
    return 1 if errors else 0
158
159
160
if __name__ == "__main__":
161
    raise SystemExit(main())