#!/usr/bin/env python3 """ issue_index.py — rebuild tmp/issues/INDEX.md from what is on disk. Offline. A map of the local store, nothing else. The `origin` column is the only place the index acknowledges that a tracker exists: `local` means the issue has never left this machine, `gitea` means the sync layer has pushed or pulled it. Both are ordinary issues here. The store is /tmp/issues unless --out says otherwise; an existing store with nothing in it gets an "_empty_" table, a store that is not there is an error rather than a directory to create. Usage: issue_index.py [--out DIR] """ import argparse import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import issue # noqa: E402 def cell(v): if isinstance(v, (list, tuple)): return ", ".join(str(x) for x in v) or "—" v = str(v or "").strip() return v.replace("|", "\\|") or "—" def progress(body): """`3/7` for a body with checkboxes, "" for one without. Counted from the body every time the index is built and stored nowhere — the boxes are the state, and a second copy of it in a metadata field would be wrong by the next edit.""" done, total = issue.checkbox_progress(body) return "%d/%d" % (done, total) if total else "" def build(root): # An index of a store that is not there is not an empty index, it is a bad # path. Raising beats writing INDEX.md into a directory nobody asked for. issue.require_store(root) issues = issue.load_all(root) rows = [] for i in sorted(issues): iss = issues[i] rest = [l for l in iss.labels if not l.startswith("type/")] rows.append({ "id": i, "state": cell(iss.state), "progress": progress(iss.body), "type": cell(iss.type), "labels": cell(rest), "title": cell(iss.title), "milestone": cell(iss.milestone), "depends": cell(iss.depends), "origin": cell(iss.origin), }) listing = os.listdir(root) if os.path.isdir(root) else [] trees = sorted(f for f in listing if re.match(r'^tree-.+\.md$', f)) out = ["# Issue store", "", "Every issue this project knows about. `origin: local` means it " "exists nowhere else — a complete state, not a pending one. Any " "other value names the tracker it also lives in; the handle is in " "the file. `progress` counts the body's checkboxes, ticked over " "total, and is blank for an issue that has none — read off the " "body at build time, stored nowhere. Rebuild with `issue_index.py`; " "tick a box with `issue_ac.py`.", ""] if rows: out += ["| id | state | progress | type | labels | title | milestone | depends | origin |", "|---|---|---|---|---|---|---|---|---|"] out += ["| [%s](%s.md) | %s | %s | %s | %s | %s | %s | %s | %s |" % ( r["id"], r["id"], r["state"], r["progress"], r["type"], r["labels"], r["title"], r["milestone"], r["depends"], r["origin"]) for r in rows] else: out.append("_empty_") if trees: out += ["", "## Dependency trees", ""] out += ["- [%s](%s)" % (t, t) for t in trees] cycles = issue.find_cycles(issue.graph(issues)) if cycles: out += ["", "## Dependency cycles", ""] out += ["- %s" % " -> ".join(c) for c in cycles] out.append("") path = os.path.join(root, "INDEX.md") with open(path, "w") as f: f.write("\n".join(out)) return path, len(rows) def main(): ap = argparse.ArgumentParser(description="Rebuild the local issue index (offline)") ap.add_argument("--out", default=issue.ISSUE_ROOT, help="store root (default: /tmp/issues)") args = ap.parse_args() # An existing store with nothing in it is a legitimate thing to index — it # gets an "_empty_" table. A store that is not there is not. try: path, n = build(args.out) except issue.StoreMissing as e: sys.exit("issue_index.py: %s — nothing was created; create an issue with " "issue_new.py, or pass --out" % e) print("%s — %d issue(s)" % (path, n)) if __name__ == "__main__": main()