diffimpactscout 0.3.0__tar.gz → 0.4.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.3.0/src/diffimpactscout.egg-info → diffimpactscout-0.4.0}/PKG-INFO +26 -9
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/README.md +25 -8
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/setup.py +1 -1
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/__init__.py +1 -1
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/base.py +2 -1
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/syntax.py +4 -1
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/cli.py +95 -3
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/config.py +3 -1
- diffimpactscout-0.4.0/src/diffimpactscout/dependency_check.py +108 -0
- diffimpactscout-0.4.0/src/diffimpactscout/impact/_parse.py +15 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/diff_parser.py +73 -4
- diffimpactscout-0.4.0/src/diffimpactscout/impact/impact.py +571 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/python_analyzer.py +159 -21
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/reporter.py +86 -14
- diffimpactscout-0.4.0/src/diffimpactscout/impact/route_linker.py +631 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/launcher.py +9 -7
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0/src/diffimpactscout.egg-info}/PKG-INFO +26 -9
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/SOURCES.txt +2 -0
- diffimpactscout-0.3.0/src/diffimpactscout/impact/impact.py +0 -350
- diffimpactscout-0.3.0/src/diffimpactscout/impact/route_linker.py +0 -363
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/LICENSE +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/pyproject.toml +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/setup.cfg +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/__main__.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/__init__.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/base.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/eslint.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/hygiene.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/prettier.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/repo_checks.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/ruff.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/detect.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/env.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/gitrun.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/guard.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/__init__.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/cache.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/django.json +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/fastapi.json +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/frontend.json +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/generic.json +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/python.json +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout/scope.py +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/dependency_links.txt +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/entry_points.txt +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/top_level.txt +0 -0
- {diffimpactscout-0.3.0 → diffimpactscout-0.4.0}/tests/test_fixture_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffimpactscout
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Incremental pre-push guard and AST blast-radius impact analyzer for git repositories.
|
|
5
5
|
Author: DiffImpactScout contributors
|
|
6
6
|
License: MIT
|
|
@@ -66,11 +66,11 @@ diffimpactscout guard
|
|
|
66
66
|
# 3. Analyze the blast radius of your change
|
|
67
67
|
diffimpactscout impact
|
|
68
68
|
|
|
69
|
-
# 4.
|
|
69
|
+
# 4. Install the pre-push hook (guard + impact report by default)
|
|
70
70
|
diffimpactscout install-hooks
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)).
|
|
73
|
+
`init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)) and, unless the config's `mode` says otherwise, the hook runs both the guard and the impact report on every push.
|
|
74
74
|
|
|
75
75
|
## Supported setups
|
|
76
76
|
|
|
@@ -120,15 +120,29 @@ Analyzes the blast radius of the change-set. In an interactive terminal it print
|
|
|
120
120
|
| `--fast` | Skip template and frontend scanning; limit Python scanning to the changed files plus import-linked files. |
|
|
121
121
|
| `--json` | Emit the report as JSON (`{changed_count, rows, unresolved}`) instead of a markdown table. |
|
|
122
122
|
|
|
123
|
+
### `diffimpactscout guard-and-impact-check`
|
|
124
|
+
|
|
125
|
+
Runs the `guard` checks first, then always falls through to the `impact` blast-radius report so the developer sees the guard verdict and the affected areas together. This is the default pre-push hook scope, so one push runs the whole workflow. The command exits non-zero when the guard blocks in strict mode (or when impact is set to block), so a failing guard still surfaces the impact report but the push is rejected.
|
|
126
|
+
|
|
127
|
+
| Flag | Description |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `--staged` | Restrict both the guard and the impact analysis to staged files. |
|
|
130
|
+
| `--fast` | Passes through to `impact` (skip template/frontend scanning). |
|
|
131
|
+
| `--json` | Passes through to `impact` (emit the report as JSON). |
|
|
132
|
+
|
|
133
|
+
### `diffimpactscout dependency-check`
|
|
134
|
+
|
|
135
|
+
Verifies that every external tool referenced by the configured guard checks is on `PATH` (`ruff`, `eslint`, `prettier`). Missing tools print an install hint and exit `1`; a clean run prints an OK banner and exits `0`. `install-hooks` also runs a non-blocking version of this check after installing a hook, warning about missing tools only when stdin is a terminal.
|
|
136
|
+
|
|
123
137
|
### `diffimpactscout check CHECK_ID [files...]`
|
|
124
138
|
|
|
125
139
|
Runs a single check by id (see [Checks reference](#checks-reference)). With no file arguments it runs against the change-set.
|
|
126
140
|
|
|
127
141
|
### `diffimpactscout install-hooks`
|
|
128
142
|
|
|
129
|
-
Installs a `pre-push` git hook that runs `diffimpactscout guard`
|
|
143
|
+
Installs a `pre-push` git hook that runs the configured hook scope on push: `diffimpactscout guard-and-impact-check` by default (guard checks then the impact blast-radius report), or `diffimpactscout guard` when `mode` is `pre-push`.
|
|
130
144
|
|
|
131
|
-
On first run the command detects the stack, prints a preview of the `.diffimpactscout.json` it would write, and (when stdin is a terminal) asks for confirmation. Answer `n` to override profile, blocking mode, extra checks, and
|
|
145
|
+
On first run the command detects the stack, prints a preview of the `.diffimpactscout.json` it would write, and (when stdin is a terminal) asks for confirmation. Answer `n` to override profile, blocking mode, extra checks, impact, and the hook scope (mode). Use `--yes` in CI to skip prompts, `--profile` and `--blocking` to pre-select settings, and `--reconfigure` to rewrite an existing config. Without `--reconfigure` an existing config is left untouched (and its mode is reused for the hook).
|
|
132
146
|
|
|
133
147
|
| Flag | Description |
|
|
134
148
|
| --- | --- |
|
|
@@ -139,6 +153,8 @@ On first run the command detects the stack, prints a preview of the `.diffimpact
|
|
|
139
153
|
| `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
|
|
140
154
|
| `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
|
|
141
155
|
|
|
156
|
+
After a hook is installed a non-blocking dependency check runs: tools referenced by the guard checks (`ruff`, `eslint`, `prettier`) that are missing on `PATH` are reported with install hints, so e.g. a first-time `ruff` install is prompted instead of silently degrading the checks.
|
|
157
|
+
|
|
142
158
|
### `diffimpactscout --version`
|
|
143
159
|
|
|
144
160
|
Prints the installed version.
|
|
@@ -150,7 +166,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
|
|
|
150
166
|
```json
|
|
151
167
|
{
|
|
152
168
|
"version": 1,
|
|
153
|
-
"mode": "
|
|
169
|
+
"mode": "guard-and-impact-check",
|
|
154
170
|
"ignore_paths": [
|
|
155
171
|
"**/node_modules/**",
|
|
156
172
|
"**/venv/**",
|
|
@@ -189,6 +205,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
|
|
|
189
205
|
|
|
190
206
|
| Section | Key | Meaning |
|
|
191
207
|
| --- | --- | --- |
|
|
208
|
+
| top-level | `mode` | Hook scope: `guard-and-impact-check` (default, runs the guard then the impact report on every push) or `pre-push` (guard only). |
|
|
192
209
|
| top-level | `ignore_paths` | Glob patterns (matching any path suffix) excluded from all checks and from impact scanning. |
|
|
193
210
|
| top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
|
|
194
211
|
| `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
|
|
@@ -226,12 +243,12 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
|
|
|
226
243
|
|
|
227
244
|
## Pre-push hook
|
|
228
245
|
|
|
229
|
-
`diffimpactscout install-hooks` writes a `pre-push` hook that runs `
|
|
246
|
+
`diffimpactscout install-hooks` writes a `pre-push` hook that runs the configured hook scope on every push — `guard-and-impact-check` by default (guard checks plus the impact report), `guard` only when `mode` is `pre-push`.
|
|
230
247
|
|
|
231
248
|
- The hook directory is resolved from `git rev-parse --git-common-dir` and honors `core.hooksPath`.
|
|
232
249
|
- The hook script is marked with `# diffimpactscout pre-push hook`. On `--uninstall` (or a re-install with `--force`), only a hook carrying this marker is touched.
|
|
233
250
|
- If a `pre-push` hook already exists without the marker, installation refuses with a `use --force to overwrite` message.
|
|
234
|
-
-
|
|
251
|
+
- If the tool is not found, the hook prints a notice and exits `0`, so pushes are never blocked by a missing install.
|
|
235
252
|
- Because the guard is warn-by-default, an installed hook reports issues without blocking the push unless you enable strict mode.
|
|
236
253
|
|
|
237
254
|
### Environment variables
|
|
@@ -248,7 +265,7 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
|
|
|
248
265
|
The `impact` command answers: if I push these changes, what else could break?
|
|
249
266
|
|
|
250
267
|
1. It parses the change-set (`git diff --find-renames`) and extracts the Python entities you added, removed, or renamed: classes, functions, methods, module fields, and class fields.
|
|
251
|
-
2. It builds an AST symbol map of every tracked Python file in the repo (skipping excluded paths), cached in `impact.cache_file`, and finds every reference to the changed symbols. Lookup kind follows the entity: class fields match attribute loads, methods match name and attribute loads, functions/classes also match imports.
|
|
268
|
+
2. It builds an AST symbol map of every tracked Python file in the repo (skipping excluded paths), cached in `impact.cache_file`, and finds every reference to the changed symbols. Lookup kind follows the entity: class fields match attribute loads, methods match name and attribute loads, functions/classes also match imports. To keep the blast radius honest, entities are scoped to the diff hunks that actually changed (a one-line tweak inside a large function does not report the whole function), and same-name references in other files are only reported when the file imports the name from the changed module (`views.orders` counts, a bare `orders` in an unrelated file does not).
|
|
252
269
|
3. Depending on the profile it extracts routes:
|
|
253
270
|
- `django`: `path()`/`re_path()`/`url()` entries in `urls_globs` that carry a `name=`.
|
|
254
271
|
- `fastapi`: `get`/`post`/`put`/`delete`/`patch`/`options` decorators on `router`/`app`-style objects in `urls_globs`.
|
|
@@ -49,11 +49,11 @@ diffimpactscout guard
|
|
|
49
49
|
# 3. Analyze the blast radius of your change
|
|
50
50
|
diffimpactscout impact
|
|
51
51
|
|
|
52
|
-
# 4.
|
|
52
|
+
# 4. Install the pre-push hook (guard + impact report by default)
|
|
53
53
|
diffimpactscout install-hooks
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
`init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)).
|
|
56
|
+
`init` accepts `--profile generic|django|fastapi|python|frontend` to seed profile-appropriate settings. `install-hooks` detects the stack automatically (see [Supported setups](#supported-setups)) and, unless the config's `mode` says otherwise, the hook runs both the guard and the impact report on every push.
|
|
57
57
|
|
|
58
58
|
## Supported setups
|
|
59
59
|
|
|
@@ -103,15 +103,29 @@ Analyzes the blast radius of the change-set. In an interactive terminal it print
|
|
|
103
103
|
| `--fast` | Skip template and frontend scanning; limit Python scanning to the changed files plus import-linked files. |
|
|
104
104
|
| `--json` | Emit the report as JSON (`{changed_count, rows, unresolved}`) instead of a markdown table. |
|
|
105
105
|
|
|
106
|
+
### `diffimpactscout guard-and-impact-check`
|
|
107
|
+
|
|
108
|
+
Runs the `guard` checks first, then always falls through to the `impact` blast-radius report so the developer sees the guard verdict and the affected areas together. This is the default pre-push hook scope, so one push runs the whole workflow. The command exits non-zero when the guard blocks in strict mode (or when impact is set to block), so a failing guard still surfaces the impact report but the push is rejected.
|
|
109
|
+
|
|
110
|
+
| Flag | Description |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `--staged` | Restrict both the guard and the impact analysis to staged files. |
|
|
113
|
+
| `--fast` | Passes through to `impact` (skip template/frontend scanning). |
|
|
114
|
+
| `--json` | Passes through to `impact` (emit the report as JSON). |
|
|
115
|
+
|
|
116
|
+
### `diffimpactscout dependency-check`
|
|
117
|
+
|
|
118
|
+
Verifies that every external tool referenced by the configured guard checks is on `PATH` (`ruff`, `eslint`, `prettier`). Missing tools print an install hint and exit `1`; a clean run prints an OK banner and exits `0`. `install-hooks` also runs a non-blocking version of this check after installing a hook, warning about missing tools only when stdin is a terminal.
|
|
119
|
+
|
|
106
120
|
### `diffimpactscout check CHECK_ID [files...]`
|
|
107
121
|
|
|
108
122
|
Runs a single check by id (see [Checks reference](#checks-reference)). With no file arguments it runs against the change-set.
|
|
109
123
|
|
|
110
124
|
### `diffimpactscout install-hooks`
|
|
111
125
|
|
|
112
|
-
Installs a `pre-push` git hook that runs `diffimpactscout guard`
|
|
126
|
+
Installs a `pre-push` git hook that runs the configured hook scope on push: `diffimpactscout guard-and-impact-check` by default (guard checks then the impact blast-radius report), or `diffimpactscout guard` when `mode` is `pre-push`.
|
|
113
127
|
|
|
114
|
-
On first run the command detects the stack, prints a preview of the `.diffimpactscout.json` it would write, and (when stdin is a terminal) asks for confirmation. Answer `n` to override profile, blocking mode, extra checks, and
|
|
128
|
+
On first run the command detects the stack, prints a preview of the `.diffimpactscout.json` it would write, and (when stdin is a terminal) asks for confirmation. Answer `n` to override profile, blocking mode, extra checks, impact, and the hook scope (mode). Use `--yes` in CI to skip prompts, `--profile` and `--blocking` to pre-select settings, and `--reconfigure` to rewrite an existing config. Without `--reconfigure` an existing config is left untouched (and its mode is reused for the hook).
|
|
115
129
|
|
|
116
130
|
| Flag | Description |
|
|
117
131
|
| --- | --- |
|
|
@@ -122,6 +136,8 @@ On first run the command detects the stack, prints a preview of the `.diffimpact
|
|
|
122
136
|
| `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
|
|
123
137
|
| `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
|
|
124
138
|
|
|
139
|
+
After a hook is installed a non-blocking dependency check runs: tools referenced by the guard checks (`ruff`, `eslint`, `prettier`) that are missing on `PATH` are reported with install hints, so e.g. a first-time `ruff` install is prompted instead of silently degrading the checks.
|
|
140
|
+
|
|
125
141
|
### `diffimpactscout --version`
|
|
126
142
|
|
|
127
143
|
Prints the installed version.
|
|
@@ -133,7 +149,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
|
|
|
133
149
|
```json
|
|
134
150
|
{
|
|
135
151
|
"version": 1,
|
|
136
|
-
"mode": "
|
|
152
|
+
"mode": "guard-and-impact-check",
|
|
137
153
|
"ignore_paths": [
|
|
138
154
|
"**/node_modules/**",
|
|
139
155
|
"**/venv/**",
|
|
@@ -172,6 +188,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
|
|
|
172
188
|
|
|
173
189
|
| Section | Key | Meaning |
|
|
174
190
|
| --- | --- | --- |
|
|
191
|
+
| top-level | `mode` | Hook scope: `guard-and-impact-check` (default, runs the guard then the impact report on every push) or `pre-push` (guard only). |
|
|
175
192
|
| top-level | `ignore_paths` | Glob patterns (matching any path suffix) excluded from all checks and from impact scanning. |
|
|
176
193
|
| top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
|
|
177
194
|
| `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
|
|
@@ -209,12 +226,12 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
|
|
|
209
226
|
|
|
210
227
|
## Pre-push hook
|
|
211
228
|
|
|
212
|
-
`diffimpactscout install-hooks` writes a `pre-push` hook that runs `
|
|
229
|
+
`diffimpactscout install-hooks` writes a `pre-push` hook that runs the configured hook scope on every push — `guard-and-impact-check` by default (guard checks plus the impact report), `guard` only when `mode` is `pre-push`.
|
|
213
230
|
|
|
214
231
|
- The hook directory is resolved from `git rev-parse --git-common-dir` and honors `core.hooksPath`.
|
|
215
232
|
- The hook script is marked with `# diffimpactscout pre-push hook`. On `--uninstall` (or a re-install with `--force`), only a hook carrying this marker is touched.
|
|
216
233
|
- If a `pre-push` hook already exists without the marker, installation refuses with a `use --force to overwrite` message.
|
|
217
|
-
-
|
|
234
|
+
- If the tool is not found, the hook prints a notice and exits `0`, so pushes are never blocked by a missing install.
|
|
218
235
|
- Because the guard is warn-by-default, an installed hook reports issues without blocking the push unless you enable strict mode.
|
|
219
236
|
|
|
220
237
|
### Environment variables
|
|
@@ -231,7 +248,7 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
|
|
|
231
248
|
The `impact` command answers: if I push these changes, what else could break?
|
|
232
249
|
|
|
233
250
|
1. It parses the change-set (`git diff --find-renames`) and extracts the Python entities you added, removed, or renamed: classes, functions, methods, module fields, and class fields.
|
|
234
|
-
2. It builds an AST symbol map of every tracked Python file in the repo (skipping excluded paths), cached in `impact.cache_file`, and finds every reference to the changed symbols. Lookup kind follows the entity: class fields match attribute loads, methods match name and attribute loads, functions/classes also match imports.
|
|
251
|
+
2. It builds an AST symbol map of every tracked Python file in the repo (skipping excluded paths), cached in `impact.cache_file`, and finds every reference to the changed symbols. Lookup kind follows the entity: class fields match attribute loads, methods match name and attribute loads, functions/classes also match imports. To keep the blast radius honest, entities are scoped to the diff hunks that actually changed (a one-line tweak inside a large function does not report the whole function), and same-name references in other files are only reported when the file imports the name from the changed module (`views.orders` counts, a bare `orders` in an unrelated file does not).
|
|
235
252
|
3. Depending on the profile it extracts routes:
|
|
236
253
|
- `django`: `path()`/`re_path()`/`url()` entries in `urls_globs` that carry a `name=`.
|
|
237
254
|
- `fastapi`: `get`/`post`/`put`/`delete`/`patch`/`options` decorators on `router`/`app`-style objects in `urls_globs`.
|
|
@@ -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.4.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",
|
|
@@ -34,7 +34,8 @@ the `HEAD` tree. Consequences:
|
|
|
34
34
|
|
|
35
35
|
Sprint branch resolution:
|
|
36
36
|
|
|
37
|
-
In sprint-based development models (e.g.
|
|
37
|
+
In sprint-based development models (e.g. custom in-house platforms) work is
|
|
38
|
+
branched from active
|
|
38
39
|
sprint branches such as `refs/remotes/upstream/sprint/33.1` rather than from
|
|
39
40
|
`master`. The resolver inspects `refs/remotes/upstream/master` plus the top
|
|
40
41
|
`DEFAULT_MAX_SPRINT_CANDIDATES` (5) most recent sprint branches sorted by
|
|
@@ -7,6 +7,7 @@ merge conflict, and a dev-added invalid JSON file is flagged.
|
|
|
7
7
|
import ast
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import warnings
|
|
10
11
|
|
|
11
12
|
from diffimpactscout.checks.base import (
|
|
12
13
|
Check,
|
|
@@ -64,7 +65,9 @@ class AstSyntaxCheck(Check):
|
|
|
64
65
|
except OSError:
|
|
65
66
|
continue
|
|
66
67
|
try:
|
|
67
|
-
|
|
68
|
+
with warnings.catch_warnings():
|
|
69
|
+
warnings.simplefilter("ignore", SyntaxWarning)
|
|
70
|
+
ast.parse(data.decode("utf-8-sig"), filename=path)
|
|
68
71
|
except SyntaxError as exc:
|
|
69
72
|
issues.append(
|
|
70
73
|
CheckIssue(
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
8
|
import diffimpactscout.config as config
|
|
9
|
+
import diffimpactscout.dependency_check as dependency_check
|
|
9
10
|
import diffimpactscout.detect as detect
|
|
10
11
|
import diffimpactscout.env as env
|
|
11
12
|
import diffimpactscout.gitrun as gitrun
|
|
@@ -53,6 +54,20 @@ def _parser():
|
|
|
53
54
|
)
|
|
54
55
|
p_impact.set_defaults(func=_cmd_impact)
|
|
55
56
|
|
|
57
|
+
p_guard_impact = sub.add_parser(
|
|
58
|
+
"guard-and-impact-check",
|
|
59
|
+
help="run guard checks then the impact blast-radius report (full pre-push pipeline)",
|
|
60
|
+
)
|
|
61
|
+
p_guard_impact.add_argument("--staged", action="store_true")
|
|
62
|
+
p_guard_impact.add_argument("--fast", action="store_true")
|
|
63
|
+
p_guard_impact.add_argument("--json", action="store_true")
|
|
64
|
+
p_guard_impact.add_argument(
|
|
65
|
+
"--markdown",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="emit a Markdown table instead of the plain ASCII table",
|
|
68
|
+
)
|
|
69
|
+
p_guard_impact.set_defaults(func=_cmd_guard_and_analyse_impact)
|
|
70
|
+
|
|
56
71
|
p_check = sub.add_parser(
|
|
57
72
|
"check", help="run a single check by id against files or the change-set"
|
|
58
73
|
)
|
|
@@ -93,6 +108,12 @@ def _parser():
|
|
|
93
108
|
)
|
|
94
109
|
p_hooks.set_defaults(func=_cmd_install_hooks)
|
|
95
110
|
|
|
111
|
+
p_dep = sub.add_parser(
|
|
112
|
+
"dependency-check",
|
|
113
|
+
help="verify the external tools referenced by the config are installed",
|
|
114
|
+
)
|
|
115
|
+
p_dep.set_defaults(func=_cmd_dependency_check)
|
|
116
|
+
|
|
96
117
|
return parser
|
|
97
118
|
|
|
98
119
|
|
|
@@ -157,6 +178,35 @@ def _cmd_impact(args):
|
|
|
157
178
|
)
|
|
158
179
|
|
|
159
180
|
|
|
181
|
+
def _cmd_guard_and_analyse_impact(args):
|
|
182
|
+
root = _require_repo()
|
|
183
|
+
if root is None:
|
|
184
|
+
return 1
|
|
185
|
+
cfg = config.load_config(root)
|
|
186
|
+
guard_rc = guard.run_guard(
|
|
187
|
+
root,
|
|
188
|
+
cfg,
|
|
189
|
+
staged=args.staged,
|
|
190
|
+
)
|
|
191
|
+
impact_rc = impact_module.run_impact(
|
|
192
|
+
root,
|
|
193
|
+
cfg,
|
|
194
|
+
staged=args.staged,
|
|
195
|
+
fast=args.fast,
|
|
196
|
+
json_out=args.json,
|
|
197
|
+
markdown=args.markdown,
|
|
198
|
+
)
|
|
199
|
+
return guard_rc if guard_rc != 0 else impact_rc
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _cmd_dependency_check(args):
|
|
203
|
+
root = _require_repo()
|
|
204
|
+
if root is None:
|
|
205
|
+
return 1
|
|
206
|
+
cfg = config.load_config(root)
|
|
207
|
+
return dependency_check.run_dependency_check(root, cfg)
|
|
208
|
+
|
|
209
|
+
|
|
160
210
|
def _cmd_check(args):
|
|
161
211
|
root = _require_repo()
|
|
162
212
|
if root is None:
|
|
@@ -263,6 +313,21 @@ def _prompt_blocking(default):
|
|
|
263
313
|
return default
|
|
264
314
|
|
|
265
315
|
|
|
316
|
+
def _prompt_mode(default):
|
|
317
|
+
sys.stdout.write(
|
|
318
|
+
"Push hook scope: guard-and-impact-check (recommended: guard checks + "
|
|
319
|
+
"impact blast-radius report) or pre-push (guard checks only)? "
|
|
320
|
+
"[guard-and-impact-check/Pre-push] (default: %s): " % default
|
|
321
|
+
)
|
|
322
|
+
sys.stdout.flush()
|
|
323
|
+
answer = _read_answer()
|
|
324
|
+
if answer in ("guard-and-impact-check", "g"):
|
|
325
|
+
return config.MODE_GUARD_AND_IMPACT
|
|
326
|
+
if answer in ("pre-push", "p"):
|
|
327
|
+
return config.MODE_PRE_PUSH
|
|
328
|
+
return default
|
|
329
|
+
|
|
330
|
+
|
|
266
331
|
def _check_id(entry):
|
|
267
332
|
if isinstance(entry, dict):
|
|
268
333
|
return entry.get("id", "?")
|
|
@@ -284,6 +349,14 @@ def _render_preview(detected, cfg):
|
|
|
284
349
|
checks = cfg["guard"]["checks"]
|
|
285
350
|
impact_on = profile != config.DEFAULT_PROFILE
|
|
286
351
|
blocking = cfg["guard"].get("blocking", "warn")
|
|
352
|
+
mode = cfg.get("mode") or config.DEFAULT_MODE
|
|
353
|
+
if mode not in (config.MODE_PRE_PUSH, config.MODE_GUARD_AND_IMPACT):
|
|
354
|
+
mode = config.DEFAULT_MODE
|
|
355
|
+
hook_command = (
|
|
356
|
+
config.MODE_GUARD_AND_IMPACT
|
|
357
|
+
if mode == config.MODE_GUARD_AND_IMPACT
|
|
358
|
+
else "guard"
|
|
359
|
+
)
|
|
287
360
|
lines = []
|
|
288
361
|
if detected == profile:
|
|
289
362
|
lines.append("detected framework: %s" % detected)
|
|
@@ -291,13 +364,15 @@ def _render_preview(detected, cfg):
|
|
|
291
364
|
lines.append("diffimpactscout will write .diffimpactscout.json")
|
|
292
365
|
lines.append(" profile : %s" % profile)
|
|
293
366
|
lines.append(" blocking : %s" % blocking)
|
|
367
|
+
lines.append(" mode : %s" % mode)
|
|
294
368
|
lines.append(" impact : %s" % ("on" if impact_on else "off"))
|
|
295
369
|
lines.append(" checks : %d" % len(checks))
|
|
296
370
|
for entry in checks:
|
|
297
371
|
lines.append(" - %s" % _check_id(entry))
|
|
298
372
|
lines.append("")
|
|
299
373
|
lines.append(
|
|
300
|
-
" then installs the pre-push hook -> runs `diffimpactscout
|
|
374
|
+
" then installs the pre-push hook -> runs `diffimpactscout %s` on each push."
|
|
375
|
+
% hook_command
|
|
301
376
|
)
|
|
302
377
|
lines.append("")
|
|
303
378
|
return "\n".join(lines)
|
|
@@ -321,6 +396,7 @@ def _build_config_data(profile, args):
|
|
|
321
396
|
data = config._deep_merge(data, config.load_profile(profile))
|
|
322
397
|
if args.blocking:
|
|
323
398
|
data["guard"]["blocking"] = args.blocking
|
|
399
|
+
data["mode"] = data.get("mode") or config.DEFAULT_MODE
|
|
324
400
|
return data
|
|
325
401
|
|
|
326
402
|
|
|
@@ -371,6 +447,7 @@ def _ask_overrides(data, args):
|
|
|
371
447
|
config._defaults(), config.load_profile("generic")
|
|
372
448
|
)
|
|
373
449
|
data["impact"] = generic["impact"]
|
|
450
|
+
data["mode"] = _prompt_mode(data.get("mode") or config.DEFAULT_MODE)
|
|
374
451
|
|
|
375
452
|
|
|
376
453
|
def _cmd_install_hooks(args):
|
|
@@ -390,6 +467,7 @@ def _cmd_install_hooks(args):
|
|
|
390
467
|
else:
|
|
391
468
|
print("nothing to remove")
|
|
392
469
|
return 0
|
|
470
|
+
data = None
|
|
393
471
|
cfg_path = os.path.join(root, config.CFG_NAME)
|
|
394
472
|
has_config = os.path.exists(cfg_path)
|
|
395
473
|
proceed = True
|
|
@@ -422,8 +500,20 @@ def _cmd_install_hooks(args):
|
|
|
422
500
|
if not proceed:
|
|
423
501
|
print("diffimpactscout: setup skipped; no changes made")
|
|
424
502
|
return 0
|
|
503
|
+
mode = None
|
|
504
|
+
if has_config and not args.reconfigure:
|
|
505
|
+
existing = config._read_user(root)
|
|
506
|
+
if existing and isinstance(existing.get("mode"), str):
|
|
507
|
+
mode = existing["mode"]
|
|
508
|
+
if mode is None:
|
|
509
|
+
mode = (data or {}).get("mode") or config.DEFAULT_MODE
|
|
510
|
+
command = (
|
|
511
|
+
config.MODE_GUARD_AND_IMPACT
|
|
512
|
+
if mode == config.MODE_GUARD_AND_IMPACT
|
|
513
|
+
else "guard"
|
|
514
|
+
)
|
|
425
515
|
try:
|
|
426
|
-
installed = launcher.install_hook(root, force=args.force)
|
|
516
|
+
installed = launcher.install_hook(root, force=args.force, command=command)
|
|
427
517
|
except OSError as exc:
|
|
428
518
|
sys.stderr.write(
|
|
429
519
|
"diffimpactscout: cannot install pre-push hook: %s\n" % exc
|
|
@@ -432,4 +522,6 @@ def _cmd_install_hooks(args):
|
|
|
432
522
|
if not installed:
|
|
433
523
|
return 1
|
|
434
524
|
print("pre-push hook installed")
|
|
435
|
-
|
|
525
|
+
effective = config.load_config(root)
|
|
526
|
+
dependency_check.report_missing(effective)
|
|
527
|
+
return 0
|
|
@@ -11,6 +11,8 @@ CFG_NAME = ".diffimpactscout.json"
|
|
|
11
11
|
DEFAULT_PROFILE = "generic"
|
|
12
12
|
PROFILE_CHOICES = ("generic", "django", "fastapi", "python", "frontend")
|
|
13
13
|
MODE_PRE_PUSH = "pre-push"
|
|
14
|
+
MODE_GUARD_AND_IMPACT = "guard-and-impact-check"
|
|
15
|
+
DEFAULT_MODE = MODE_GUARD_AND_IMPACT
|
|
14
16
|
|
|
15
17
|
DEFAULT_IGNORE_PATHS = [
|
|
16
18
|
"**/node_modules/**",
|
|
@@ -40,7 +42,7 @@ DEFAULT_GUARD_CHECKS = [
|
|
|
40
42
|
def _defaults():
|
|
41
43
|
return {
|
|
42
44
|
"version": 1,
|
|
43
|
-
"mode":
|
|
45
|
+
"mode": DEFAULT_MODE,
|
|
44
46
|
"ignore_paths": list(DEFAULT_IGNORE_PATHS),
|
|
45
47
|
"use_gitignore": False,
|
|
46
48
|
"guard": {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Check that the tools referenced by the guard checks and impact profile are available.
|
|
2
|
+
|
|
3
|
+
Example: a Django profile that enables ruff/ruff-format reports `ruff` as
|
|
4
|
+
missing with an install hint the first time hooks are set up.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
_CHECK_TOOLS = (
|
|
11
|
+
("ruff", "ruff", "pip install ruff"),
|
|
12
|
+
("eslint", "eslint", "npm install --save-dev eslint"),
|
|
13
|
+
("prettier", "prettier", "npm install --save-dev prettier"),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_TOOL_CHECK_IDS = {
|
|
17
|
+
"ruff": "ruff",
|
|
18
|
+
"ruff-format": "ruff",
|
|
19
|
+
"eslint": "eslint",
|
|
20
|
+
"prettier": "prettier",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def required_tools(cfg):
|
|
25
|
+
"""Return the set of tool names referenced by the current config."""
|
|
26
|
+
tools = set()
|
|
27
|
+
for entry in cfg.get("guard", {}).get("checks") or []:
|
|
28
|
+
if isinstance(entry, dict):
|
|
29
|
+
check_id = entry.get("id")
|
|
30
|
+
else:
|
|
31
|
+
check_id = entry
|
|
32
|
+
mapped = _TOOL_CHECK_IDS.get(check_id)
|
|
33
|
+
if mapped:
|
|
34
|
+
tools.add(mapped)
|
|
35
|
+
return sorted(tools)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve(binary):
|
|
39
|
+
return shutil.which(binary) or shutil.which(binary + ".exe")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_tools(cfg):
|
|
43
|
+
"""Return a list of {"tool", "found", "hint"} for every config-referenced tool."""
|
|
44
|
+
results = []
|
|
45
|
+
for tool, binary, hint in _CHECK_TOOLS:
|
|
46
|
+
if tool in required_tools(cfg):
|
|
47
|
+
results.append(
|
|
48
|
+
{
|
|
49
|
+
"tool": tool,
|
|
50
|
+
"binary": binary,
|
|
51
|
+
"found": _resolve(binary) is not None,
|
|
52
|
+
"hint": hint,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
return results
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_dependency_check(root, cfg):
|
|
59
|
+
results = check_tools(cfg)
|
|
60
|
+
if not results:
|
|
61
|
+
print("No external tools are referenced by the current configuration.")
|
|
62
|
+
return 0
|
|
63
|
+
missing = [r for r in results if not r["found"]]
|
|
64
|
+
print("=" * 70)
|
|
65
|
+
print(" DiffImpactScout Dependency Check")
|
|
66
|
+
print("=" * 70)
|
|
67
|
+
for result in results:
|
|
68
|
+
state = "OK" if result["found"] else "MISSING"
|
|
69
|
+
location = ""
|
|
70
|
+
if result["found"]:
|
|
71
|
+
location = " (%s)" % _resolve(result["binary"])
|
|
72
|
+
print(
|
|
73
|
+
" [%s] %s%s" % (state, result["tool"], location)
|
|
74
|
+
)
|
|
75
|
+
print("-" * 70)
|
|
76
|
+
if not missing:
|
|
77
|
+
print(" All referenced tools are available.")
|
|
78
|
+
print("=" * 70)
|
|
79
|
+
return 0
|
|
80
|
+
for result in missing:
|
|
81
|
+
sys.stderr.write(
|
|
82
|
+
"diffimpactscout: %s is required by the guard checks but was not "
|
|
83
|
+
"found. Install it with: %s\n" % (result["tool"], result["hint"])
|
|
84
|
+
)
|
|
85
|
+
sys.stderr.write(
|
|
86
|
+
"diffimpactscout: missing tools degrade the affected checks to warnings; "
|
|
87
|
+
"install them to enforce the full guard.\n"
|
|
88
|
+
)
|
|
89
|
+
print("=" * 70)
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def report_missing(cfg, tty=None):
|
|
94
|
+
"""Print a non-fatal warning for every referenced-but-missing tool."""
|
|
95
|
+
if tty is None:
|
|
96
|
+
try:
|
|
97
|
+
tty = sys.stdin.isatty()
|
|
98
|
+
except (OSError, ValueError):
|
|
99
|
+
tty = False
|
|
100
|
+
if not tty:
|
|
101
|
+
return
|
|
102
|
+
missing = [r for r in check_tools(cfg) if not r["found"]]
|
|
103
|
+
for result in missing:
|
|
104
|
+
sys.stderr.write(
|
|
105
|
+
"diffimpactscout: %s was not found; the affected checks will only "
|
|
106
|
+
"warn until it is installed (%s).\n"
|
|
107
|
+
% (result["tool"], result["hint"])
|
|
108
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Quiet AST parsing of third-party source.
|
|
2
|
+
|
|
3
|
+
Analyzed code may carry latent issues (for example invalid escape sequences
|
|
4
|
+
in string literals) that Python surfaces as ``SyntaxWarning`` when compiling
|
|
5
|
+
the module. Those warnings must not leak into the tool's report output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_quiet(source, filename="<unknown>"):
|
|
13
|
+
with warnings.catch_warnings():
|
|
14
|
+
warnings.simplefilter("ignore", SyntaxWarning)
|
|
15
|
+
return ast.parse(source, filename=filename)
|