diffimpactscout 0.2.0__tar.gz → 0.3.0__tar.gz
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-0.2.0/src/diffimpactscout.egg-info → diffimpactscout-0.3.0}/PKG-INFO +1 -1
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/setup.py +1 -1
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/__init__.py +1 -1
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/base.py +43 -2
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/base.py +14 -10
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/eslint.py +2 -2
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/prettier.py +2 -2
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/repo_checks.py +36 -6
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/ruff.py +4 -4
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/syntax.py +10 -6
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/cli.py +24 -5
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/config.py +2 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/guard.py +85 -8
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/impact.py +7 -2
- diffimpactscout-0.3.0/src/diffimpactscout/impact/reporter.py +191 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0/src/diffimpactscout.egg-info}/PKG-INFO +1 -1
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/tests/test_fixture_smoke.py +2 -0
- diffimpactscout-0.2.0/src/diffimpactscout/impact/reporter.py +0 -107
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/LICENSE +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/README.md +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/pyproject.toml +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/setup.cfg +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/__main__.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/__init__.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/hygiene.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/detect.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/env.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/gitrun.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/__init__.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/cache.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/diff_parser.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/python_analyzer.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/route_linker.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/launcher.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/django.json +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/fastapi.json +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/frontend.json +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/generic.json +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/python.json +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/scope.py +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/SOURCES.txt +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/dependency_links.txt +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/entry_points.txt +0 -0
- {diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ with io.open("README.md", encoding="utf-8") as fh:
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="diffimpactscout",
|
|
10
|
-
version="0.
|
|
10
|
+
version="0.3.0",
|
|
11
11
|
description="Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.",
|
|
12
12
|
long_description=long_description,
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
@@ -2,13 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
Example: when the repo has an upstream/sprint/12 ref, diffs are measured
|
|
4
4
|
against it instead of origin/master.
|
|
5
|
+
|
|
6
|
+
================================================================================
|
|
7
|
+
ARCHITECTURAL OVERVIEW: 2-DOT TREE DIFF FOR INCREMENTAL CHECKS
|
|
8
|
+
================================================================================
|
|
9
|
+
|
|
10
|
+
Why a 2-dot tree diff (`git diff <anchor> HEAD`):
|
|
11
|
+
|
|
12
|
+
In standard Git workflows a 3-dot diff (`git diff <anchor>...HEAD`) computes
|
|
13
|
+
changes relative to the common merge-base ancestor of the two tips. Whenever a
|
|
14
|
+
developer syncs their branch with upstream (merge, rebase, or squash), the
|
|
15
|
+
merge-base moves and the 3-dot range silently grows to include every commit
|
|
16
|
+
introduced by upstream since the branch diverged. Every pre-existing upstream
|
|
17
|
+
violation (a stale trailing space, a long line, an old lint error) then shows up
|
|
18
|
+
as a "new" issue in the pre-push guard, producing false positives the developer
|
|
19
|
+
did not author and cannot reasonably fix in that push.
|
|
20
|
+
|
|
21
|
+
A 2-dot tree diff (`git diff <anchor> HEAD`, no `...`) compares the exact
|
|
22
|
+
filesystem snapshot ("tree") of the resolved upstream anchor directly against
|
|
23
|
+
the `HEAD` tree. Consequences:
|
|
24
|
+
|
|
25
|
+
1. Zero Upstream Blame: any file or line that exists byte-identically in the
|
|
26
|
+
anchor tree contributes no diff entries. Only files and lines actually
|
|
27
|
+
added or changed by the developer are passed to the checks.
|
|
28
|
+
2. Sync Agnostic: behaves identically whether the branch was synced with
|
|
29
|
+
`git merge`, `git rebase`, or `git merge --squash`, because it never walks
|
|
30
|
+
commit ancestry, only the two tip trees.
|
|
31
|
+
3. Shallow-Clone Safe: computing the name-only diff needs just the two tip
|
|
32
|
+
trees (plus any blobs referenced in the range), so it works on shallow
|
|
33
|
+
clones that lack full commit history.
|
|
34
|
+
|
|
35
|
+
Sprint branch resolution:
|
|
36
|
+
|
|
37
|
+
In sprint-based development models (e.g. Contify) work is branched from active
|
|
38
|
+
sprint branches such as `refs/remotes/upstream/sprint/33.1` rather than from
|
|
39
|
+
`master`. The resolver inspects `refs/remotes/upstream/master` plus the top
|
|
40
|
+
`DEFAULT_MAX_SPRINT_CANDIDATES` (5) most recent sprint branches sorted by
|
|
41
|
+
committer date, and picks the candidate whose tree is closest to `HEAD` (the
|
|
42
|
+
fewest changed files on a `--diff-filter=ACMRT` name-only diff). The closest
|
|
43
|
+
tree means the smallest honest change-set, which is what incremental checks
|
|
44
|
+
want to target.
|
|
45
|
+
================================================================================
|
|
5
46
|
"""
|
|
6
47
|
|
|
7
48
|
import diffimpactscout.gitrun as gitrun
|
|
8
49
|
|
|
9
50
|
MASTER_REF = "refs/remotes/upstream/master"
|
|
10
51
|
SPRINT_PREFIX = "sprint/"
|
|
11
|
-
DEFAULT_MAX_SPRINT_CANDIDATES =
|
|
52
|
+
DEFAULT_MAX_SPRINT_CANDIDATES = 5
|
|
12
53
|
REMOTE_FALLBACKS = ("main", "master", "develop")
|
|
13
54
|
|
|
14
55
|
|
|
@@ -16,7 +57,7 @@ def resolve_change_base(root, limit=DEFAULT_MAX_SPRINT_CANDIDATES):
|
|
|
16
57
|
"""Return the remote ref closest to HEAD, or None.
|
|
17
58
|
|
|
18
59
|
`limit` caps how many `sprint/*` refs are considered: only the
|
|
19
|
-
`limit` (default
|
|
60
|
+
`limit` (default 5) most-recent ones by committer date, unlike the
|
|
20
61
|
bash original dev_base.sh which considered every sprint branch.
|
|
21
62
|
"""
|
|
22
63
|
for remote in _remote_order(root):
|
|
@@ -97,7 +97,9 @@ class ExternalCheck(Check):
|
|
|
97
97
|
output = (out or err).decode("utf-8", errors="replace").strip()
|
|
98
98
|
if not output:
|
|
99
99
|
output = "exit code %s" % proc.returncode
|
|
100
|
-
|
|
100
|
+
else:
|
|
101
|
+
output = "%s (exit code %s)" % (output, proc.returncode)
|
|
102
|
+
issues.append(CheckIssue(".", None, None, self.id, output))
|
|
101
103
|
return CheckResult(issues=issues, fixed=fixed, skipped=skipped, warned=warned)
|
|
102
104
|
for path in files or []:
|
|
103
105
|
argv = self._argv_for(path)
|
|
@@ -116,7 +118,9 @@ class ExternalCheck(Check):
|
|
|
116
118
|
output = (out or err).decode("utf-8", errors="replace").strip()
|
|
117
119
|
if not output:
|
|
118
120
|
output = "exit code %s" % proc.returncode
|
|
119
|
-
|
|
121
|
+
else:
|
|
122
|
+
output = "%s (exit code %s)" % (output, proc.returncode)
|
|
123
|
+
issues.append(CheckIssue(path, None, None, self.id, output))
|
|
120
124
|
return CheckResult(issues=issues, fixed=fixed, skipped=skipped, warned=warned)
|
|
121
125
|
|
|
122
126
|
|
|
@@ -151,7 +155,7 @@ class CheckContext(object):
|
|
|
151
155
|
|
|
152
156
|
|
|
153
157
|
class CheckIssue(object):
|
|
154
|
-
def __init__(self, path, line, column, code, message):
|
|
158
|
+
def __init__(self, path, line=None, column=None, code=None, message=None):
|
|
155
159
|
self.path = path
|
|
156
160
|
self.line = line
|
|
157
161
|
self.column = column
|
|
@@ -159,13 +163,13 @@ class CheckIssue(object):
|
|
|
159
163
|
self.message = message
|
|
160
164
|
|
|
161
165
|
def format(self):
|
|
162
|
-
|
|
163
|
-
self.path
|
|
164
|
-
|
|
165
|
-
self.
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
168
|
-
)
|
|
166
|
+
if self.line is None and self.column is None:
|
|
167
|
+
location = self.path
|
|
168
|
+
else:
|
|
169
|
+
line = "-" if self.line is None else str(self.line)
|
|
170
|
+
column = "-" if self.column is None else str(self.column)
|
|
171
|
+
location = "%s:%s:%s" % (self.path, line, column)
|
|
172
|
+
return "%s: %s %s" % (location, self.code, self.message)
|
|
169
173
|
|
|
170
174
|
|
|
171
175
|
class CheckResult(object):
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
"""Repo-wide checks for oversized files
|
|
1
|
+
"""Repo-wide checks for oversized files, leaked private keys, and case-colliding file paths.
|
|
2
2
|
|
|
3
|
-
Example: a 300 MB binary is flagged with a git-lfs hint,
|
|
4
|
-
containing a private key block always blocks the push
|
|
3
|
+
Example: a 300 MB binary is flagged with a git-lfs hint, any file
|
|
4
|
+
containing a private key block always blocks the push, and a pushed
|
|
5
|
+
`FILE.TXT` colliding with a tracked `file.txt` is flagged because the two
|
|
6
|
+
names clobber each other on case-insensitive filesystems (macOS, Windows).
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
import os
|
|
8
10
|
|
|
11
|
+
import diffimpactscout.gitrun as gitrun
|
|
9
12
|
from diffimpactscout.checks.base import (
|
|
10
13
|
Check,
|
|
11
14
|
CheckIssue,
|
|
@@ -23,6 +26,33 @@ PRIVATE_KEY_MARKERS = (
|
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
@register
|
|
30
|
+
class CaseConflictCheck(Check):
|
|
31
|
+
id = "repo/case-conflict"
|
|
32
|
+
scoped = "files"
|
|
33
|
+
|
|
34
|
+
def run(self, context, files):
|
|
35
|
+
tracked = gitrun.git_nul(["ls-files", "-z"], context.root)
|
|
36
|
+
by_lower = {}
|
|
37
|
+
for path in tracked:
|
|
38
|
+
by_lower.setdefault(path.lower(), []).append(path)
|
|
39
|
+
issues = []
|
|
40
|
+
for path in files or []:
|
|
41
|
+
for other in by_lower.get(path.lower(), ()):
|
|
42
|
+
if other != path:
|
|
43
|
+
issues.append(
|
|
44
|
+
CheckIssue(
|
|
45
|
+
path,
|
|
46
|
+
None,
|
|
47
|
+
None,
|
|
48
|
+
self.id,
|
|
49
|
+
"case conflict detected with %s" % other,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
break
|
|
53
|
+
return CheckResult(issues=issues)
|
|
54
|
+
|
|
55
|
+
|
|
26
56
|
@register
|
|
27
57
|
class LargeFilesCheck(Check):
|
|
28
58
|
id = "repo/large-files"
|
|
@@ -61,8 +91,8 @@ class LargeFilesCheck(Check):
|
|
|
61
91
|
issues.append(
|
|
62
92
|
CheckIssue(
|
|
63
93
|
path,
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
None,
|
|
95
|
+
None,
|
|
66
96
|
self.id,
|
|
67
97
|
"file size %d kB exceeds limit of %d kB; consider git lfs"
|
|
68
98
|
% (size_kb, self.max_kb),
|
|
@@ -89,7 +119,7 @@ class PrivateKeyCheck(Check):
|
|
|
89
119
|
for marker in PRIVATE_KEY_MARKERS:
|
|
90
120
|
if marker in data:
|
|
91
121
|
issues.append(
|
|
92
|
-
CheckIssue(path,
|
|
122
|
+
CheckIssue(path, None, None, self.id, "private key detected")
|
|
93
123
|
)
|
|
94
124
|
break
|
|
95
125
|
return CheckResult(issues=issues)
|
|
@@ -157,8 +157,8 @@ class RuffCheck(Check):
|
|
|
157
157
|
issues.append(
|
|
158
158
|
CheckIssue(
|
|
159
159
|
path,
|
|
160
|
-
row
|
|
161
|
-
location.get("column")
|
|
160
|
+
row,
|
|
161
|
+
location.get("column"),
|
|
162
162
|
violation.get("code") or self.id,
|
|
163
163
|
violation.get("message") or "",
|
|
164
164
|
)
|
|
@@ -215,8 +215,8 @@ class RuffFormatCheck(Check):
|
|
|
215
215
|
issues.append(
|
|
216
216
|
CheckIssue(
|
|
217
217
|
path,
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
None,
|
|
219
|
+
None,
|
|
220
220
|
self.id,
|
|
221
221
|
"formatting issue in changed lines:\n%s\nFix with: ruff format %s"
|
|
222
222
|
% (_truncate(kept), path),
|
|
@@ -38,8 +38,8 @@ class JsonSyntaxCheck(Check):
|
|
|
38
38
|
issues.append(
|
|
39
39
|
CheckIssue(
|
|
40
40
|
path,
|
|
41
|
-
getattr(exc, "lineno",
|
|
42
|
-
getattr(exc, "colno",
|
|
41
|
+
getattr(exc, "lineno", None),
|
|
42
|
+
getattr(exc, "colno", None),
|
|
43
43
|
self.id,
|
|
44
44
|
"invalid JSON: %s" % exc,
|
|
45
45
|
)
|
|
@@ -69,8 +69,8 @@ class AstSyntaxCheck(Check):
|
|
|
69
69
|
issues.append(
|
|
70
70
|
CheckIssue(
|
|
71
71
|
path,
|
|
72
|
-
exc.lineno
|
|
73
|
-
exc.offset
|
|
72
|
+
exc.lineno,
|
|
73
|
+
exc.offset,
|
|
74
74
|
self.id,
|
|
75
75
|
"invalid python syntax: %s" % (exc.msg or "syntax error"),
|
|
76
76
|
)
|
|
@@ -78,7 +78,11 @@ class AstSyntaxCheck(Check):
|
|
|
78
78
|
except ValueError as exc:
|
|
79
79
|
issues.append(
|
|
80
80
|
CheckIssue(
|
|
81
|
-
path,
|
|
81
|
+
path,
|
|
82
|
+
None,
|
|
83
|
+
None,
|
|
84
|
+
self.id,
|
|
85
|
+
"invalid python source: %s" % exc,
|
|
82
86
|
)
|
|
83
87
|
)
|
|
84
88
|
return CheckResult(issues=issues)
|
|
@@ -111,7 +115,7 @@ class MergeConflictCheck(Check):
|
|
|
111
115
|
line = lines[lineno - 1]
|
|
112
116
|
if self._is_marker(line):
|
|
113
117
|
issues.append(
|
|
114
|
-
CheckIssue(path, lineno,
|
|
118
|
+
CheckIssue(path, lineno, None, self.id, "conflict marker found")
|
|
115
119
|
)
|
|
116
120
|
return CheckResult(issues=issues)
|
|
117
121
|
|
|
@@ -46,6 +46,11 @@ def _parser():
|
|
|
46
46
|
p_impact.add_argument("--staged", action="store_true")
|
|
47
47
|
p_impact.add_argument("--fast", action="store_true")
|
|
48
48
|
p_impact.add_argument("--json", action="store_true")
|
|
49
|
+
p_impact.add_argument(
|
|
50
|
+
"--markdown",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="emit a Markdown table instead of the plain ASCII table",
|
|
53
|
+
)
|
|
49
54
|
p_impact.set_defaults(func=_cmd_impact)
|
|
50
55
|
|
|
51
56
|
p_check = sub.add_parser(
|
|
@@ -148,6 +153,7 @@ def _cmd_impact(args):
|
|
|
148
153
|
staged=args.staged,
|
|
149
154
|
fast=args.fast,
|
|
150
155
|
json_out=args.json,
|
|
156
|
+
markdown=args.markdown,
|
|
151
157
|
)
|
|
152
158
|
|
|
153
159
|
|
|
@@ -243,6 +249,20 @@ def _prompt_choice(text, choices, default):
|
|
|
243
249
|
return default
|
|
244
250
|
|
|
245
251
|
|
|
252
|
+
def _prompt_blocking(default):
|
|
253
|
+
sys.stdout.write(
|
|
254
|
+
"Guard blocking mode: strict (block the push) or warn (report only)? "
|
|
255
|
+
"[strict/Warn] (default: %s): " % default
|
|
256
|
+
)
|
|
257
|
+
sys.stdout.flush()
|
|
258
|
+
answer = _read_answer()
|
|
259
|
+
if answer in ("strict", "s"):
|
|
260
|
+
return "strict"
|
|
261
|
+
if answer in ("warn", "w"):
|
|
262
|
+
return "warn"
|
|
263
|
+
return default
|
|
264
|
+
|
|
265
|
+
|
|
246
266
|
def _check_id(entry):
|
|
247
267
|
if isinstance(entry, dict):
|
|
248
268
|
return entry.get("id", "?")
|
|
@@ -334,10 +354,7 @@ def _ask_overrides(data, args):
|
|
|
334
354
|
data["guard"] = merged["guard"]
|
|
335
355
|
data["impact"] = merged["impact"]
|
|
336
356
|
if not args.blocking:
|
|
337
|
-
|
|
338
|
-
"Block the push when checks report issues? (strict mode) [y/N]", False
|
|
339
|
-
)
|
|
340
|
-
data["guard"]["blocking"] = "strict" if strict else "warn"
|
|
357
|
+
data["guard"]["blocking"] = _prompt_blocking("strict")
|
|
341
358
|
hint = _LINT_HINT.get(profile)
|
|
342
359
|
if hint:
|
|
343
360
|
keep = _prompt_yes_default(
|
|
@@ -385,11 +402,13 @@ def _cmd_install_hooks(args):
|
|
|
385
402
|
detected = detect.detect_stack(root)
|
|
386
403
|
profile = _resolve_profile(args, detected)
|
|
387
404
|
data = _build_config_data(profile, args)
|
|
405
|
+
interactive = not args.yes and _is_tty()
|
|
406
|
+
if interactive and not args.blocking:
|
|
407
|
+
data["guard"]["blocking"] = "strict"
|
|
388
408
|
sys.stdout.write(_render_preview(detected, data))
|
|
389
409
|
note = _other_stack_note(root, profile)
|
|
390
410
|
if note:
|
|
391
411
|
sys.stderr.write("diffimpactscout: %s\n" % note)
|
|
392
|
-
interactive = not args.yes and _is_tty()
|
|
393
412
|
if interactive:
|
|
394
413
|
proceed = _prompt_yes_default("Proceed? [Y/n]", True)
|
|
395
414
|
if not proceed:
|
|
@@ -21,6 +21,7 @@ DEFAULT_IGNORE_PATHS = [
|
|
|
21
21
|
"**/dist/**",
|
|
22
22
|
"**/build/**",
|
|
23
23
|
"**/.git/**",
|
|
24
|
+
"**/.impact_analysis_cache.json",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
DEFAULT_GUARD_CHECKS = [
|
|
@@ -32,6 +33,7 @@ DEFAULT_GUARD_CHECKS = [
|
|
|
32
33
|
{"id": "syntax/merge-conflict"},
|
|
33
34
|
{"id": "repo/large-files", "args": ["--maxkb=250000"]},
|
|
34
35
|
{"id": "repo/private-key"},
|
|
36
|
+
{"id": "repo/case-conflict"},
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
|
|
@@ -10,6 +10,7 @@ import diffimpactscout.env as env
|
|
|
10
10
|
import diffimpactscout.gitrun as gitrun
|
|
11
11
|
import diffimpactscout.scope as scope
|
|
12
12
|
from diffimpactscout.checks.base import CheckContext, make_check
|
|
13
|
+
from diffimpactscout.config import is_excluded
|
|
13
14
|
|
|
14
15
|
DIFF_FILTER = "ACMRT"
|
|
15
16
|
|
|
@@ -48,6 +49,77 @@ def _warn(text):
|
|
|
48
49
|
sys.stderr.write("diffimpactscout: warning: %s\n" % text)
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
_FIX_HINTS = {
|
|
53
|
+
"ruff": "ruff check --fix {path}",
|
|
54
|
+
"ruff-format": "ruff format {path}",
|
|
55
|
+
"eslint": "eslint --fix {path}",
|
|
56
|
+
"prettier": "prettier --write {path}",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fix_hint(check_id, path):
|
|
61
|
+
template = _FIX_HINTS.get(check_id)
|
|
62
|
+
if not template or not path:
|
|
63
|
+
return None
|
|
64
|
+
return template.format(path=path)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _print_header(anchor, num_files, mode):
|
|
68
|
+
print("=" * 70)
|
|
69
|
+
print(" DiffImpactScout Guard")
|
|
70
|
+
print(
|
|
71
|
+
" Diff Base: %s | %d changed file(s) | %s mode"
|
|
72
|
+
% (anchor or "none", num_files, mode)
|
|
73
|
+
)
|
|
74
|
+
print("=" * 70)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_check_result(check, result):
|
|
78
|
+
if result.has_issues():
|
|
79
|
+
print(" [FAIL] %s" % check.id)
|
|
80
|
+
for issue in result.issues:
|
|
81
|
+
print(" %s" % issue.format())
|
|
82
|
+
hint = _fix_hint(check.id, result.issues[0].path)
|
|
83
|
+
if hint:
|
|
84
|
+
print(" --> Fix with: %s" % hint)
|
|
85
|
+
elif result.fixed:
|
|
86
|
+
print(" [FIXED] %s" % check.id)
|
|
87
|
+
for path in result.fixed:
|
|
88
|
+
print(" [fixed] %s" % path)
|
|
89
|
+
elif result.warned:
|
|
90
|
+
print(" [SKIP] %s" % check.id)
|
|
91
|
+
else:
|
|
92
|
+
print(" [PASS] %s" % check.id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _print_summary(ran, total_issues, total_fixed):
|
|
96
|
+
print("-" * 70)
|
|
97
|
+
print(
|
|
98
|
+
" diffimpactscout: %d check(s), %d issue(s), %d fixed"
|
|
99
|
+
% (ran, total_issues, total_fixed)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _print_verdict(blocked, total_issues, mode, always_blocked):
|
|
104
|
+
if blocked:
|
|
105
|
+
if always_blocked:
|
|
106
|
+
verdict = (
|
|
107
|
+
"[BLOCKED] push rejected (%s mode; hard-block check: %d issue(s))"
|
|
108
|
+
% (mode, total_issues)
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
verdict = "[BLOCKED] push rejected (strict mode: %d issue(s))" % total_issues
|
|
112
|
+
elif total_issues:
|
|
113
|
+
verdict = "[ALLOWED] push allowed (%s mode: %d issue(s) reported)" % (
|
|
114
|
+
mode,
|
|
115
|
+
total_issues,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
verdict = "[ALLOWED] push allowed (%s mode: no issues)" % mode
|
|
119
|
+
print(" Verdict: %s" % verdict)
|
|
120
|
+
print("=" * 70)
|
|
121
|
+
|
|
122
|
+
|
|
51
123
|
def run_guard(root, cfg, staged=False, all_files=False, files=None):
|
|
52
124
|
if _skip_requested():
|
|
53
125
|
return 0
|
|
@@ -67,6 +139,8 @@ def run_guard(root, cfg, staged=False, all_files=False, files=None):
|
|
|
67
139
|
anchor = dev_scope.scope_base(from_ref, to_ref)
|
|
68
140
|
file_set = dev_scope.dev_files(anchor, from_ref, to_ref)
|
|
69
141
|
|
|
142
|
+
file_set = [p for p in file_set if not is_excluded(p, cfg, root)]
|
|
143
|
+
|
|
70
144
|
mode = _guard_mode(cfg)
|
|
71
145
|
ctx = CheckContext(
|
|
72
146
|
root=root,
|
|
@@ -78,9 +152,13 @@ def run_guard(root, cfg, staged=False, all_files=False, files=None):
|
|
|
78
152
|
echo=True,
|
|
79
153
|
)
|
|
80
154
|
|
|
155
|
+
_print_header(anchor, len(file_set), mode)
|
|
156
|
+
|
|
81
157
|
ran = 0
|
|
82
158
|
total_issues = 0
|
|
159
|
+
total_fixed = 0
|
|
83
160
|
blocked = False
|
|
161
|
+
always_blocked = False
|
|
84
162
|
|
|
85
163
|
for entry in cfg.get("guard", {}).get("checks") or []:
|
|
86
164
|
check = None
|
|
@@ -96,21 +174,20 @@ def run_guard(root, cfg, staged=False, all_files=False, files=None):
|
|
|
96
174
|
check_files = [] if check.scoped == "repo" else file_set
|
|
97
175
|
try:
|
|
98
176
|
result = check.run(ctx, check_files)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
total_issues += 1
|
|
102
|
-
for path in result.fixed:
|
|
103
|
-
print("[fixed] %s" % path)
|
|
177
|
+
total_issues += len(result.issues)
|
|
178
|
+
total_fixed += len(result.fixed)
|
|
104
179
|
for text in result.warned:
|
|
105
180
|
_warn(text)
|
|
106
181
|
should_block = check.always_block or (check.blocking and mode == "strict")
|
|
107
182
|
if should_block and result.has_issues():
|
|
108
183
|
blocked = True
|
|
184
|
+
if check.always_block:
|
|
185
|
+
always_blocked = True
|
|
186
|
+
_print_check_result(check, result)
|
|
109
187
|
except Exception as exc:
|
|
110
188
|
_warn("check %r failed: %s" % (check.id, exc))
|
|
111
189
|
continue
|
|
112
190
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
191
|
+
_print_summary(ran, total_issues, total_fixed)
|
|
192
|
+
_print_verdict(blocked, total_issues, mode, always_blocked)
|
|
116
193
|
return 1 if blocked else 0
|
|
@@ -30,7 +30,7 @@ _ENTITY_KINDS = {
|
|
|
30
30
|
_DEFAULT_KINDS = ("name",)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def run_impact(root, cfg, staged=False, fast=False, json_out=False):
|
|
33
|
+
def run_impact(root, cfg, staged=False, fast=False, json_out=False, markdown=False):
|
|
34
34
|
if _skip_requested():
|
|
35
35
|
return 0
|
|
36
36
|
impact_cfg = cfg.get("impact") or {}
|
|
@@ -82,7 +82,9 @@ def run_impact(root, cfg, staged=False, fast=False, json_out=False):
|
|
|
82
82
|
+ "\n"
|
|
83
83
|
)
|
|
84
84
|
else:
|
|
85
|
-
sys.stdout.write(
|
|
85
|
+
sys.stdout.write(
|
|
86
|
+
reporter.render_report(rows, unresolved, changed_count, markdown=markdown)
|
|
87
|
+
)
|
|
86
88
|
strict_env = os.environ.get("IMPACT_CHECK_STRICT")
|
|
87
89
|
tty = reporter.interactive_tty()
|
|
88
90
|
block, reason = reporter.should_block(strict_env, tty)
|
|
@@ -174,6 +176,9 @@ def _compose_rows(root, impact_cfg, profile, entities, analyses, changed_paths,
|
|
|
174
176
|
row["severity"] = reporter.classify_severity(
|
|
175
177
|
layers.get(name) or (), deleted, changed_paths, row.get("path")
|
|
176
178
|
)
|
|
179
|
+
row["reason"] = reporter.severity_reason(
|
|
180
|
+
layers.get(name) or (), deleted, changed_paths, row.get("path")
|
|
181
|
+
)
|
|
177
182
|
row["action"] = _action(row["severity"], deleted)
|
|
178
183
|
return rows, unresolved
|
|
179
184
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Render the impact report, classify severity, and decide whether a push should be blocked.
|
|
2
|
+
|
|
3
|
+
The terminal/stdout default is a plain aligned ASCII table that matches the
|
|
4
|
+
guard report's visual style; a Markdown table is available via an explicit
|
|
5
|
+
`--markdown` output mode for piping into PR comments or docs pages.
|
|
6
|
+
|
|
7
|
+
Example: a changed view referenced from a template and a frontend service
|
|
8
|
+
gets High severity, while a reference inside the changed file gets Low.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import diffimpactscout.env as env
|
|
14
|
+
|
|
15
|
+
_HEADER = "| # | Impacted File Path | Module / Subsystem | Category | Detected Reference / Usage | Severity | Action Required |"
|
|
16
|
+
_SEPARATOR = "| --- | --- | --- | --- | --- | --- | --- |"
|
|
17
|
+
_COLUMNS = ("#", "Impacted File Path", "Module / Subsystem", "Category", "Detected Reference / Usage", "Severity", "Action Required")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def classify_severity(layers, deleted_renamed, changed_paths, ref_path):
|
|
21
|
+
if deleted_renamed:
|
|
22
|
+
return "High"
|
|
23
|
+
if len(set(layers or ())) >= 2:
|
|
24
|
+
return "High"
|
|
25
|
+
if ref_path in (changed_paths or ()):
|
|
26
|
+
return "Low"
|
|
27
|
+
if not ref_path or not changed_paths:
|
|
28
|
+
return "Low"
|
|
29
|
+
return "Medium"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def severity_reason(layers, deleted_renamed, changed_paths, ref_path):
|
|
33
|
+
if deleted_renamed:
|
|
34
|
+
return "symbol was deleted or renamed in this change-set"
|
|
35
|
+
layers = set(layers or ())
|
|
36
|
+
if len(layers) >= 2:
|
|
37
|
+
return "referenced from multiple layers (%s)" % " + ".join(sorted(layers))
|
|
38
|
+
if ref_path in (changed_paths or ()):
|
|
39
|
+
return "reference is inside the changed file itself"
|
|
40
|
+
if not ref_path or not changed_paths:
|
|
41
|
+
return "no external reference beyond the change-set"
|
|
42
|
+
return "referenced from a file outside the change-set"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cell(row, key):
|
|
46
|
+
if isinstance(row, dict):
|
|
47
|
+
value = row.get(key)
|
|
48
|
+
else:
|
|
49
|
+
value = getattr(row, key, None)
|
|
50
|
+
if value is None:
|
|
51
|
+
return ""
|
|
52
|
+
return str(value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _row_values(row, index):
|
|
56
|
+
return [str(index)] + [_cell(row, k) for k in ("path", "module", "category", "ref", "severity", "action")]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _counts(rows):
|
|
60
|
+
counts = {"High": 0, "Medium": 0, "Low": 0}
|
|
61
|
+
for row in rows:
|
|
62
|
+
sev = _cell(row, "severity")
|
|
63
|
+
if sev in counts:
|
|
64
|
+
counts[sev] += 1
|
|
65
|
+
return counts
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _summary_line(rows, changed_count):
|
|
69
|
+
counts = _counts(rows or [])
|
|
70
|
+
return "Summary: High: %d, Medium: %d, Low: %d (%d changed file(s))" % (
|
|
71
|
+
counts["High"],
|
|
72
|
+
counts["Medium"],
|
|
73
|
+
counts["Low"],
|
|
74
|
+
changed_count,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _rationale_lines(rows):
|
|
79
|
+
lines = []
|
|
80
|
+
for i, row in enumerate(rows or [], start=1):
|
|
81
|
+
reason = _cell(row, "reason")
|
|
82
|
+
if not reason:
|
|
83
|
+
continue
|
|
84
|
+
label = "%s -> %s" % (_cell(row, "path") or "-", _cell(row, "ref") or "-")
|
|
85
|
+
lines.append(" %d. %s - %s (%s)" % (i, _cell(row, "severity") or "?", label, reason))
|
|
86
|
+
return lines
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ascii_table(rows):
|
|
90
|
+
values = [_row_values(row, i) for i, row in enumerate(rows or [], start=1)]
|
|
91
|
+
widths = []
|
|
92
|
+
for i in range(len(_COLUMNS)):
|
|
93
|
+
width = len(_COLUMNS[i])
|
|
94
|
+
for vals in values:
|
|
95
|
+
width = max(width, len(vals[i]))
|
|
96
|
+
widths.append(width)
|
|
97
|
+
header = " " + " ".join(c.ljust(widths[i]) for i, c in enumerate(_COLUMNS))
|
|
98
|
+
body = []
|
|
99
|
+
for vals in values:
|
|
100
|
+
body.append(" " + " ".join(v.ljust(widths[i]) for i, v in enumerate(vals)))
|
|
101
|
+
return header, body
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _render_ascii(rows, unresolved, changed_count):
|
|
105
|
+
lines = []
|
|
106
|
+
lines.append("=" * 70)
|
|
107
|
+
lines.append(" Impact Analysis Report")
|
|
108
|
+
lines.append(" Blast radius: %d changed file(s)" % changed_count)
|
|
109
|
+
lines.append("=" * 70)
|
|
110
|
+
lines.append("")
|
|
111
|
+
header, body = _ascii_table(rows)
|
|
112
|
+
lines.append(header)
|
|
113
|
+
for line in body:
|
|
114
|
+
lines.append(line)
|
|
115
|
+
if unresolved:
|
|
116
|
+
lines.append("")
|
|
117
|
+
lines.append(" Unresolved references (manual check required):")
|
|
118
|
+
for ref in unresolved:
|
|
119
|
+
lines.append(" - %s" % str(ref))
|
|
120
|
+
rationale = _rationale_lines(rows)
|
|
121
|
+
if rationale:
|
|
122
|
+
lines.append("")
|
|
123
|
+
lines.append(" Severity rationale:")
|
|
124
|
+
lines.extend(rationale)
|
|
125
|
+
lines.append("")
|
|
126
|
+
lines.append("-" * 70)
|
|
127
|
+
lines.append(" " + _summary_line(rows, changed_count))
|
|
128
|
+
lines.append("=" * 70)
|
|
129
|
+
return "\n".join(lines) + "\n"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _render_markdown(rows, unresolved, changed_count):
|
|
133
|
+
lines = []
|
|
134
|
+
lines.append("# Impact Analysis Report")
|
|
135
|
+
lines.append("")
|
|
136
|
+
lines.append(_HEADER)
|
|
137
|
+
lines.append(_SEPARATOR)
|
|
138
|
+
for i, row in enumerate(rows or [], start=1):
|
|
139
|
+
cells = [str(i)] + [_cell(row, k).replace("|", "\\|") for k in ("path", "module", "category", "ref", "severity", "action")]
|
|
140
|
+
lines.append("| " + " | ".join(cells) + " |")
|
|
141
|
+
if unresolved:
|
|
142
|
+
lines.append("")
|
|
143
|
+
lines.append("## Unresolved references (manual check required)")
|
|
144
|
+
for ref in unresolved:
|
|
145
|
+
lines.append("- %s" % str(ref))
|
|
146
|
+
rationale = _rationale_lines(rows)
|
|
147
|
+
if rationale:
|
|
148
|
+
lines.append("")
|
|
149
|
+
lines.append("## Severity rationale")
|
|
150
|
+
for line in rationale:
|
|
151
|
+
lines.append("- " + line.lstrip())
|
|
152
|
+
lines.append("")
|
|
153
|
+
lines.append(_summary_line(rows, changed_count))
|
|
154
|
+
return "\n".join(lines) + "\n"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def render_report(rows, unresolved, changed_count, markdown=False):
|
|
158
|
+
if markdown:
|
|
159
|
+
return _render_markdown(rows, unresolved, changed_count)
|
|
160
|
+
return _render_ascii(rows, unresolved, changed_count)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def interactive_tty():
|
|
164
|
+
return sys.stdin.isatty()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def confirm(prompt_text):
|
|
168
|
+
sys.stdout.write(prompt_text + " ")
|
|
169
|
+
sys.stdout.flush()
|
|
170
|
+
answer = (sys.stdin.readline() or "").strip().lower()
|
|
171
|
+
if answer in ("", "y", "yes"):
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_true(value):
|
|
177
|
+
return env.is_true(value)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _skip_requested():
|
|
181
|
+
return env.skip_requested()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def should_block(strict_env, tty):
|
|
185
|
+
if _skip_requested():
|
|
186
|
+
return (False, "skip env var set; impact check skipped")
|
|
187
|
+
if tty:
|
|
188
|
+
return (True, "interactive; prompt user to proceed")
|
|
189
|
+
if _is_true(strict_env):
|
|
190
|
+
return (True, "strict mode; blocking in non-interactive context")
|
|
191
|
+
return (False, "non-interactive; report shown as warning, commit not blocked")
|
|
@@ -89,11 +89,13 @@ CHECKERS = {
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def test_gitrepo_scenarios(gitrepo):
|
|
92
|
+
"""Verifies that the gitrepo fixture is valid and matches its expected scenario state."""
|
|
92
93
|
assert gitrepo.git_ok("rev-parse", "--verify", "HEAD")
|
|
93
94
|
CHECKERS[gitrepo.scenario](gitrepo)
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def test_repo_helpers_rm_mv_merge(tmp_path):
|
|
98
|
+
"""Verifies that the GitRepo helper methods for merge, rm, mv, and commit behave correctly."""
|
|
97
99
|
repo = GitRepo.init(str(tmp_path / "repo"))
|
|
98
100
|
repo.write("a.txt", "a\n")
|
|
99
101
|
repo.commit("A")
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""Render the impact report table, classify severity, and decide whether a push should be blocked.
|
|
2
|
-
|
|
3
|
-
Example: a changed view referenced from a template and a frontend service
|
|
4
|
-
gets High severity, while a reference inside the changed file gets Low.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
import diffimpactscout.env as env
|
|
10
|
-
|
|
11
|
-
_HEADER = "| # | Impacted File Path | Module / Subsystem | Category | Detected Reference / Usage | Severity | Action Required |"
|
|
12
|
-
_SEPARATOR = "| --- | --- | --- | --- | --- | --- | --- |"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def classify_severity(layers, deleted_renamed, changed_paths, ref_path):
|
|
16
|
-
if deleted_renamed:
|
|
17
|
-
return "High"
|
|
18
|
-
if len(set(layers or ())) >= 2:
|
|
19
|
-
return "High"
|
|
20
|
-
if ref_path in (changed_paths or ()):
|
|
21
|
-
return "Low"
|
|
22
|
-
if not ref_path or not changed_paths:
|
|
23
|
-
return "Low"
|
|
24
|
-
return "Medium"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _cell(row, key):
|
|
28
|
-
if isinstance(row, dict):
|
|
29
|
-
value = row.get(key)
|
|
30
|
-
else:
|
|
31
|
-
value = getattr(row, key, None)
|
|
32
|
-
if value is None:
|
|
33
|
-
return ""
|
|
34
|
-
return str(value).replace("|", "\\|")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _counts(rows):
|
|
38
|
-
counts = {"High": 0, "Medium": 0, "Low": 0}
|
|
39
|
-
for row in rows:
|
|
40
|
-
sev = _cell(row, "severity")
|
|
41
|
-
if sev in counts:
|
|
42
|
-
counts[sev] += 1
|
|
43
|
-
return counts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def render_report(rows, unresolved, changed_count):
|
|
47
|
-
lines = []
|
|
48
|
-
lines.append("# Impact Analysis Report")
|
|
49
|
-
lines.append("")
|
|
50
|
-
lines.append(_HEADER)
|
|
51
|
-
lines.append(_SEPARATOR)
|
|
52
|
-
for i, row in enumerate(rows or [], start=1):
|
|
53
|
-
lines.append(
|
|
54
|
-
"| %d | %s | %s | %s | %s | %s | %s |"
|
|
55
|
-
% (
|
|
56
|
-
i,
|
|
57
|
-
_cell(row, "path"),
|
|
58
|
-
_cell(row, "module"),
|
|
59
|
-
_cell(row, "category"),
|
|
60
|
-
_cell(row, "ref"),
|
|
61
|
-
_cell(row, "severity"),
|
|
62
|
-
_cell(row, "action"),
|
|
63
|
-
)
|
|
64
|
-
)
|
|
65
|
-
if unresolved:
|
|
66
|
-
lines.append("")
|
|
67
|
-
lines.append("## Unresolved references (manual check required)")
|
|
68
|
-
for ref in unresolved:
|
|
69
|
-
lines.append("- %s" % str(ref))
|
|
70
|
-
lines.append("")
|
|
71
|
-
counts = _counts(rows or [])
|
|
72
|
-
lines.append(
|
|
73
|
-
"Summary: %d changed file(s); High: %d, Medium: %d, Low: %d"
|
|
74
|
-
% (changed_count, counts["High"], counts["Medium"], counts["Low"])
|
|
75
|
-
)
|
|
76
|
-
return "\n".join(lines) + "\n"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def interactive_tty():
|
|
80
|
-
return sys.stdin.isatty()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def confirm(prompt_text):
|
|
84
|
-
sys.stdout.write(prompt_text + " ")
|
|
85
|
-
sys.stdout.flush()
|
|
86
|
-
answer = (sys.stdin.readline() or "").strip().lower()
|
|
87
|
-
if answer in ("", "y", "yes"):
|
|
88
|
-
return True
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _is_true(value):
|
|
93
|
-
return env.is_true(value)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _skip_requested():
|
|
97
|
-
return env.skip_requested()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def should_block(strict_env, tty):
|
|
101
|
-
if _skip_requested():
|
|
102
|
-
return (False, "skip env var set; impact check skipped")
|
|
103
|
-
if tty:
|
|
104
|
-
return (True, "interactive; prompt user to proceed")
|
|
105
|
-
if _is_true(strict_env):
|
|
106
|
-
return (True, "strict mode; blocking in non-interactive context")
|
|
107
|
-
return (False, "non-interactive; report shown as warning, commit not blocked")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/python_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{diffimpactscout-0.2.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|