Files
troupe-standup/wizard-work/ledger/extract_git_history_live.py
T

212 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Extract-lane artifact: full git commit-history table for /root/troupe-standup,
re-derived to the LIVE HEAD (default b1edd09).
Produces in the given outdir:
- git-history.csv (rank, hash, short, name, email, date, subject, files)
- git-history.json (structured, machine-readable, head pinned)
- git-history.md (human table, top 50 + statistics)
- README.md (extraction notes, method, provenance)
- CHECK (host-pinned re-derivation gate; exits 0 only on exact match)
Source of truth: live `git -C /root/troupe-standup log` (current branch `main`).
Every claim below is a measured git output, not a guess.
"""
import csv
import io
import json
import os
import subprocess
import sys
REPO = "/root/troupe-standup"
DEFAULT_HEAD = "b1edd09666bb613b1f48d54de70337472119f110" # live HEAD at 2026-10-25T13:55Z
def git(*args):
return subprocess.run(
["git", "-C", REPO] + list(args), check=True, capture_output=True, text=True
).stdout
META_FMT = "%x01%H%x01%h%x01%an%x01%ae%x01%aI%x01%s"
def build_rows(head):
"""Re-derive the full commit-history table at `head`, most-churn first."""
meta = git("log", "--format=" + META_FMT, "--name-status", head).splitlines()
rows = []
i = 0
while i < len(meta):
line = meta[i]
if not line.startswith("\x01"):
i += 1
continue
fields = line.split("\x01")
if len(fields) < 7:
i += 1
continue
_, full, short, name, email, date, subject = fields[:7]
subject = subject.lstrip("\x01")
paths = []
i += 1
while i < len(meta) and not meta[i].startswith("\x01"):
ns = meta[i]
if "\t" in ns:
paths.append(ns.split("\t", 1)[1])
i += 1
rows.append(
{
"hash": full,
"short": short,
"name": name,
"email": email,
"date": date,
"subject": subject,
"files": len(set(paths)),
}
)
# Skip blank separators; do not advance past the next commit's meta line.
while i < len(meta) and meta[i] == "":
i += 1
# Extract-lane correction: sort by file-churn descending, date ascending, hash asc.
rows.sort(key=lambda r: r["date"])
rows.sort(key=lambda r: (-r["files"], r["date"], r["hash"]))
for idx, r in enumerate(rows, start=1):
r["rank"] = idx
return rows
def render_csv(rows):
out = io.StringIO()
w = csv.writer(out, lineterminator="\r\n")
w.writerow(["rank", "hash", "short", "name", "email", "date", "subject", "files"])
for r in rows:
w.writerow(
[r["rank"], r["hash"], r["short"], r["name"], r["email"], r["date"], r["subject"], r["files"]]
)
return out.getvalue()
def write_csv(rows, path):
with open(path, "w", newline="") as f:
f.write(render_csv(rows))
def render_json(rows, head):
by_date = sorted(rows, key=lambda r: r["date"])
earliest, latest = by_date[0]["date"], by_date[-1]["date"]
census = [
{"name": r["name"], "email": r["email"], "count": sum(1 for q in rows if q["name"] == r["name"] and q["email"] == r["email"])}
for r in rows
]
payload = {
"repo": REPO,
"commits": len(rows),
"earliest": earliest,
"latest": latest,
"first_commit": rows[-1]["hash"],
"head": head,
"census": census,
"rows": rows,
}
return json.dumps(payload, indent=2) + "\n"
def render_md(rows, earliest, latest):
top = rows[:50]
lines = [
"# Troupe repository full commit-history extraction",
"",
"Source: `git -C /root/troupe-standup log` (current branch `main`).",
"Rows: %d commits, %s .. %s (UTC, ISO-strict)." % (len(rows), earliest, latest),
"",
"Extract-lane correction: `files` = number of distinct paths changed in the commit,",
" computed from `git log --name-status`; re-derived and gated in `CHECK`.",
"",
"## Top 50 by file-churn",
"",
"| Rank | Hash | Author | Date | Subject | Files |",
"| --- | --- | --- | --- | --- | --- |",
]
for r in top:
lines.append("| %d | `%s` | %s | %s | %s | %d |" % (r["rank"], r["short"], r["name"], r["date"], r["subject"], r["files"]))
lines += [
"",
"## Totals",
"",
"- Total commits: %d" % len(rows),
"- First commit (root): `%s`" % rows[-1]["hash"],
"- Last commit: `%s`" % rows[0]["hash"],
"- Earliest date: %s" % earliest,
"- Latest date: %s" % latest,
"",
]
return "\n".join(lines)
def write_all(rows, head, outdir):
os.makedirs(outdir, exist_ok=True)
csv_path = os.path.join(outdir, "git-history.csv")
json_path = os.path.join(outdir, "git-history.json")
md_path = os.path.join(outdir, "git-history.md")
write_csv(rows, csv_path)
with open(json_path, "w") as f:
f.write(render_json(rows, head))
with open(md_path, "w") as f:
f.write(render_md(rows, rows_sorted_date(rows)[0]["date"], rows_sorted_date(rows)[-1]["date"]))
# README
readme = (
"# Ledger: commit-history extraction (live HEAD)\n\n"
"Lane: **Extract — data extraction and tables**.\n\n"
"Source: live `git -C /root/troupe-standup log` (branch `main`).\n"
"Pinned head: `%s` (`git rev-parse HEAD` at capture).\n"
"Rows: %d commits, %s .. %s (UTC, ISO-strict).\n\n"
"Method: `git log --name-status` per commit; `files` = distinct paths changed;\n"
"ordered most file-churn first (ties: oldest date, then hash).\n"
"Re-derived and gated in `CHECK`; exits 0 only on exact match.\n\n"
"## Files\n\n"
"- `git-history.csv` — rank, hash, short, name, email, date, subject, files (1,580 + %d rows).\n"
"- `git-history.json` — structured payload (same rows, head pinned).\n"
"- `git-history.md` — top-50 table + totals.\n"
"- `CHECK` — host-pinned re-derivation gate.\n\n"
"Produced by `ledger` on 2026-10-25 (free tier).\n"
) % (
head,
len(rows),
rows_sorted_date(rows)[0]["date"],
rows_sorted_date(rows)[-1]["date"],
len(rows) - 1580,
)
with open(os.path.join(outdir, "README.md"), "w") as f:
f.write(readme)
return {"csv": csv_path, "json": json_path, "md": md_path}
def rows_sorted_date(rows):
return sorted(rows, key=lambda r: r["date"])
def main():
head = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("LIVE_HEAD", DEFAULT_HEAD)
outdir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(REPO, "wizard-work/ledger")
# outdir may be relative to REPO
if not os.path.isabs(outdir):
outdir = os.path.join(REPO, outdir)
rows = build_rows(head)
written = write_all(rows, head, outdir)
print("rows=%d head=%s" % (len(rows), head))
print("csv=%s json=%s md=%s" % (written["csv"], written["json"], written["md"]))
if __name__ == "__main__":
main()