132 lines
5.1 KiB
Python
132 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Git author census: extract author identities + commit counts from the worktree
|
|
git history, write a table, and (as 'check') verify the artifact against a fresh
|
|
recompile from `git log`. Pure extraction; no fabricated numbers.
|
|
"""
|
|
import csv
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = "/root/troupe-standup"
|
|
LEDGER_DIR = REPO + "/wizard-work/ledger"
|
|
CSV_NAME = "2026-10-30-git-author-census.csv"
|
|
MD_NAME = "2026-10-30-git-author-census.md"
|
|
ASN_DATE = "2026-10-30"
|
|
|
|
|
|
def git_author_census():
|
|
"""Recompute from git; returns {author_identity: count} with identity = 'Name <email>'."""
|
|
proc = subprocess.run(
|
|
["git", "log", "--format=%an|%ae"],
|
|
cwd=REPO,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
counts = {}
|
|
for raw in proc.stdout.splitlines():
|
|
if not raw:
|
|
continue
|
|
name, _, email = raw.partition("|")
|
|
key = "{} <{}>".format(name.strip(), email.strip())
|
|
counts[key] = counts.get(key, 0) + 1
|
|
return counts
|
|
|
|
|
|
def write_artifacts(counts):
|
|
total = sum(counts.values())
|
|
rows = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
os_dir = __import__("os")
|
|
os_dir.makedirs(LEDGER_DIR, exist_ok=True)
|
|
|
|
csv_path = LEDGER_DIR + "/" + CSV_NAME
|
|
with open(csv_path, "w", newline="") as f:
|
|
w = csv.writer(f)
|
|
w.writerow(["rank", "author", "commits"])
|
|
for i, (k, v) in enumerate(rows, 1):
|
|
w.writerow([i, k, v])
|
|
|
|
md_path = LEDGER_DIR + "/" + MD_NAME
|
|
with open(md_path, "w") as f:
|
|
f.write("# Git author census ({date})\n\n".format(date=ASN_DATE))
|
|
f.write("Source: `git log` of the troupe worktree (`d9e4a66`, {total} commits\n".format(total=total))
|
|
f.write("reachable from HEAD). Extracted {date}.\n\n".format(date=ASN_DATE))
|
|
f.write("Identity key is the author name plus email in `Name <email>` form as\n")
|
|
f.write("recorded by git; no canonicalization (e.g. `Timmy` vs `timmy`, or a\n")
|
|
f.write("stackchain.ai vs agents.invalid address, stays a distinct row).\n\n")
|
|
f.write("| rank | author | commits | pct |\n")
|
|
f.write("|---:|---|---:|---:|\n")
|
|
for i, (k, v) in enumerate(rows, 1):
|
|
pct = 100.0 * v / total if total else 0.0
|
|
f.write("| {rank} | {author} | {commits} | {pct:.2f} |\n".format(
|
|
rank=i, author=k, commits=v, pct=pct))
|
|
f.write("\n**Total: {total} commits** across {n} distinct author identities.\n".format(
|
|
total=total, n=len(rows)))
|
|
return csv_path, md_path, total
|
|
|
|
|
|
def verify(csv_path, md_path):
|
|
expected = git_author_census()
|
|
total = sum(expected.values())
|
|
with open(csv_path, newline="") as f:
|
|
rows = list(csv.reader(f))
|
|
errs = []
|
|
if not rows or rows[0] != ["rank", "author", "commits"]:
|
|
errs.append("CSV header wrong: {!r}".format(rows[0] if rows else None))
|
|
body = rows[1:]
|
|
if len(body) != len(expected):
|
|
errs.append("row count {} != author count {}".format(len(body), len(expected)))
|
|
for i, row in enumerate(body, 1):
|
|
if len(row) != 3:
|
|
errs.append("bad row width: {!r}".format(row)); continue
|
|
rank, author, commits = row
|
|
if str(i) != rank:
|
|
errs.append("rank mismatch at row {}: {!r}".format(i, rank))
|
|
exp = expected.get(author)
|
|
if exp is None:
|
|
errs.append("author not in recomputed census: {!r}".format(author))
|
|
elif str(exp) != commits:
|
|
errs.append("commits mismatch for {!r}: artifact {} expected {}".format(author, commits, exp))
|
|
# independent total from the commit object count
|
|
proc = subprocess.run(
|
|
["git", "rev-list", "--count", "HEAD"], cwd=REPO,
|
|
check=True, capture_output=True, text=True)
|
|
if proc.stdout.strip() != str(total):
|
|
errs.append("rev-list total {} != census total {}".format(proc.stdout.strip(), total))
|
|
# md should contain the total and the correct number of rows
|
|
md = open(md_path).read()
|
|
if "Total: {} commits".format(total) not in md:
|
|
errs.append("MD missing total line")
|
|
if str(len(expected)) not in md.split("across ")[-1].split(" distinct")[0]:
|
|
errs.append("MD row count wrong")
|
|
|
|
if errs:
|
|
print("FAIL:")
|
|
for e in errs:
|
|
print(" - " + e)
|
|
return 1
|
|
print("PASS: {} author identities, {} commits; {} CSV rows + MD match a fresh "
|
|
"recompile from `git log` and `git rev-list --count HEAD`.".format(
|
|
len(expected), total, len(body)))
|
|
return 0
|
|
|
|
|
|
def main():
|
|
mode = sys.argv[1] if len(sys.argv) > 1 else "check"
|
|
if mode == "generate":
|
|
csv_path, md_path, total = write_artifacts(git_author_census())
|
|
print("wrote", csv_path)
|
|
print("wrote", md_path)
|
|
print("total commits:", total)
|
|
return 0
|
|
if mode == "check":
|
|
csv_path = LEDGER_DIR + "/" + CSV_NAME
|
|
md_path = LEDGER_DIR + "/" + MD_NAME
|
|
return verify(csv_path, md_path)
|
|
print("usage: check_git_author_census.py [generate|check]", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|