136 lines
4.1 KiB
Python
136 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the ledger tracked-file census for /root/troupe-standup.
|
|
|
|
Reads the live git index (git ls-files), then writes:
|
|
|
|
wizard-work/ledger/ledger-tracked-files.csv one data row per tracked file
|
|
wizard-work/ledger/ledger-tracked-files.md mode counts + row tally
|
|
|
|
Every field is derived from real tool output (git ls-files / git cat-file -s);
|
|
nothing is fabricated here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import hashlib
|
|
from collections import Counter
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from datetime import datetime, timezone
|
|
|
|
REPO = "/root/troupe-standup"
|
|
OUTDIR = os.path.join(REPO, "wizard-work", "ledger")
|
|
CSV_PATH = os.path.join(OUTDIR, "ledger-tracked-files.csv")
|
|
SUMMARY_PATH = os.path.join(OUTDIR, "ledger-tracked-files.md")
|
|
|
|
BAD_CHARS = {"\t", ",", "\"", "\n", "\r"}
|
|
|
|
|
|
def git(*args: str) -> str:
|
|
return subprocess.run(
|
|
["git", "-C", REPO] + list(args), check=True,
|
|
capture_output=True, text=True,
|
|
).stdout
|
|
|
|
|
|
def cat_file_size(sha1: str) -> int:
|
|
out = subprocess.run(
|
|
["git", "cat-file", "-s", sha1], check=True,
|
|
capture_output=True, text=True,
|
|
).stdout
|
|
return int(out.split()[0])
|
|
|
|
|
|
def main() -> None:
|
|
os.makedirs(OUTDIR, exist_ok=True)
|
|
|
|
paths = git("ls-files").splitlines()
|
|
paths.sort()
|
|
n = len(paths)
|
|
print(f"tracked files: {n}", file=sys.stderr)
|
|
|
|
# Index metadata: path -> (sha1, mode, stage)
|
|
sha1_of_path = {}
|
|
mode_of_sha1 = {}
|
|
stage_of_path = {}
|
|
for line in git("ls-files", "-s").splitlines():
|
|
head, sep, path = line.partition("\t")
|
|
if not sep:
|
|
continue
|
|
mode, sha1, stage = head.split()
|
|
sha1_of_path[path] = sha1
|
|
mode_of_sha1[sha1] = mode
|
|
stage_of_path[path] = stage
|
|
|
|
# Object length for each sha1 (git cat-file -s), parallelised.
|
|
def size_at(i: int) -> int:
|
|
return cat_file_size(sha1_of_path[paths[i]])
|
|
|
|
lengths = [0] * n
|
|
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
future_to_i = {pool.submit(size_at, i): i for i in range(n)}
|
|
for fut in future_to_i:
|
|
lengths[future_to_i[fut]] = fut.result()
|
|
|
|
# Guard: raw paths must not contain CSV-special characters, else the
|
|
# row text would not round-trip through a plain one-line-per-row file.
|
|
weird = [p for p in paths if any(ch in BAD_CHARS for ch in p)]
|
|
if weird:
|
|
print(f"ABORT: paths contain CSV-special chars: {weird[:5]}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
rows = []
|
|
mode_counter = Counter()
|
|
for i, p in enumerate(paths, start=1):
|
|
sha1 = sha1_of_path[p]
|
|
mode = mode_of_sha1[sha1]
|
|
stage = stage_of_path[p]
|
|
mode_counter[mode] += 1
|
|
rows.append(f"{i},{p},{mode},{sha1},{lengths[i-1]},{stage}")
|
|
|
|
with open(CSV_PATH, "w", encoding="utf-8", newline="\n") as fh:
|
|
fh.write("row,path,mode,sha1,length_bytes,stage\n")
|
|
fh.write("\n".join(rows))
|
|
fh.write("\n")
|
|
|
|
md5 = hashlib.md5(open(CSV_PATH, "rb").read()).hexdigest()
|
|
print(f"wrote {CSV_PATH} md5={md5}", file=sys.stderr)
|
|
|
|
lines = [
|
|
"# Ledger: tracked-file census",
|
|
"",
|
|
f"- Generated: {datetime.now(timezone.utc).isoformat()}Z",
|
|
f"- Repository: {REPO}",
|
|
f"- Tracked files (git ls-files): {n}",
|
|
"",
|
|
"## Mode counts",
|
|
"",
|
|
"| mode | count |",
|
|
"| --- | --- |",
|
|
]
|
|
for m in sorted(mode_counter):
|
|
lines.append(f"| {m} | {mode_counter[m]} |")
|
|
lines += [
|
|
"",
|
|
"## Row tally",
|
|
"",
|
|
f"Total data rows: {n}",
|
|
"",
|
|
"## Verification",
|
|
"",
|
|
"A companion CHECK (printed in the standup note) diffs the CSV data rows, sorted,",
|
|
"against `git ls-files | sort`; it must be empty, and the data-row count must equal",
|
|
"the live `git ls-files | wc -l` count.",
|
|
"",
|
|
]
|
|
with open(SUMMARY_PATH, "w", encoding="utf-8", newline="\n") as fh:
|
|
fh.write("\n".join(lines))
|
|
|
|
print(f"wrote {SUMMARY_PATH}", file=sys.stderr)
|
|
print(f"modes: {dict(mode_counter)}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|