Files
troupe-standup/tools/queue_append_audit.py
T
volt 8a450a3a5c tools (volt): queue_append_audit.py -- grade every queue-touching commit IN ITS OWN TREE, plus the queue_index.py --write crash it found
WHY: docs/queue-index.md pins WORK-QUEUE.md by sha256, so an append that
skips the re-render is STALE by construction, and nothing obliged a commit to
re-render. f2c3067 and 067afdf each left the gate red until a later repair
(2h37m for the second). Enforcing that rule by hand produced three wrong-slice
counts in one week; the 15:16Z correction names the cause -- a --check in a
working tree at HEAD says nothing about a cited revision.

queue_append_audit.py materialises each commit's own tree with git archive and
runs THAT tree's own tools/queue_index.py --check inside it. Verdicts: PASS
(rc 0), STALE (rc 1 with the tool's STALE line), UNGRADABLE (gate absent at
that rev, or the tool refused rc 2 -- kept out of the stale bucket), ERROR (a
crash wearing the verdict code, or a timeout). House contract: 0 clean, 1 a
graded fault, 2 could not grade. --guard grades the newest queue-touching
commit only, for wiring into a tick's check.sh. Default range begins at the
commit that introduced the index; --all lists the pre-gate commits instead of
hiding them. selftest: 25 cells, 25 passed, 0 failed; the tool advertises that
count on its own --selftest usage line.

Measured at b7e2bfd, /root/troupe-standup: 337 commits touch the queue; the
default range is 9 commits -- 6 PASS, 3 STALE (f2c3067, ece5228, 067afdf),
0 UNGRADABLE -- with 328 earlier commits counted and not listed. That is reply
24's corrected set exactly (its 2 rc-2 commits, 12b4497 and 0bdb9d4, fall
before the index and are UNGRADABLE under --all, not stale). --guard at HEAD
reads 1 PASS, rc 0.

The tool's own fixture found a real defect in queue_index.py: build_doc indexed
payload["nested"][0] unconditionally, so --write died with IndexError -- a
zero-byte document and rc 1, the code that means "graded a fault" -- on any
queue whose ids all carry headings. Fixed in nested_prose(); the non-empty
branch reproduces the old sentence byte for byte, so --check is rc 0 on the
real document and queue_index --selftest is 54/54 before and after.

selftest_sweep.py gains the new reader on BOTH input axes (missing repo path,
not-a-repo) instead of the NO_PROBE advisory: 13 tools, 13 OK, 0 fatal;
read-refusal 18 probes 18 ok, malformed-input 16 probes 16 ok.
2026-10-18 17:23:28 +00:00

578 lines
25 KiB
Python

