diffimpactscout 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffimpactscout/__init__.py +3 -0
- diffimpactscout/__main__.py +8 -0
- diffimpactscout/base.py +95 -0
- diffimpactscout/checks/__init__.py +20 -0
- diffimpactscout/checks/base.py +206 -0
- diffimpactscout/checks/eslint.py +164 -0
- diffimpactscout/checks/hygiene.py +104 -0
- diffimpactscout/checks/prettier.py +103 -0
- diffimpactscout/checks/repo_checks.py +95 -0
- diffimpactscout/checks/ruff.py +210 -0
- diffimpactscout/checks/syntax.py +133 -0
- diffimpactscout/cli.py +220 -0
- diffimpactscout/config.py +205 -0
- diffimpactscout/env.py +29 -0
- diffimpactscout/gitrun.py +68 -0
- diffimpactscout/guard.py +116 -0
- diffimpactscout/impact/__init__.py +1 -0
- diffimpactscout/impact/cache.py +55 -0
- diffimpactscout/impact/diff_parser.py +181 -0
- diffimpactscout/impact/impact.py +345 -0
- diffimpactscout/impact/python_analyzer.py +284 -0
- diffimpactscout/impact/reporter.py +107 -0
- diffimpactscout/impact/route_linker.py +363 -0
- diffimpactscout/launcher.py +108 -0
- diffimpactscout/profiles/django.json +14 -0
- diffimpactscout/profiles/fastapi.json +14 -0
- diffimpactscout/profiles/plain.json +8 -0
- diffimpactscout/scope.py +210 -0
- diffimpactscout-0.1.0.dist-info/METADATA +314 -0
- diffimpactscout-0.1.0.dist-info/RECORD +34 -0
- diffimpactscout-0.1.0.dist-info/WHEEL +5 -0
- diffimpactscout-0.1.0.dist-info/entry_points.txt +2 -0
- diffimpactscout-0.1.0.dist-info/licenses/LICENSE +21 -0
- diffimpactscout-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Extract Django and FastAPI routes, then link template and frontend references to those routes.
|
|
2
|
+
|
|
3
|
+
Example: a template tag {% url order-list %} resolves to the changed view
|
|
4
|
+
handler that backs the order-list route.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from diffimpactscout.config import _matches_glob
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Route(object):
|
|
15
|
+
def __init__(self, name, path, handler, module):
|
|
16
|
+
self.name = name
|
|
17
|
+
self.path = path
|
|
18
|
+
self.handler = handler
|
|
19
|
+
self.module = module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_HTTP_METHODS = ("get", "post", "put", "delete", "patch", "options")
|
|
23
|
+
_HTTP_METHOD_RE_SRC = r"get|post|put|delete|patch"
|
|
24
|
+
|
|
25
|
+
HTTP_CALL_RE = re.compile(
|
|
26
|
+
r"(\$http|HttpClient|http)\s*\.\s*(" + _HTTP_METHOD_RE_SRC + r")\s*\("
|
|
27
|
+
)
|
|
28
|
+
QUOTED_LITERAL_RE = re.compile(r"(['\"])([^'\"\r\n]{1,500})\1")
|
|
29
|
+
URL_TAG_RE = re.compile(r"{%\s*url\s+['\"]([^'\"]+)['\"]\s*%}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _str_value(node):
|
|
33
|
+
if isinstance(node, ast.Constant):
|
|
34
|
+
value = node.value
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
return value
|
|
37
|
+
return None
|
|
38
|
+
Str = getattr(ast, "Str", None)
|
|
39
|
+
if Str is not None and isinstance(node, Str):
|
|
40
|
+
return node.s
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _iter_files(root, globs, exts):
|
|
45
|
+
if isinstance(globs, str):
|
|
46
|
+
globs = [globs]
|
|
47
|
+
globs = globs or []
|
|
48
|
+
exts = tuple(exts or ())
|
|
49
|
+
exts = tuple(e.lower() for e in exts)
|
|
50
|
+
if not root or not os.path.isdir(root):
|
|
51
|
+
return
|
|
52
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
53
|
+
dirnames[:] = [d for d in dirnames if d not in ("node_modules", ".git")]
|
|
54
|
+
for name in filenames:
|
|
55
|
+
if exts and os.path.splitext(name)[1].lower() not in exts:
|
|
56
|
+
continue
|
|
57
|
+
full = os.path.join(dirpath, name)
|
|
58
|
+
rel = os.path.relpath(full, root)
|
|
59
|
+
posix = rel.replace(os.sep, "/")
|
|
60
|
+
for pattern in globs:
|
|
61
|
+
if _matches_glob(posix, pattern):
|
|
62
|
+
yield posix
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_file(full):
|
|
67
|
+
text = _read_text(full)
|
|
68
|
+
if text is None:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
return ast.parse(text)
|
|
72
|
+
except (SyntaxError, ValueError, TypeError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_text(full):
|
|
77
|
+
try:
|
|
78
|
+
with open(full, "rb") as fh:
|
|
79
|
+
data = fh.read()
|
|
80
|
+
except (OSError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
return data.decode("utf-8", "replace")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _call_has_kwarg(call, name):
|
|
86
|
+
for kw in call.keywords:
|
|
87
|
+
if kw.arg == name:
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _call_kwarg_value(call, name):
|
|
93
|
+
for kw in call.keywords:
|
|
94
|
+
if kw.arg == name:
|
|
95
|
+
return kw.value
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _attribute_qname(node):
|
|
100
|
+
parts = []
|
|
101
|
+
cur = node
|
|
102
|
+
while isinstance(cur, ast.Attribute):
|
|
103
|
+
parts.append(cur.attr)
|
|
104
|
+
cur = cur.value
|
|
105
|
+
if isinstance(cur, ast.Name):
|
|
106
|
+
parts.append(cur.id)
|
|
107
|
+
parts.reverse()
|
|
108
|
+
if len(parts) >= 2:
|
|
109
|
+
parts = parts[1:]
|
|
110
|
+
return ".".join(parts)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve_handler(node):
|
|
114
|
+
if isinstance(node, ast.Call):
|
|
115
|
+
return _resolve_handler(node.func)
|
|
116
|
+
if isinstance(node, ast.Attribute):
|
|
117
|
+
return _attribute_qname(node)
|
|
118
|
+
if isinstance(node, ast.Name):
|
|
119
|
+
return node.id
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handler_name(call):
|
|
124
|
+
if len(call.args) < 2:
|
|
125
|
+
return None
|
|
126
|
+
return _resolve_handler(call.args[1])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def extract_django_routes(root, urls_globs):
|
|
130
|
+
routes = []
|
|
131
|
+
for posix in _iter_files(root, urls_globs, (".py",)):
|
|
132
|
+
full = os.path.join(root, posix) if root else posix
|
|
133
|
+
tree = _parse_file(full)
|
|
134
|
+
if tree is None:
|
|
135
|
+
continue
|
|
136
|
+
for node in ast.walk(tree):
|
|
137
|
+
if not isinstance(node, ast.Call):
|
|
138
|
+
continue
|
|
139
|
+
func = node.func
|
|
140
|
+
if not isinstance(func, ast.Name) or func.id not in ("path", "re_path", "url"):
|
|
141
|
+
continue
|
|
142
|
+
if not _call_has_kwarg(node, "name"):
|
|
143
|
+
continue
|
|
144
|
+
path_val = _str_value(node.args[0]) if node.args else None
|
|
145
|
+
if path_val is None:
|
|
146
|
+
continue
|
|
147
|
+
name_val = _str_value(_call_kwarg_value(node, "name"))
|
|
148
|
+
handler = _handler_name(node)
|
|
149
|
+
routes.append(Route(name_val, path_val, handler, posix))
|
|
150
|
+
return routes
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_ROUTE_BASES = ("router", "app", "api", "bp", "blueprint", "APIRouter", "FastAPI")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _decorator_route_path(deco):
|
|
157
|
+
if not isinstance(deco, ast.Call) or not deco.args:
|
|
158
|
+
return None
|
|
159
|
+
if not isinstance(deco.func, ast.Attribute):
|
|
160
|
+
return None
|
|
161
|
+
if deco.func.attr not in _HTTP_METHODS:
|
|
162
|
+
return None
|
|
163
|
+
if not isinstance(deco.func.value, ast.Name):
|
|
164
|
+
return None
|
|
165
|
+
if deco.func.value.id not in _ROUTE_BASES:
|
|
166
|
+
return None
|
|
167
|
+
return _str_value(deco.args[0])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def extract_fastapi_routes(root, py_globs):
|
|
171
|
+
routes = []
|
|
172
|
+
for posix in _iter_files(root, py_globs, (".py",)):
|
|
173
|
+
full = os.path.join(root, posix) if root else posix
|
|
174
|
+
tree = _parse_file(full)
|
|
175
|
+
if tree is None:
|
|
176
|
+
continue
|
|
177
|
+
for node in ast.walk(tree):
|
|
178
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
179
|
+
continue
|
|
180
|
+
for deco in node.decorator_list:
|
|
181
|
+
path = _decorator_route_path(deco)
|
|
182
|
+
if path is not None:
|
|
183
|
+
routes.append(Route(None, path, node.name, posix))
|
|
184
|
+
return routes
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def extract_template_refs(root, template_globs):
|
|
188
|
+
refs = []
|
|
189
|
+
for posix in _iter_files(root, template_globs, (".html",)):
|
|
190
|
+
full = os.path.join(root, posix) if root else posix
|
|
191
|
+
text = _read_text(full)
|
|
192
|
+
if text is None:
|
|
193
|
+
continue
|
|
194
|
+
for m in URL_TAG_RE.finditer(text):
|
|
195
|
+
refs.append((posix, m.group(1)))
|
|
196
|
+
return refs
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _block_repl(match):
|
|
200
|
+
return "\n" * match.group(0).count("\n")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _strip_comments(text):
|
|
204
|
+
text = re.sub(r"/\*.*?\*/", _block_repl, text, flags=re.S)
|
|
205
|
+
text = re.sub(r"(?m)(?<!:)//.*$", "", text)
|
|
206
|
+
return text
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def extract_frontend_refs(root, frontend_globs):
|
|
210
|
+
refs = []
|
|
211
|
+
for posix in _iter_files(root, frontend_globs, (".ts", ".js", ".tsx", ".jsx")):
|
|
212
|
+
full = os.path.join(root, posix) if root else posix
|
|
213
|
+
text = _read_text(full)
|
|
214
|
+
if text is None:
|
|
215
|
+
continue
|
|
216
|
+
text = _strip_comments(text)
|
|
217
|
+
for m in HTTP_CALL_RE.finditer(text):
|
|
218
|
+
line = text[:m.start()].count("\n") + 1
|
|
219
|
+
method = m.group(2)
|
|
220
|
+
window = text[m.end():m.end() + 300]
|
|
221
|
+
stripped = window.lstrip()
|
|
222
|
+
if stripped[:1] in ("'", '"'):
|
|
223
|
+
lit = QUOTED_LITERAL_RE.match(stripped)
|
|
224
|
+
if lit:
|
|
225
|
+
refs.append(
|
|
226
|
+
{
|
|
227
|
+
"file": posix,
|
|
228
|
+
"ref": lit.group(2),
|
|
229
|
+
"dynamic": False,
|
|
230
|
+
"line": line,
|
|
231
|
+
"method": method,
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
continue
|
|
235
|
+
litm = QUOTED_LITERAL_RE.search(window)
|
|
236
|
+
literal = litm.group(2) if litm else None
|
|
237
|
+
refs.append(
|
|
238
|
+
{
|
|
239
|
+
"file": posix,
|
|
240
|
+
"ref": literal,
|
|
241
|
+
"dynamic": True,
|
|
242
|
+
"line": line,
|
|
243
|
+
"method": method,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
return refs
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _route_regex(route_path):
|
|
250
|
+
parts = []
|
|
251
|
+
i = 0
|
|
252
|
+
n = len(route_path)
|
|
253
|
+
while i < n:
|
|
254
|
+
ch = route_path[i]
|
|
255
|
+
if ch == "<":
|
|
256
|
+
j = route_path.find(">", i)
|
|
257
|
+
if j == -1:
|
|
258
|
+
parts.append(re.escape(route_path[i:]))
|
|
259
|
+
break
|
|
260
|
+
parts.append(r"[^/]+")
|
|
261
|
+
i = j + 1
|
|
262
|
+
elif ch == "{":
|
|
263
|
+
j = route_path.find("}", i)
|
|
264
|
+
if j == -1:
|
|
265
|
+
parts.append(re.escape(route_path[i:]))
|
|
266
|
+
break
|
|
267
|
+
parts.append(r"[^/]+")
|
|
268
|
+
i = j + 1
|
|
269
|
+
else:
|
|
270
|
+
parts.append(re.escape(ch))
|
|
271
|
+
i += 1
|
|
272
|
+
return re.compile("^" + "".join(parts) + "$")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_path_prefix(ref, route_path):
|
|
276
|
+
if not route_path.startswith(ref):
|
|
277
|
+
return False
|
|
278
|
+
if len(ref) == len(route_path):
|
|
279
|
+
return False
|
|
280
|
+
nxt = route_path[len(ref)]
|
|
281
|
+
return ref.endswith("/") or nxt in ("/", "<", "{")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def match_endpoints(refs, routes):
|
|
285
|
+
matched = []
|
|
286
|
+
unresolved = []
|
|
287
|
+
for ref in refs:
|
|
288
|
+
file = ref.get("file")
|
|
289
|
+
path = ref.get("ref")
|
|
290
|
+
dynamic = ref.get("dynamic", False)
|
|
291
|
+
line = ref.get("line")
|
|
292
|
+
method = ref.get("method")
|
|
293
|
+
if dynamic or path is None:
|
|
294
|
+
exact = _exact_route(path, routes) if path is not None else None
|
|
295
|
+
if exact is not None:
|
|
296
|
+
matched.append(
|
|
297
|
+
{
|
|
298
|
+
"route": exact,
|
|
299
|
+
"file": file,
|
|
300
|
+
"ref": path,
|
|
301
|
+
"line": line,
|
|
302
|
+
"method": method,
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
unresolved.append({"file": file, "path": path, "dynamic": True})
|
|
307
|
+
continue
|
|
308
|
+
hit = _best_route_match(path, routes)
|
|
309
|
+
if hit is not None:
|
|
310
|
+
matched.append(
|
|
311
|
+
{
|
|
312
|
+
"route": hit,
|
|
313
|
+
"file": file,
|
|
314
|
+
"ref": path,
|
|
315
|
+
"line": line,
|
|
316
|
+
"method": method,
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
unresolved.append({"file": file, "path": path, "dynamic": False})
|
|
321
|
+
return matched, unresolved
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _norm_slash(path):
|
|
325
|
+
if path and path.startswith("/") and not path.startswith("//"):
|
|
326
|
+
return path[1:]
|
|
327
|
+
return path
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _exact_route(ref_path, routes):
|
|
331
|
+
norm = _norm_slash(ref_path)
|
|
332
|
+
for route in routes:
|
|
333
|
+
if _norm_slash(route.path) == norm:
|
|
334
|
+
return route
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _best_route_match(ref_path, routes):
|
|
339
|
+
ref_norm = _norm_slash(ref_path)
|
|
340
|
+
for route in routes:
|
|
341
|
+
if _norm_slash(route.path) == ref_norm:
|
|
342
|
+
return route
|
|
343
|
+
for route in routes:
|
|
344
|
+
if _route_regex(_norm_slash(route.path or "")).match(ref_norm):
|
|
345
|
+
return route
|
|
346
|
+
for route in routes:
|
|
347
|
+
route_norm = _norm_slash(route.path or "")
|
|
348
|
+
if ref_norm and route_norm and _is_path_prefix(ref_norm, route_norm):
|
|
349
|
+
return route
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def match_template_refs(refs, routes):
|
|
354
|
+
by_name = {r.name: r for r in routes if r.name is not None}
|
|
355
|
+
matched = []
|
|
356
|
+
unresolved = []
|
|
357
|
+
for file, name in refs:
|
|
358
|
+
route = by_name.get(name)
|
|
359
|
+
if route is not None:
|
|
360
|
+
matched.append({"route": route, "file": file, "ref": name})
|
|
361
|
+
else:
|
|
362
|
+
unresolved.append({"file": file, "ref": name})
|
|
363
|
+
return matched, unresolved
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Pre-push hook lifecycle.
|
|
2
|
+
|
|
3
|
+
Handles everything about the git pre-push hook: resolves where hook files
|
|
4
|
+
live (core.hooksPath, linked-worktree common dirs), renders the hook body
|
|
5
|
+
that forwards the pushed refs from stdin so the guard checks exactly the
|
|
6
|
+
range being pushed, falls back to PATH or the recorded interpreter, warns
|
|
7
|
+
without blocking when the tool is missing, and installs, inspects, and
|
|
8
|
+
removes the hook without overwriting a foreign one unless forced.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import diffimpactscout.gitrun as gitrun
|
|
15
|
+
|
|
16
|
+
MARKER = "# diffimpactscout pre-push hook"
|
|
17
|
+
HOOK_NAME = "pre-push"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _sh_single_quote(path):
|
|
21
|
+
return "'" + path.replace("'", "'\"'\"'") + "'"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _hooks_dir(root):
|
|
25
|
+
configured = gitrun.git_out(["config", "core.hooksPath"], root)
|
|
26
|
+
if configured:
|
|
27
|
+
if os.path.isabs(configured):
|
|
28
|
+
return configured
|
|
29
|
+
return gitrun.git_abs_dir(root, configured)
|
|
30
|
+
git_dir = gitrun.git_out(["rev-parse", "--git-common-dir"], root)
|
|
31
|
+
if git_dir:
|
|
32
|
+
if os.path.isabs(git_dir):
|
|
33
|
+
base = git_dir
|
|
34
|
+
else:
|
|
35
|
+
base = os.path.join(root, git_dir)
|
|
36
|
+
return os.path.normpath(os.path.join(base, "hooks"))
|
|
37
|
+
return os.path.join(root, ".git", "hooks")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _hook_path(root):
|
|
41
|
+
return os.path.join(_hooks_dir(root), HOOK_NAME)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_ZERO_OID = "0000000000000000000000000000000000000000"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hook_body(executable):
|
|
48
|
+
quoted = _sh_single_quote(executable)
|
|
49
|
+
return (
|
|
50
|
+
"#!/bin/sh\n"
|
|
51
|
+
+ MARKER
|
|
52
|
+
+ "\n"
|
|
53
|
+
+ "if IFS= read -r pre_push_line && [ -n \"$pre_push_line\" ]; then\n"
|
|
54
|
+
+ " set -- $pre_push_line\n"
|
|
55
|
+
+ " if [ $# -eq 4 ] && [ \"$2\" != \"%s\" ] && [ \"$4\" != \"%s\" ] && ! IFS= read -r pre_push_extra; then\n"
|
|
56
|
+
+ " PRE_COMMIT_FROM_REF=\"$4\"\n"
|
|
57
|
+
+ " PRE_COMMIT_TO_REF=\"$2\"\n"
|
|
58
|
+
+ " export PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF\n"
|
|
59
|
+
+ " fi\n"
|
|
60
|
+
+ "fi\n"
|
|
61
|
+
+ "if command -v diffimpactscout >/dev/null 2>&1 && diffimpactscout --version >/dev/null 2>&1; then\n"
|
|
62
|
+
+ " exec diffimpactscout guard\n"
|
|
63
|
+
+ "elif [ -x %s ]; then\n"
|
|
64
|
+
+ " exec %s -m diffimpactscout guard\n"
|
|
65
|
+
+ "else\n"
|
|
66
|
+
+ " echo \"diffimpactscout: pre-push guard skipped (tool not found; install via 'pip install diffimpactscout' or activate your venv).\" >&2\n"
|
|
67
|
+
+ " exit 0\n"
|
|
68
|
+
+ "fi\n"
|
|
69
|
+
) % (_ZERO_OID, _ZERO_OID, quoted, quoted)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hook_installed(root):
|
|
73
|
+
path = _hook_path(root)
|
|
74
|
+
if not os.path.isfile(path):
|
|
75
|
+
return False
|
|
76
|
+
with open(path) as fh:
|
|
77
|
+
return MARKER in fh.read()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def install_hook(root, force=False):
|
|
81
|
+
directory = _hooks_dir(root)
|
|
82
|
+
path = os.path.join(directory, HOOK_NAME)
|
|
83
|
+
if os.path.isfile(path):
|
|
84
|
+
with open(path) as fh:
|
|
85
|
+
content = fh.read()
|
|
86
|
+
if MARKER not in content and not force:
|
|
87
|
+
sys.stderr.write(
|
|
88
|
+
"diffimpactscout: pre-push hook exists (use --force to overwrite)\n"
|
|
89
|
+
)
|
|
90
|
+
return False
|
|
91
|
+
if not os.path.isdir(directory):
|
|
92
|
+
os.makedirs(directory)
|
|
93
|
+
with open(path, "w") as fh:
|
|
94
|
+
fh.write(hook_body(sys.executable))
|
|
95
|
+
os.chmod(path, 0o755)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def uninstall_hook(root):
|
|
100
|
+
path = _hook_path(root)
|
|
101
|
+
if not os.path.isfile(path):
|
|
102
|
+
return False
|
|
103
|
+
with open(path) as fh:
|
|
104
|
+
content = fh.read()
|
|
105
|
+
if MARKER not in content:
|
|
106
|
+
return False
|
|
107
|
+
os.remove(path)
|
|
108
|
+
return True
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"impact": {
|
|
3
|
+
"profile": "django",
|
|
4
|
+
"urls_globs": ["**/urls.py"],
|
|
5
|
+
"template_globs": ["**/templates/**/*.html"],
|
|
6
|
+
"frontend_globs": ["**/src/**/*.ts", "**/app/**/*.js"]
|
|
7
|
+
},
|
|
8
|
+
"guard": {
|
|
9
|
+
"checks": [
|
|
10
|
+
{"id": "ruff"},
|
|
11
|
+
{"id": "ruff-format"}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|