#!/usr/bin/env python3 """ issue_tree.py — draw the dependency graph of the local store. Offline. Edges come from the `depends:` metadata, which is the authoritative edge list; prose in the body is never walked. Because the graph is slugs all the way down, this works identically for issues that were never pushed anywhere. issue_tree.py every root (nothing depends on it) issue_tree.py wire-sqlc-appclick one subtree issue_tree.py --depth 2 --write Downwards is what this draws (what an issue depends on). The other direction is a grep, not a flag: grep -ln 'depends:.*migrate-schema' tmp/issues/*.md """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import issue # noqa: E402 def label(id, issues, seen, edges): iss = issues.get(id) if not iss: return "%s (not in the store)" % id tail = " (see above)" if id in seen and edges.get(id) else "" return "%s [%s] %s — %s %s.md%s" % ( id, iss.type or "-", iss.title, iss.state, id, tail) def render(roots, issues, edges, depth): lines, seen = [], set() def walk(id, prefix, is_last, is_root, level): connector = "" if is_root else ("└── " if is_last else "├── ") lines.append(prefix + connector + label(id, issues, seen, edges)) if id in seen or level >= depth: return seen.add(id) kids = edges.get(id) or [] child_prefix = prefix if is_root else prefix + (" " if is_last else "│ ") for i, k in enumerate(kids): walk(k, child_prefix, i == len(kids) - 1, False, level + 1) for r in roots: if r in seen: continue # already drawn as somebody's child — one tree, not two walk(r, "", True, True, 0) lines.append("") head = roots[0] if len(roots) == 1 else "%d root(s)" % len(roots) out = "# Dependency tree — %s\n\n```\n%s```\n" % (head, "\n".join(lines)) cycles = issue.find_cycles(edges) if cycles: out += "\n## Cycles\n\n" + "\n".join("- %s" % " -> ".join(c) for c in cycles) + "\n" return out def main(): ap = argparse.ArgumentParser(description="Draw the local dependency graph (offline)") ap.add_argument("ids", nargs="*", help="roots (default: issues nothing depends on)") ap.add_argument("--depth", type=int, default=6, help="max depth (default: 6)") ap.add_argument("--write", action="store_true", help="also write tmp/issues/tree-.md") ap.add_argument("--out", default=issue.ISSUE_ROOT, help="store root (default: tmp/issues)") args = ap.parse_args() issues = issue.load_all(args.out) if not issues: sys.exit("issue_tree.py: store %s is empty" % args.out) edges = issue.graph(issues) roots = args.ids for r in roots: if r not in issues: sys.exit("issue_tree.py: no issue %r in %s" % (r, args.out)) if not roots: depended_on = {d for deps in edges.values() for d in deps} roots = sorted(i for i in issues if i not in depended_on) or sorted(issues) text = render(roots, issues, edges, args.depth) sys.stdout.write(text) if args.write: slug = roots[0] if len(roots) == 1 else "all" path = os.path.join(args.out, "tree-%s.md" % slug) with open(path, "w") as f: f.write(text) print("written: %s" % path) if __name__ == "__main__": main()