spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / security-guidance / hooks / reporesolve.py
306 lines · 9.5 KBRaw
1import os
2import re
3import shlex
4import subprocess
5import time
6
7from gitutil import GIT_CMD, _git_toplevel
8from session_state import with_locked_state
9
10
11RES_NONE = 0
12RES_CWD = 1
13RES_COMMAND = 2
14RES_SHA_SCAN = 3
15RES_TOUCHED_PATHS = 4
16RES_HINT = 5
17
18COMMIT_SUBCOMMANDS = {("git", "commit"), ("gt", "create"), ("gt", "modify")}
19PUSH_SUBCOMMANDS = {("git", "push"), ("gt", "submit")}
20
21SCAN_SKIP_DIRS = {
22 "node_modules", ".venv", "venv", "__pycache__", ".tox", "dist",
23 "build", "target", ".cache", ".git", "vendor", "site-packages",
24}
25SCAN_MAX_DEPTH = int(os.environ.get("SG_REPO_SCAN_MAX_DEPTH", "3"))
26SCAN_MAX_REPOS = int(os.environ.get("SG_REPO_SCAN_MAX_REPOS", "64"))
27SCAN_BUDGET_S = float(os.environ.get("SG_REPO_SCAN_BUDGET_S", "4"))
28
29_SEPARATORS = frozenset(";&|()\n")
30_SHA_RE = re.compile(r"^[0-9a-f]{7,40}$")
31_ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
32_PREFIX_WORDS = frozenset(("env", "time", "exec", "command", "nohup"))
33
34
35def _abs(base, p):
36 try:
37 p = os.path.expanduser(p)
38 if not os.path.isabs(p):
39 p = os.path.join(base or os.getcwd(), p)
40 return os.path.normpath(p)
41 except (TypeError, ValueError, OSError):
42 return None
43
44
45def _is_sep(tok):
46 return bool(tok) and set(tok) <= _SEPARATORS
47
48
49def _tokenize(command):
50 if os.sep == "\\":
51 command = command.replace("\\", "\\\\")
52 try:
53 lex = shlex.shlex(command, posix=True, punctuation_chars=";&|()")
54 lex.whitespace_split = True
55 return list(lex)
56 except ValueError:
57 try:
58 return command.replace("&&", " && ").replace(";", " ; ").split()
59 except Exception:
60 return []
61
62
63def dirs_from_command(command, cwd, subcommands=None):
64 if not isinstance(command, str) or not command.strip():
65 return []
66 tokens = _tokenize(command)
67 out = []
68 cur = cwd or ""
69 i = 0
70 n = len(tokens)
71 at_start = True
72 while i < n:
73 t = tokens[i]
74 if _is_sep(t):
75 at_start = True
76 i += 1
77 continue
78 if at_start and (_ENV_ASSIGN_RE.match(t) or t in _PREFIX_WORDS):
79 i += 1
80 continue
81 if at_start and t in ("cd", "pushd"):
82 if i + 1 < n and not _is_sep(tokens[i + 1]) and tokens[i + 1] not in ("-",) \
83 and not tokens[i + 1].startswith("-"):
84 nxt = _abs(cur, tokens[i + 1])
85 if nxt:
86 cur = nxt
87 i += 2
88 else:
89 i += 1
90 at_start = False
91 continue
92 prog = os.path.basename(t) if t else t
93 if at_start and prog in ("git", "gt"):
94 j = i + 1
95 cdir = cur
96 gdir = None
97 wtree = None
98 if prog == "git":
99 while j < n and not _is_sep(tokens[j]):
100 a = tokens[j]
101 if a == "-C" and j + 1 < n:
102 cdir = _abs(cdir, tokens[j + 1]) or cdir
103 j += 2
104 continue
105 if a == "-c" and j + 1 < n:
106 j += 2
107 continue
108 if a.startswith("--git-dir="):
109 gdir = _abs(cdir, a.split("=", 1)[1])
110 j += 1
111 continue
112 if a == "--git-dir" and j + 1 < n:
113 gdir = _abs(cdir, tokens[j + 1])
114 j += 2
115 continue
116 if a.startswith("--work-tree="):
117 wtree = _abs(cdir, a.split("=", 1)[1])
118 j += 1
119 continue
120 if a == "--work-tree" and j + 1 < n:
121 wtree = _abs(cdir, tokens[j + 1])
122 j += 2
123 continue
124 if a.startswith("-"):
125 j += 1
126 continue
127 break
128 sub = tokens[j] if j < n and not _is_sep(tokens[j]) else None
129 if subcommands is None or (prog, sub) in subcommands:
130 cand = wtree
131 if not cand and gdir:
132 cand = os.path.dirname(gdir) if os.path.basename(gdir) == ".git" else gdir
133 if not cand:
134 cand = cdir
135 if cand:
136 out.append(cand)
137 i = j + 1 if j < n else j
138 at_start = False
139 continue
140 at_start = False
141 i += 1
142 return list(dict.fromkeys(d for d in out if d))
143
144
145def toplevel_from_command(command, cwd, subcommands=None, cwd_root=None):
146 cwd_abs = _abs(None, cwd) if cwd else None
147 for d in dirs_from_command(command, cwd, subcommands):
148 if cwd_root and d == cwd_abs:
149 return cwd_root
150 try:
151 if os.path.isdir(d):
152 top = _git_toplevel(d)
153 if top:
154 return top
155 except OSError:
156 continue
157 return None
158
159
160def scan_roots(cwd):
161 roots = []
162 for r in (cwd, os.environ.get("CLAUDE_PROJECT_DIR")):
163 if r and os.path.isdir(r):
164 a = os.path.abspath(r)
165 if a not in roots:
166 roots.append(a)
167 return roots
168
169
170def iter_git_repos(roots, max_depth=None, max_repos=None, deadline=None):
171 max_depth = SCAN_MAX_DEPTH if max_depth is None else max_depth
172 max_repos = SCAN_MAX_REPOS if max_repos is None else max_repos
173 seen = set()
174 for root in roots or []:
175 try:
176 if not root or not os.path.isdir(root):
177 continue
178 root = os.path.abspath(root)
179 except OSError:
180 continue
181 base_depth = root.rstrip(os.sep).count(os.sep)
182 for dirpath, dirnames, filenames in os.walk(root):
183 if deadline is not None and time.monotonic() > deadline:
184 return
185 if ".git" in dirnames or ".git" in filenames:
186 try:
187 key = os.path.realpath(dirpath)
188 except OSError:
189 key = dirpath
190 if key not in seen:
191 seen.add(key)
192 yield dirpath
193 if len(seen) >= max_repos:
194 return
195 depth = dirpath.rstrip(os.sep).count(os.sep) - base_depth
196 if depth >= max_depth:
197 dirnames[:] = []
198 else:
199 dirnames[:] = [
200 d for d in dirnames
201 if d not in SCAN_SKIP_DIRS and not d.startswith(".")
202 ]
203
204
205def repo_containing_commit(sha, roots, budget_s=None):
206 if not isinstance(sha, str) or not _SHA_RE.match(sha):
207 return None
208 budget_s = SCAN_BUDGET_S if budget_s is None else budget_s
209 deadline = time.monotonic() + budget_s
210 for repo in iter_git_repos(roots, deadline=deadline):
211 try:
212 r = subprocess.run(
213 [*GIT_CMD, "cat-file", "-e", f"{sha}^{{commit}}"],
214 cwd=repo, capture_output=True, timeout=3,
215 )
216 except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
217 continue
218 if r.returncode == 0:
219 return _git_toplevel(repo) or repo
220 return None
221
222
223def repos_from_paths(paths, cwd=None, limit=200):
224 counts = {}
225 order = []
226 cache = {}
227 for p in list(paths or [])[:limit]:
228 if not isinstance(p, str) or not p:
229 continue
230 ap = p if os.path.isabs(p) else _abs(cwd, p)
231 if not ap:
232 continue
233 d = os.path.dirname(ap)
234 while d and not os.path.isdir(d):
235 parent = os.path.dirname(d)
236 if parent == d:
237 break
238 d = parent
239 if not d:
240 continue
241 if d in cache:
242 top = cache[d]
243 else:
244 try:
245 top = _git_toplevel(d) if os.path.isdir(d) else None
246 except OSError:
247 top = None
248 cache[d] = top
249 if top:
250 if top not in counts:
251 order.append(top)
252 counts[top] = counts.get(top, 0) + 1
253 return sorted(order, key=lambda t: (-counts[t], order.index(t)))
254
255
256def save_repo_hint(session_id, repo_root):
257 if not session_id or not repo_root:
258 return
259
260 def _save(state):
261 state["repo_root_hint"] = repo_root
262 try:
263 with_locked_state(session_id, _save)
264 except Exception:
265 pass
266
267
268def load_repo_hint(session_id):
269 if not session_id:
270 return None
271 try:
272 hint = with_locked_state(session_id, lambda s: s.get("repo_root_hint"))
273 except Exception:
274 return None
275 if isinstance(hint, str) and hint and os.path.isdir(hint):
276 top = _git_toplevel(hint)
277 if top:
278 return top
279 return None
280
281
282_UNSET = object()
283
284
285def resolve_repo_root(cwd, command=None, subcommands=None, sha=None,
286 touched_paths=None, session_id=None, cwd_root=_UNSET):
287 if cwd_root is _UNSET:
288 cwd_root = _git_toplevel(cwd) if cwd else None
289 if command:
290 top = toplevel_from_command(command, cwd, subcommands, cwd_root)
291 if top and top != cwd_root:
292 return top, RES_COMMAND
293 if cwd_root:
294 return cwd_root, RES_CWD
295 if touched_paths:
296 tops = repos_from_paths(touched_paths, cwd)
297 if tops:
298 return tops[0], RES_TOUCHED_PATHS
299 if sha:
300 top = repo_containing_commit(sha, scan_roots(cwd))
301 if top:
302 return top, RES_SHA_SCAN
303 hint = load_repo_hint(session_id)
304 if hint:
305 return hint, RES_HINT
306 return None, RES_NONE