My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add bespoke repo history dumper

+170
+170
tools/history_dumper.py
··· 1 + #!/usr/bin/env python3 2 + 3 + import subprocess 4 + import re 5 + import argparse 6 + import json 7 + from collections import defaultdict, Counter 8 + 9 + IMPACT_THRESHOLD = 800 10 + FILE_THRESHOLD = 15 11 + 12 + 13 + def run(cmd): 14 + result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace") 15 + if result.returncode != 0: 16 + raise RuntimeError(result.stderr.strip()) 17 + return result.stdout.strip() 18 + 19 + 20 + def get_tags(): 21 + tags = run(["git", "tag", "--sort=creatordate"]).splitlines() 22 + return [t for t in tags if t.strip()] 23 + 24 + 25 + def get_log(range_spec=None, path=None): 26 + cmd = [ 27 + "git", "log", 28 + "--reverse", 29 + "-p", 30 + "--pretty=format:<<<COMMIT>>>%n%H%n%an%n%aI%n%B%n<<<END>>>" 31 + ] 32 + 33 + if range_spec: 34 + cmd.insert(2, range_spec) 35 + 36 + if path: 37 + cmd.extend(["--", path]) 38 + 39 + return run(cmd) 40 + 41 + 42 + def classify(message): 43 + msg = message.lower() 44 + if "fix" in msg: 45 + return "fix" 46 + if "refactor" in msg: 47 + return "refactor" 48 + if "feat" in msg or "feature" in msg: 49 + return "feature" 50 + if "doc" in msg: 51 + return "docs" 52 + return "chore" 53 + 54 + 55 + def extract_stats(diff): 56 + added = len(re.findall(r'^\+(?!\+\+)', diff, re.MULTILINE)) 57 + removed = len(re.findall(r'^\-(?!\-\-)', diff, re.MULTILINE)) 58 + files = re.findall(r'^diff --git a/([^ ]+) b/([^ \n]+)', diff, re.MULTILINE) 59 + file_list = list(set(f[1] for f in files)) 60 + impact = added + removed 61 + return added, removed, file_list, impact 62 + 63 + 64 + def parse_commits(raw): 65 + commits = [] 66 + blocks = raw.split("<<<COMMIT>>>")[1:] 67 + 68 + for block in blocks: 69 + meta, diff = block.split("<<<END>>>", 1) 70 + lines = meta.strip().splitlines() 71 + 72 + if len(lines) < 3: 73 + continue 74 + 75 + commit_hash = lines[0] 76 + author = lines[1] 77 + date = lines[2] 78 + message = "\n".join(lines[3:]).strip() 79 + 80 + added, removed, files, impact = extract_stats(diff) 81 + ctype = classify(message) 82 + 83 + commits.append({ 84 + "hash": commit_hash, 85 + "author": author, 86 + "date": date, 87 + "message": message, 88 + "files_changed": files, 89 + "lines_added": added, 90 + "lines_removed": removed, 91 + "impact": impact, 92 + "type": ctype, 93 + "risk": impact > IMPACT_THRESHOLD or len(files) > FILE_THRESHOLD 94 + }) 95 + 96 + return commits 97 + 98 + 99 + def main(): 100 + parser = argparse.ArgumentParser() 101 + parser.add_argument("--path", help="Limit to a specific folder/file") 102 + args = parser.parse_args() 103 + 104 + tags = get_tags() 105 + releases = defaultdict(list) 106 + 107 + if tags: 108 + previous = None 109 + for tag in tags: 110 + range_spec = f"{previous}..{tag}" if previous else tag 111 + raw = get_log(range_spec=range_spec, path=args.path) 112 + releases[tag] = parse_commits(raw) 113 + previous = tag 114 + 115 + raw = get_log(range_spec=f"{previous}..HEAD", path=args.path) 116 + releases["unreleased"] = parse_commits(raw) 117 + 118 + else: 119 + raw = get_log(path=args.path) 120 + releases["full_history"] = parse_commits(raw) 121 + 122 + summary_output = [] 123 + summary_output.append("# Repository Internal Summary\n") 124 + 125 + all_commits_flat = [] 126 + 127 + for release, commits in releases.items(): 128 + if not commits: 129 + continue 130 + 131 + all_commits_flat.extend(commits) 132 + 133 + total_added = sum(c["lines_added"] for c in commits) 134 + total_removed = sum(c["lines_removed"] for c in commits) 135 + types = Counter(c["type"] for c in commits) 136 + top = sorted(commits, key=lambda x: x["impact"], reverse=True)[:5] 137 + 138 + summary_output.append(f"## {release}") 139 + summary_output.append(f"- Commits: {len(commits)}") 140 + summary_output.append(f"- Lines added: {total_added}") 141 + summary_output.append(f"- Lines removed: {total_removed}") 142 + summary_output.append("") 143 + 144 + summary_output.append("### Type Breakdown") 145 + for t, count in types.items(): 146 + summary_output.append(f"- {t}: {count}") 147 + summary_output.append("") 148 + 149 + summary_output.append("### Top Impact Commits") 150 + for c in top: 151 + summary_output.append( 152 + f"- {c['message'].splitlines()[0]} " 153 + f"(+{c['lines_added']}/-{c['lines_removed']}, " 154 + f"{len(c['files_changed'])} files)" 155 + ) 156 + summary_output.append("") 157 + 158 + with open("repo_internal_summary.md", "w", encoding="utf-8") as f: 159 + f.write("\n".join(summary_output)) 160 + 161 + with open("repo_internal_summary.json", "w", encoding="utf-8") as f: 162 + json.dump(releases, f, indent=2) 163 + 164 + print("Generated:") 165 + print(" - repo_internal_summary.md") 166 + print(" - repo_internal_summary.json") 167 + 168 + 169 + if __name__ == "__main__": 170 + main()