Files
BFM-decomp/tools/public_rewrite/hash_dict.py
T
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

105 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""hash_dict.py — the dictionary of EVERY old commit hash → its inert token (P33 C1/C2).
tools/public_rewrite/hash_dict.py [--repo PATH] [--write-mailmap]
Writes .run/public_rewrite/dict.json (private: it holds every old hash):
* every commit OBJECT in the store — reachable or not (`git cat-file --batch-all-objects`), because a doc may cite a
commit that was later rewritten away (S86: 119 unreachable commit objects; one cited by tools/verify_worktree.py);
* main's commits → ordinal 1..N in `git rev-list --reverse main` order (token `commit:NNNN`, zero-padded to 4);
* every other commit → the ordinal of its TWIN on main — same (tree, author timestamp, subject); the S76 tag lineage
is 126 such re-authored twins — else `orphan-K`;
* the prefix index (every prefix 7..40 of every hash) is rebuilt on load; ambiguous prefixes (two commits sharing
one) are recorded and MUST be 0 (measured 0 at 7 chars over 4,420 objects); prefixes that are also prefixes of a
cited CONTENT hash (the manifest / check.*.sha / dumps sha1s, the sha256 checksums, the redump CRC32) are recorded
as `excluded` — the scrub leaves such tokens untouched (expected < 1; R41 prints the count).
`--write-mailmap` also writes .run/public_rewrite/mailmap from the history's identities (never a literal anywhere).
"""
import argparse
import collections
import datetime as dt
import json
import sys
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
import common as C # noqa: E402
def build(repo):
objs = [oid for oid, typ, _ in C.iter_all_objects(repo, types=("commit",))]
main = C.git(["rev-list", "--reverse", "main"], repo).split()
ordinal = {h: i + 1 for i, h in enumerate(main)}
cf = C.CatFile(repo)
meta = {}
for oid in objs:
_, typ, raw = cf.get(oid)
d = C.parse_commit(raw)
subject = d["message"].split(b"\n", 1)[0]
meta[oid] = (d["tree"], d["author"][2], subject)
cf.close()
twin_key = {}
for h in main: # first main commit with a key wins
twin_key.setdefault(meta[h], ordinal[h])
commits, n_twin, n_orphan = {}, 0, 0
orphans = sorted(h for h in objs if h not in ordinal and meta[h] not in twin_key)
orphan_no = {h: i + 1 for i, h in enumerate(orphans)}
for h in objs:
if h in ordinal:
commits[h] = {"kind": "main", "ord": ordinal[h]}
elif meta[h] in twin_key:
commits[h] = {"kind": "twin", "ord": twin_key[meta[h]]}
n_twin += 1
else:
commits[h] = {"kind": "orphan", "orphan": orphan_no[h]}
n_orphan += 1
# prefix index → ambiguity
index = {}
ambiguous = set()
for h in objs:
for p in C.prefixes_of(h):
if p in index and index[p] != h:
ambiguous.add(p)
index.setdefault(p, h)
# collisions with cited content hashes
content = C.cited_content_hashes()
excluded = sorted(p for h in content for p in C.prefixes_of(h) if p in index)
return {
"built_from": {"repo": str(repo), "head": C.git(["rev-parse", "HEAD"], repo).strip(),
"date": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds")},
"main_count": len(main),
"commits": commits,
"ambiguous": sorted(ambiguous),
"excluded": excluded,
"stats": {"objects": len(objs), "main": len(main), "twins": n_twin, "orphans": n_orphan,
"prefixes": len(index), "ambiguous": len(ambiguous), "excluded": len(excluded),
"content_hashes": len(content)},
}
def main(argv):
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--repo", default=str(C.REPO))
ap.add_argument("--write-mailmap", action="store_true")
a = ap.parse_args(argv)
C.SCRATCH.mkdir(parents=True, exist_ok=True)
d = build(a.repo)
C.DICT_FILE.write_text(json.dumps(d, indent=0), encoding="utf-8")
s = d["stats"]
print(f"hash_dict: {s['objects']} commit objects ({s['main']} on main, {s['twins']} twins, {s['orphans']} orphans), "
f"{s['prefixes']} prefixes 7..40, {s['ambiguous']} ambiguous, {s['excluded']} collide with {s['content_hashes']} "
f"cited content hashes -> {C.DICT_FILE}")
if d["excluded"]:
print(" excluded (left untouched by the scrub): " + ", ".join(d["excluded"][:20]))
if a.write_mailmap:
ids = C.write_mailmap(repo=a.repo)
print(f"hash_dict: mailmap written ({len(ids)} personal identities -> the noreply identity) -> {C.MAILMAP_FILE}")
if d["ambiguous"]:
print(f"hash_dict: {len(d['ambiguous'])} AMBIGUOUS prefixes (scrub would emit commit:amb-N) — FAIL, investigate",
file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))