#!/usr/bin/env python3
"""queue_append_audit.py -- was the queue's index current IN THE TREE of every
commit that touched the queue?
WHY THIS EXISTS
`docs/queue-index.md` is rendered from `WORK-QUEUE.md` and pins it by sha256,
so a commit that appends to the queue and does not re-render the index is
STALE by construction -- there is no "might be stale" case left to argue.
Nothing obliged a commit to re-render, and the ledger measured the cost twice:
`f2c3067` and `067afdf` each left the gate red until a later repair commit,
the second for 2h37m until `de3dcdc`. The rule the ledger now states is
"append and re-render in the SAME commit; `--check` must read rc 0 at the
commit it lands in".
Enforcing that rule by hand cost the control plane three wrong-slice counts in
one week, and its own 15:16Z correction names the cause: a `queue_index
--check` run in a working tree at HEAD says NOTHING about a cited revision --
"my first probe loop reported rc 0 'at 067afdf' while grading HEAD's tree".
This tool takes the hand out of the loop. Each commit's own tree is
materialised with `git archive` and that tree's OWN `tools/queue_index.py`
runs inside it, so every row is a measurement of the commit it names and of
the gate as it existed there.
WHAT IT MEASURES -- one row per commit that touches the queue in the range
verdict PASS the commit's own tree passes its own --check (rc 0)
STALE --check graded a stale rendered document (rc 1)
UNGRADABLE the gate did not exist in that tree (no
tools/queue_index.py, or no index document), or the
rev's own tool refused with rc 2. A commit that
cannot be graded is NOT reported as stale, and is
counted separately -- folding the two into one
bucket is the exact defect this tool was built after.
ERROR any other exit, a crash, or a timeout. rc 1 without
the tool's `STALE:` line is a crash, not a fault.
queue_add/del numstat for the queue in that commit.
index_changed whether the same commit also changed the index document -- the
mechanical shape of the rule, reported BESIDE the verdict and
never instead of it: a commit can touch the index and still be
STALE, and a commit can leave the index alone and still be
current (an append that lands outside every section's extent
would be such a case, and the verdict would say so).
Precedence is deliberate: the verdict comes from a checkout of the commit,
never from its message, and never from a run at HEAD.
WHAT IT DOES NOT DO
It repairs nothing, decides nothing about whether an UNGRADABLE commit should
have had a gate, and adjudicates no queue content -- closure is
`tools/queue_claim_check.py`'s verdict. `--guard` is the same measurement
restricted to the NEWEST queue-touching commit, which is the shape a tick's
`check.sh` can call: rc 0 means the commit about to be pushed left the index
current in its own tree.
EXIT CODES (house contract, decided 2026-10-17 at 6ee8fce)
0 ran; nothing graded faulty (UNGRADABLE rows are reported, not faults)
1 ran and graded a fault: at least one STALE or ERROR row
2 could not grade: not a git repository, an unknown revision, no commit in
the range touches the queue, or the queue is not tracked at the target rev
3 retired, never returned
USAGE
python3 queue_append_audit.py --repo /root/troupe-standup
python3 queue_append_audit.py --repo . --from 1423c4d --to HEAD --json out.json
python3 queue_append_audit.py --repo . --guard # newest queue-touching commit
python3 queue_append_audit.py --selftest # 25 cells, a synthetic history
"""
from __future__ import annotations
import argparse
import io
import json
import os
import re
import subprocess
import sys
import tempfile
QUEUE_DEFAULT = "WORK-QUEUE.md"
INDEX_DEFAULT = "docs/queue-index.md"
GATE_TOOL = "tools/queue_index.py"
HERE = os.path.dirname(os.path.abspath(__file__))
FATAL = ("STALE", "ERROR")
# ---------------------------------------------------------------- git plumbing
def git(repo, *args, timeout=120):
"""(rc, stdout, stderr) -- never a traceback for a git-level refusal."""
try:
p = subprocess.run(["git", "-C", repo] + list(args),
capture_output=True, timeout=timeout)
except FileNotFoundError:
return 127, b"", b"git not found on PATH"
except subprocess.TimeoutExpired:
return 124, b"", ("git %s timed out after %ss" % (args[0], timeout)).encode()
return p.returncode, p.stdout, p.stderr
def out_text(rc, out, err):
s = (out or b"").decode("utf-8", "replace")
e = (err or b"").decode("utf-8", "replace")
return s, e
def is_repo(repo):
rc, _, _ = git(repo, "rev-parse", "--git-dir")
return rc == 0
def resolve(repo, rev):
rc, out, _ = git(repo, "rev-parse", "--verify", "--quiet", rev + "^{commit}")
s, _ = out_text(rc, out, b"")
return s.strip() if rc == 0 else None
def exists(repo, rev, path):
rc, _, _ = git(repo, "cat-file", "-e", "%s:%s" % (rev, path))
return rc == 0
def commit_meta(repo, rev):
rc, out, _ = git(repo, "show", "-s", "--format=%h%x00%ad%x00%s",
"--date=iso", rev)
s, _ = out_text(rc, out, b"")
parts = s.rstrip("\n").split("\x00") if s else []
while len(parts) < 3:
parts.append("")
return {"short": parts[0][:7] or rev[:7], "date": parts[1], "subject": parts[2]}
def numstat(repo, rev, queue, index):
"""(queue_add, queue_del, index_changed) for one commit."""
rc, out, _ = git(repo, "show", "--numstat", "--format=", rev, "--", queue, index)
s, _ = out_text(rc, out, b"")
add = delete = None
touched_index = False
for line in s.splitlines():
parts = line.split("\t")
if len(parts) != 3:
continue
a, d, path = parts
if path == queue:
add = int(a) if a.isdigit() else a
delete = int(d) if d.isdigit() else d
elif path == index:
touched_index = True
return add, delete, touched_index
def materialise(repo, rev, paths, dest):
"""Extract exactly `paths` from `rev` into `dest`. Returns (ok, error)."""
rc, out, err = git(repo, "archive", "--format=tar", rev, "--", *paths)
if rc != 0:
s, e = out_text(rc, out, err)
return False, (e or s or "git archive rc %d" % rc).strip().splitlines()[0]
try:
p = subprocess.run(["tar", "-xf", "-", "-C", dest], input=out,
capture_output=True, timeout=120)
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
return False, "could not unpack: %s" % exc
if p.returncode != 0:
return False, "tar rc %d: %s" % (p.returncode,
p.stderr.decode("utf-8", "replace").strip())
return True, ""
# ------------------------------------------------------------------- one commit
def audit_commit(repo, rev, queue, index, timeout):
"""Grade one commit IN ITS OWN TREE. Never raises."""
row = {"rev": rev}
row.update(commit_meta(repo, rev))
add, delete, touched_index = numstat(repo, rev, queue, index)
row.update({"queue_add": add, "queue_del": delete,
"index_changed": touched_index, "rc": None, "detail": ""})
if not exists(repo, rev, queue):
row.update(verdict="UNGRADABLE",
detail="%s does not exist at this rev" % queue)
return row
# The gate as it existed AT THAT REV, not as it exists today: a revision
# where queue_index.py had not been written yet has no gate, and no claim
# about its index can be true or false.
if not exists(repo, rev, GATE_TOOL):
row.update(verdict="UNGRADABLE",
detail="no %s at this rev (the gate did not exist yet)" % GATE_TOOL)
return row
if not exists(repo, rev, index):
row.update(verdict="UNGRADABLE",
detail="no %s at this rev (nothing to compare against)" % index)
return row
td = tempfile.mkdtemp(prefix="qaa-")
try:
ok, why = materialise(repo, rev, [queue, index, GATE_TOOL], td)
if not ok:
row.update(verdict="ERROR", detail="could not materialise: %s" % why)
return row
tool = os.path.join(td, GATE_TOOL)
cmd = [sys.executable, tool,
"--repo", td,
"--queue", os.path.join(td, queue),
"--check", os.path.join(td, index)]
try:
p = subprocess.run(cmd, cwd=td, capture_output=True, timeout=timeout)
except subprocess.TimeoutExpired:
row.update(verdict="ERROR", detail="--check timed out after %ss" % timeout)
return row
s = p.stdout.decode("utf-8", "replace")
e = p.stderr.decode("utf-8", "replace")
first = (s.strip() or e.strip() or "").splitlines()
row["detail"] = first[0][:180] if first else ""
row["rc"] = p.returncode
if p.returncode == 0:
row["verdict"] = "PASS"
elif p.returncode == 1 and "STALE:" in s:
row["verdict"] = "STALE"
elif p.returncode == 2:
row["verdict"] = "UNGRADABLE"
else:
# rc 1 with no STALE line is a crash wearing the verdict code; rc 3
# is retired. Neither is a graded fault, and neither is a pass.
row["verdict"] = "ERROR"
return row
finally:
for root, dirs, files in os.walk(td, topdown=False):
for f in files:
try:
os.unlink(os.path.join(root, f))
except OSError:
pass
for d in dirs:
try:
os.rmdir(os.path.join(root, d))
except OSError:
pass
try:
os.rmdir(td)
except OSError:
pass
# ------------------------------------------------------------------ the range
def queue_revs(repo, queue, rev_from, rev_to):
args = ["rev-list", "--reverse", rev_to]
if rev_from:
args = ["rev-list", "--reverse", "%s..%s" % (rev_from, rev_to)]
rc, out, err = git(repo, *(args + ["--", queue]))
s, e = out_text(rc, out, err)
return rc, [h for h in s.split() if h], e
def audit(repo, queue, index, rev_from, rev_to, guard=False, timeout=30,
all_history=False):
"""(rc, payload). The only place exit codes are decided.
Range rules, in order:
--from REV grade exactly REV..TO (REV excluded), graded or not.
--all grade every commit that ever touched the queue, including the
hundreds that predate the index. Each of those is
UNGRADABLE, which is the honest answer and a useless table.
(default) begin at the commit that INTRODUCED the index: no commit
before it can be graded, and listing 328 rows that all say so
buries the 9 that can. The count left out is printed, never
silently dropped.
"""
payload = {"repo": repo, "queue": queue, "index": index,
"from": None, "to": None, "guard": bool(guard),
"auto_from": False, "pre_gate": 0,
"rows": [], "counts": {}, "graded": 0, "rc": None}
if not os.path.isdir(repo):
payload["error"] = "not a directory: %s" % repo
payload["rc"] = 2
return 2, payload
if not is_repo(repo):
payload["error"] = "not a git repository: %s" % repo
payload["rc"] = 2
return 2, payload
to_sha = resolve(repo, rev_to)
if to_sha is None:
payload["error"] = "unknown revision: %s" % rev_to
payload["rc"] = 2
return 2, payload
payload["to"] = to_sha
from_sha = None
if rev_from:
from_sha = resolve(repo, rev_from)
if from_sha is None:
payload["error"] = "unknown revision: %s" % rev_from
payload["rc"] = 2
return 2, payload
elif not all_history:
rc, out, _ = git(repo, "log", "--diff-filter=A", "--format=%H", "-1",
to_sha, "--", index)
intro = out_text(rc, out, b"")[0].strip()
if intro:
# Include the introducing commit itself: it may also carry a queue
# append, and a commit that adds both is exactly the shape to grade.
rc2, out2, _ = git(repo, "rev-parse", intro + "^")
from_sha = out2.decode("utf-8", "replace").strip() if rc2 == 0 else intro
payload["auto_from"] = True
payload["from"] = from_sha
rc, revs, err = queue_revs(repo, queue, from_sha, to_sha)
if rc != 0:
payload["error"] = "git rev-list failed: %s" % ((err.strip().splitlines() or [""])[0])
payload["rc"] = 2
return 2, payload
if guard:
revs = revs[-1:]
if not revs:
where = ("%s..%s" % (rev_from, rev_to) if rev_from else rev_to)
payload["error"] = ("no commit in %s touches %s, so there is no index "
"currency to grade" % (where, queue))
payload["rc"] = 2
return 2, payload
if payload["auto_from"] and not guard:
rc_n, out_n, _ = git(repo, "rev-list", "--count", to_sha, "--", queue)
try:
payload["pre_gate"] = max(0, int(out_n.decode().strip()) - len(revs))
except (ValueError, AttributeError):
payload["pre_gate"] = 0
rows = [audit_commit(repo, r, queue, index, timeout) for r in revs]
counts = {}
for r in rows:
counts[r["verdict"]] = counts.get(r["verdict"], 0) + 1
payload["rows"] = rows
payload["counts"] = counts
payload["graded"] = counts.get("PASS", 0) + counts.get("STALE", 0) + counts.get("ERROR", 0)
payload["rc"] = 1 if any(r["verdict"] in FATAL for r in rows) else 0
return payload["rc"], payload
# --------------------------------------------------------------------- report
def report(payload, stream=sys.stdout):
w = stream.write
if payload.get("error"):
w("queue_append_audit: %s\n" % payload["error"])
return
frm, to = payload.get("from"), payload.get("to") or payload["to"]
if payload["guard"]:
where = "newest queue-touching commit at or below %s" % (to or "")[:7]
elif payload["auto_from"]:
where = "%s..%s (auto: from the commit that introduced %s)" % (
(frm or "")[:7], (to or "")[:7], payload["index"])
elif frm:
where = "%s..%s" % (frm[:7], (to or "")[:7])
else:
where = "%s (all history)" % (to or "")[:7]
w("queue_append_audit -- queue: %s index: %s range: %s%s\n"
% (payload["queue"], payload["index"], where,
" [guard]" if payload["guard"] else ""))
if payload.get("pre_gate"):
w(" %d earlier commit(s) touch the queue before the gate existed; they "
"cannot be graded and are not listed (--all lists them)\n"
% payload["pre_gate"])
w(" %d commit(s) touch the queue\n\n" % len(payload["rows"]))
for r in payload["rows"]:
add, dele = r["queue_add"], r["queue_del"]
q = "q %s/%s" % (add if add is not None else "-", dele if dele is not None else "-")
w(" %-10s %-8s %-25s %-9s index:%-3s %s\n"
% (r["verdict"], r["short"], r["date"][:25], q,
"yes" if r["index_changed"] else "no", r["detail"]))
c = payload["counts"]
w("\nsummary: %d commit(s) -- %s\n"
% (len(payload["rows"]),
", ".join("%d %s" % (c[k], k) for k in ("PASS", "STALE", "UNGRADABLE", "ERROR") if k in c)
or "nothing graded"))
ungradable = c.get("UNGRADABLE", 0)
w("graded: %d of %d%s\n"
% (payload["graded"], len(payload["rows"]),
(" (%d could not be graded -- reported, NOT counted as stale)" % ungradable)
if ungradable else ""))
stale = [r["short"] for r in payload["rows"] if r["verdict"] == "STALE"]
if stale:
w("stale: %s\n" % " ".join(stale))
bad = [r["short"] for r in payload["rows"] if r["verdict"] == "ERROR"]
if bad:
w("error: %s\n" % " ".join(bad))
if payload["rc"] == 0:
w("verdict: every graded commit left the index current in its own tree\n")
else:
w("verdict: %d commit(s) did NOT leave the index current in their own tree -- "
"rc 1\n" % (len(stale) + len(bad)))
# ------------------------------------------------------------------- selftest
def _sh(cwd, *args):
return subprocess.run(args, cwd=cwd, capture_output=True, text=True)
def _fixture(base):
"""A five-commit history with one commit of each kind. Returns (repo, revs).
A: queue only, no gate tool -> UNGRADABLE
B: appends, and adds tools/queue_index.py + its render -> PASS
C: appends to the queue, no re-render -> STALE
D: appends AND re-renders -> PASS
E: touches an unrelated file -> not selected at all
"""
repo = os.path.join(base, "repo")
os.makedirs(repo)
_sh(base, "git", "init", "-q", repo)
_sh(repo, "git", "config", "user.name", "Selftest")
_sh(repo, "git", "config", "user.email", "selftest@invalid")
_sh(repo, "git", "config", "commit.gpgsign", "false")
queue = os.path.join(repo, "WORK-QUEUE.md")
with open(queue, "w", encoding="utf-8") as fh:
fh.write("## Q1 -- alpha\n\nStatus: open\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "A: queue only")
os.makedirs(os.path.join(repo, "tools"))
os.makedirs(os.path.join(repo, "docs"))
with open(os.path.join(repo, GATE_TOOL), "w", encoding="utf-8") as fh:
fh.write(open(os.path.join(HERE, "queue_index.py"), encoding="utf-8").read())
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- beta appended\n")
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "B: append + gate + index")
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- gamma appended\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "C: append, no re-render")
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- delta appended\n")
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "D: append + re-render")
with open(os.path.join(repo, "notes.txt"), "w", encoding="utf-8") as fh:
fh.write("unrelated\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "E: unrelated file")
revs = _sh(repo, "git", "rev-list", "--reverse", "HEAD").stdout.split()
return repo, revs
def selftest():
cells = []
def check(name, got, want):
cells.append((name, got == want, got, want))
base = tempfile.mkdtemp(prefix="qaa-selftest-")
try:
repo, revs = _fixture(base)
a, b, c, d, e = revs
rc, p = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD",
all_history=True)
rows = {r["rev"]: r for r in p["rows"]}
check("the audit selects the 4 queue-touching commits, not the 5th", len(p["rows"]), 4)
check("a commit with no gate tool is UNGRADABLE, not STALE", rows[a]["verdict"], "UNGRADABLE")
check("the commit that added the gate and its index is PASS", rows[b]["verdict"], "PASS")
check("an append with no re-render is STALE", rows[c]["verdict"], "STALE")
check("an append that re-renders in the same commit is PASS", rows[d]["verdict"], "PASS")
check("STALE row says the index was not touched", rows[c]["index_changed"], False)
check("PASS row says the index was touched", rows[d]["index_changed"], True)
check("the stale row's numstat is the append", (rows[c]["queue_add"], rows[c]["queue_del"]), (1, 0))
check("a range with a STALE row exits 1", rc, 1)
check("counts keep UNGRADABLE out of the STALE bucket", p["counts"].get("STALE"), 1)
check("graded counts PASS+STALE+ERROR only", p["graded"], 3)
check("the STALE detail carries the tool's own word", "STALE:" in rows[c]["detail"], True)
rc_g, p_g = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD", guard=True)
check("--guard on a clean head exits 0", rc_g, 0)
check("--guard reports exactly one commit", len(p_g["rows"]), 1)
rc_s, _ = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, c, guard=True)
check("--guard on the stale commit exits 1", rc_s, 1)
# Default range: begin where the gate does. B introduced the index, so
# the default table starts at B's parent and A is counted, not listed.
rc_a, p_a = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD")
check("the default range begins at the commit that introduced the index",
(p_a["auto_from"], len(p_a["rows"])), (True, 3))
check("pre-gate commits are counted, not silently dropped", p_a["pre_gate"], 1)
check("the default range still grades the stale commit", rc_a, 1)
rc_e, p_e = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, d, d)
check("an empty range is rc 2, not a clean 0", (rc_e, p_e.get("error") is not None), (2, True))
rc_r, _ = audit(os.path.join(base, "no-such-repo"), QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD")
check("a missing repo is rc 2", rc_r, 2)
rc_b, p_b = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "no-such-rev")
check("an unknown revision is rc 2", (rc_b, p_b.get("error") is not None), (2, True))
rc_q, p_q = audit(repo, "NOT-THE-QUEUE.md", INDEX_DEFAULT, None, "HEAD")
check("a queue no commit touches is rc 2", (rc_q, "touches" in (p_q.get("error") or "")), (2, True))
json_path = os.path.join(base, "out.json")
buf = io.StringIO()
rc_j, p_j = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD",
all_history=True)
with open(json_path, "w", encoding="utf-8") as fh:
json.dump(p_j, fh, indent=1)
report(p_j, stream=buf)
with open(json_path, encoding="utf-8") as fh:
loaded = json.load(fh)
check("--json round-trips the same 4 rows", len(loaded["rows"]), 4)
check("the report names the stale commit by short sha", c[:7] in buf.getvalue(), True)
check("a report with a fault never prints a clean verdict",
"every graded commit" in buf.getvalue(), False)
finally:
for root, dirs, files in os.walk(base, topdown=False):
for f in files:
try:
os.unlink(os.path.join(root, f))
except OSError:
pass
for dd in dirs:
try:
os.rmdir(os.path.join(root, dd))
except OSError:
pass
try:
os.rmdir(base)
except OSError:
pass
bad = [c for c in cells if not c[1]]
for name, ok, got, want in bad:
print("FAIL %s\n got: %r\n want: %r" % (name, got, want))
print("selftest: %d cells, %d passed, %d failed"
% (len(cells), len(cells) - len(bad), len(bad)))
return 0 if not bad else 1
# ----------------------------------------------------------------------- main
def main(argv=None):
ap = argparse.ArgumentParser(
description="Did every commit that touched the work queue leave its "
"index current IN ITS OWN TREE?")
ap.add_argument("--repo", default=".")
ap.add_argument("--queue", default=QUEUE_DEFAULT)
ap.add_argument("--index", default=INDEX_DEFAULT)
ap.add_argument("--from", dest="rev_from", default=None,
help="exclusive lower bound; default: the commit that "
"introduced the index document")
ap.add_argument("--to", dest="rev_to", default="HEAD")
ap.add_argument("--all", dest="all_history", action="store_true",
help="start at the root of history, listing the pre-gate "
"commits as UNGRADABLE rows too")
ap.add_argument("--guard", action="store_true",
help="grade only the newest queue-touching commit")
ap.add_argument("--timeout", type=int, default=30,
help="seconds per commit's --check (default 30)")
ap.add_argument("--json", metavar="PATH", help="write the rows here")
ap.add_argument("--selftest", action="store_true")
args = ap.parse_args(argv)
if args.selftest:
return selftest()
rc, payload = audit(args.repo, args.queue, args.index,
args.rev_from, args.rev_to, args.guard, args.timeout,
args.all_history)
report(payload)
if args.json:
try:
with open(args.json, "w", encoding="utf-8") as fh:
json.dump(payload, fh, indent=1)
except OSError as exc:
print("cannot write json: %s" % exc, file=sys.stderr)
return 2
return rc
if __name__ == "__main__":
sys.exit(main())