main
1#!/usr/bin/env python3
2"""List git repositories with GitHub remotes under ~/src/ for raffi script filter.
3
4Outputs Alfred Script Filter JSON format.
5Results are cached for 2 hours.
6Usage: lazypr-raffi [query]
7"""
8
9import json
10import os
11import subprocess
12import sys
13import tempfile
14import time
15
16CACHE_MAX_AGE = 7200 # 2 hours
17SRC_DIR = os.path.expanduser("~/src")
18
19
20def cache_paths():
21 cache_home = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
22 cache_dir = os.path.join(cache_home, "lazypr-raffi")
23 os.makedirs(cache_dir, exist_ok=True)
24 return cache_dir, os.path.join(cache_dir, "repos.json")
25
26
27def needs_rebuild(cache_file):
28 try:
29 age = time.time() - os.path.getmtime(cache_file)
30 return age > CACHE_MAX_AGE
31 except FileNotFoundError:
32 return True
33
34
35def has_github_remote(repo_path):
36 """Check if a git repo has a GitHub remote."""
37 try:
38 result = subprocess.run(
39 ["git", "-C", repo_path, "remote", "-v"],
40 capture_output=True, text=True, timeout=5,
41 )
42 return "github.com" in result.stdout
43 except (subprocess.TimeoutExpired, OSError):
44 return False
45
46
47def find_repos():
48 repos = []
49 for root, dirs, _files in os.walk(SRC_DIR):
50 # Skip worktree internal directories
51 if "worktrees" in root.split(os.sep):
52 dirs.clear()
53 continue
54 if ".git" in dirs or ".git" in _files:
55 if has_github_remote(root):
56 repos.append(root)
57 dirs.clear() # Don't descend into nested repos
58 repos.sort()
59 return [
60 {
61 "title": os.path.relpath(r, SRC_DIR),
62 "subtitle": r,
63 "arg": r,
64 }
65 for r in repos
66 ]
67
68
69def rebuild_cache(cache_dir, cache_file):
70 repos = find_repos()
71 fd, tmpfile = tempfile.mkstemp(suffix=".json", dir=cache_dir)
72 try:
73 with os.fdopen(fd, "w") as f:
74 json.dump(repos, f)
75 os.replace(tmpfile, cache_file)
76 except BaseException:
77 os.unlink(tmpfile)
78 raise
79
80
81def load_cache(cache_file):
82 try:
83 with open(cache_file) as f:
84 data = f.read()
85 if not data:
86 return []
87 return json.loads(data)
88 except (FileNotFoundError, json.JSONDecodeError):
89 return []
90
91
92def main():
93 cache_dir, cache_file = cache_paths()
94
95 if needs_rebuild(cache_file):
96 rebuild_cache(cache_dir, cache_file)
97
98 items = load_cache(cache_file)
99
100 query = " ".join(sys.argv[1:]).strip().lower()
101 if query:
102 items = [i for i in items if query in i["title"].lower()]
103
104 json.dump({"items": items}, sys.stdout)
105
106
107if __name__ == "__main__":
108 main()