Files
Drew T fca3839049 feat(phase-33): C1 the history-rewrite tooling — tools/public_rewrite/ (git-filter-repo 2.47.0), PROVEN by two trial rewrites on a scratch bare clone
- hash_dict (4,420 commit objects -> commit:NNNN / twin / orphan; 150,280 prefixes; 0 ambiguous; 0 collisions with 1,480
  cited content hashes; scratch mailmap), scrub (12/12 self-test; HEAD sample 731 distinct tokens == git's own lookup),
  gate_scan (+ expected_offenders.txt fixture: the R39 negative control, PASS over 16.8 GB in 2 m 25 s; rom_blob_ids =
  content/signature hits U purge-path blobs not shared with any other path), run_filter (module API; --force only for the
  deleted tag), verify_rewrite (pairwise proof + no purge path survives + pruned set == derived purge-only set),
  build_commit_map (0 old hashes asserted; unchanged commits exempted), resolve_tokens, absent_scan (positive control on the
  current repo: FAIL 82,362), probe_github.sh (skips unchanged commits)
- trial #1 found two defects the plan had not foreseen: the EMPTY blob in the strip list (--strip-blobs-with-ids then
  undid every "file emptied" change in history and pruned a restore commit) and the byte-identical Initial commit keeping
  its hash; both fixed with controls
- trial #2: filter 269 s, exactly 1 pruned, verify 4,029 pairs / 0 failures, map 4,030 rows, absent_scan + gate PASS on
  the clone, 1,231 tokens resolved at a trial tip (residue 7: the pruned commit x3, orphans x4), aggressive repack 500 -> 80 MB
- SETUP P33 C1 section + rows (R21); runbook §3/§5/§6/§10 measured; requirements-python.txt; CURRENT_PHASE -> NEXT = C2
2026-09-06 22:37:10 -06:00

77 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""build_commit_map.py — the public ordinal → new-hash map, and the private old → new map (P33 C7).
tools/public_rewrite/build_commit_map.py [--new .run/public_rewrite/repo.git]
Writes docs/commit-map.tsv (PUBLIC): one row per commit of the OLD main in order — `ordinal new_hash author_date
committer_date subject` — with NO old hash anywhere (asserted: the scrub over the file's own bytes makes 0
replacements). The pruned purge-only commit keeps its row with `new_hash` = 40 zeros so the ordinals stay dense and
every `commit:NNNN` token in history has a row. Dates/subjects are read from the NEW commits (already scrubbed by the
rewrite); the pruned row's from the old commit, scrubbed here.
Writes .run/public_rewrite/old-to-new.tsv (PRIVATE, for probe_github.sh): `ordinal old_hash new_hash`.
"""
import argparse
import sys
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
import common as C # noqa: E402
import scrub # noqa: E402
def main(argv):
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--new", default=str(C.CLONE_DIR))
ap.add_argument("--old", default=str(C.REPO))
ap.add_argument("--out", default=str(C.COMMIT_MAP_PUBLIC), help="the public map path (a scratch path for a trial run)")
ap.add_argument("--private-out", default=str(C.OLD_TO_NEW_FILE))
a = ap.parse_args(argv)
out_pub, out_priv = __import__("pathlib").Path(a.out), __import__("pathlib").Path(a.private_out)
new_repo = __import__("pathlib").Path(a.new)
import verify_rewrite
cmap = verify_rewrite.commit_map(new_repo)
d = C.load_dict()
old_main = [h for h, e in sorted(d["commits"].items(), key=lambda kv: kv[1].get("ord", 0)) if e["kind"] == "main"]
if len(old_main) != d["main_count"]:
C.die(f"dictionary main count mismatch ({len(old_main)} vs {d['main_count']})")
s = scrub.Scrubber()
rows, priv = [], []
for h in old_main:
o = d["commits"][h]["ord"]
new = cmap.get(h)
if new is None:
C.die(f"old main commit ordinal {o} ({h[:9]}) is not in the commit-map — the clone did not hold the same main")
if new == C.ZEROS:
ad, cd, subj = C.git(["log", "-1", "--format=%aI%x00%cI%x00%s", h], a.old).rstrip("\n").split("\0")
subj = s.scrub_text(subj.encode()).decode("utf-8", "replace")
else:
ad, cd, subj = C.git(["log", "-1", "--format=%aI%x00%cI%x00%s", new], new_repo).rstrip("\n").split("\0")
rows.append(f"{o:04d}\t{new}\t{ad}\t{cd}\t{subj}")
priv.append(f"{o:04d}\t{h}\t{new}")
header = ("# docs/commit-map.tsv — the public history's commit map (P33 C7). Generated by tools/public_rewrite/build_commit_map.py; never edit.\n"
"# Before the public flip the history was rewritten (ROM-derived paths purged, personal addresses mapped, old commit hashes\n"
"# in every historical doc replaced by the inert token commit:NNNN). NNNN is the ordinal of the commit on main in the ORIGINAL\n"
"# order (1 = the first commit); this table maps it to the rewritten commit. A row of 40 zeros is the one commit that touched\n"
"# only purged paths and was pruned. Documents at the tip cite the new hashes directly (tools/public_rewrite/resolve_tokens.py).\n"
"ordinal\tnew_hash\tauthor_date\tcommitter_date\tsubject\n")
text = header + "\n".join(rows) + "\n"
# a commit the rewrite left byte-identical (old == new: e.g. the noreply-authored "Initial commit", which cites no
# hash and touches no purge path) legitimately keeps its hash — exempt those from the old-hash assertion
unchanged = {o for o, n in cmap.items() if o == n}
d2 = {"commits": {h: e for h, e in d["commits"].items() if h not in unchanged}, "ambiguous": d["ambiguous"],
"excluded": d["excluded"]}
s2 = scrub.Scrubber(d2)
out = s2.scrub_text(text.encode())
if s2.stats["replaced"] or s2.stats["emails"]:
C.die(f"the public map would carry {s2.stats['replaced']} old-hash tokens / {s2.stats['emails']} addresses — refusing")
(C.SCRATCH / "unchanged_commits.txt").write_text("\n".join(sorted(unchanged)) + ("\n" if unchanged else ""), encoding="utf-8")
out_pub.write_text(text, encoding="utf-8")
out_priv.write_text("ordinal\told_hash\tnew_hash\n" + "\n".join(priv) + "\n", encoding="utf-8")
zeros = sum(1 for r in rows if "\t" + C.ZEROS + "\t" in r)
print(f"build_commit_map: {len(rows)} rows -> {out_pub} ({zeros} pruned rows); private old-to-new -> {out_priv}; "
f"0 old-hash tokens in the public file (asserted; {len(unchanged)} commits unchanged by the rewrite keep their hash)")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))