diffimpactscout 0.2.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.
Files changed (48) hide show
  1. {diffimpactscout-0.2.0/src/diffimpactscout.egg-info → diffimpactscout-0.4.0}/PKG-INFO +26 -9
  2. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/README.md +25 -8
  3. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/setup.py +1 -1
  4. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/__init__.py +1 -1
  5. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/base.py +44 -2
  6. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/base.py +14 -10
  7. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/eslint.py +2 -2
  8. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/prettier.py +2 -2
  9. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/repo_checks.py +36 -6
  10. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/ruff.py +4 -4
  11. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/syntax.py +14 -7
  12. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/cli.py +119 -8
  13. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/config.py +5 -1
  14. diffimpactscout-0.4.0/src/diffimpactscout/dependency_check.py +108 -0
  15. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/guard.py +85 -8
  16. diffimpactscout-0.4.0/src/diffimpactscout/impact/_parse.py +15 -0
  17. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/diff_parser.py +73 -4
  18. diffimpactscout-0.4.0/src/diffimpactscout/impact/impact.py +571 -0
  19. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/python_analyzer.py +159 -21
  20. diffimpactscout-0.4.0/src/diffimpactscout/impact/reporter.py +263 -0
  21. diffimpactscout-0.4.0/src/diffimpactscout/impact/route_linker.py +631 -0
  22. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/launcher.py +9 -7
  23. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0/src/diffimpactscout.egg-info}/PKG-INFO +26 -9
  24. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/SOURCES.txt +2 -0
  25. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/tests/test_fixture_smoke.py +2 -0
  26. diffimpactscout-0.2.0/src/diffimpactscout/impact/impact.py +0 -345
  27. diffimpactscout-0.2.0/src/diffimpactscout/impact/reporter.py +0 -107
  28. diffimpactscout-0.2.0/src/diffimpactscout/impact/route_linker.py +0 -363
  29. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/LICENSE +0 -0
  30. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/pyproject.toml +0 -0
  31. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/setup.cfg +0 -0
  32. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/__main__.py +0 -0
  33. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/__init__.py +0 -0
  34. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/checks/hygiene.py +0 -0
  35. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/detect.py +0 -0
  36. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/env.py +0 -0
  37. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/gitrun.py +0 -0
  38. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/__init__.py +0 -0
  39. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/impact/cache.py +0 -0
  40. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/django.json +0 -0
  41. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/fastapi.json +0 -0
  42. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/frontend.json +0 -0
  43. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/generic.json +0 -0
  44. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/profiles/python.json +0 -0
  45. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout/scope.py +0 -0
  46. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/dependency_links.txt +0 -0
  47. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/entry_points.txt +0 -0
  48. {diffimpactscout-0.2.0 → diffimpactscout-0.4.0}/src/diffimpactscout.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffimpactscout
3
- Version: 0.2.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. Run the guard automatically on every push
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` on push.
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 impact. 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.
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": "pre-push",
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 `diffimpactscout guard` on every push.
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
- - The hook runs `diffimpactscout guard`; if the tool is not found, it prints a notice and exits `0`, so pushes are never blocked by a missing install.
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. Run the guard automatically on every push
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` on push.
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 impact. 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.
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": "pre-push",
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 `diffimpactscout guard` on every push.
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
- - The hook runs `diffimpactscout guard`; if the tool is not found, it prints a notice and exits `0`, so pushes are never blocked by a missing install.
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.2.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",
@@ -1,3 +1,3 @@
1
1
  """diffimpactscout: incremental pre-push guard and AST blast-radius impact analyzer."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -2,13 +2,55 @@
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. custom in-house platforms) work is
38
+ branched from active
39
+ sprint branches such as `refs/remotes/upstream/sprint/33.1` rather than from
40
+ `master`. The resolver inspects `refs/remotes/upstream/master` plus the top
41
+ `DEFAULT_MAX_SPRINT_CANDIDATES` (5) most recent sprint branches sorted by
42
+ committer date, and picks the candidate whose tree is closest to `HEAD` (the
43
+ fewest changed files on a `--diff-filter=ACMRT` name-only diff). The closest
44
+ tree means the smallest honest change-set, which is what incremental checks
45
+ want to target.
46
+ ================================================================================
5
47
  """
6
48
 
7
49
  import diffimpactscout.gitrun as gitrun
8
50
 
9
51
  MASTER_REF = "refs/remotes/upstream/master"
10
52
  SPRINT_PREFIX = "sprint/"
11
- DEFAULT_MAX_SPRINT_CANDIDATES = 10
53
+ DEFAULT_MAX_SPRINT_CANDIDATES = 5
12
54
  REMOTE_FALLBACKS = ("main", "master", "develop")
13
55
 
14
56
 
@@ -16,7 +58,7 @@ def resolve_change_base(root, limit=DEFAULT_MAX_SPRINT_CANDIDATES):
16
58
  """Return the remote ref closest to HEAD, or None.
17
59
 
18
60
  `limit` caps how many `sprint/*` refs are considered: only the
19
- `limit` (default 10) most-recent ones by committer date, unlike the
61
+ `limit` (default 5) most-recent ones by committer date, unlike the
20
62
  bash original dev_base.sh which considered every sprint branch.
21
63
  """
22
64
  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
- issues.append(CheckIssue(".", 0, 0, self.id, output))
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
- issues.append(CheckIssue(path, 0, 0, self.id, output))
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
- return "%s:%s:%s: %s %s" % (
163
- self.path,
164
- self.line,
165
- self.column,
166
- self.code,
167
- self.message,
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):
@@ -155,8 +155,8 @@ class EslintCheck(Check):
155
155
  issues.append(
156
156
  CheckIssue(
157
157
  path,
158
- line or 0,
159
- column or 0,
158
+ line,
159
+ column,
160
160
  rule_id or self.id,
161
161
  message.get("message") or "",
162
162
  )
@@ -93,8 +93,8 @@ class PrettierCheck(Check):
93
93
  issues.append(
94
94
  CheckIssue(
95
95
  path,
96
- 0,
97
- 0,
96
+ None,
97
+ None,
98
98
  self.id,
99
99
  "%s is not formatted. Fix with: npx prettier --write %s"
100
100
  % (path, path),
@@ -1,11 +1,14 @@
1
- """Repo-wide checks for oversized files and leaked private keys.
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, and any file
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
- 0,
65
- 0,
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, 0, 0, self.id, "private key detected")
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 or 0,
161
- location.get("column") or 0,
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
- 0,
219
- 0,
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),
@@ -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,
@@ -38,8 +39,8 @@ class JsonSyntaxCheck(Check):
38
39
  issues.append(
39
40
  CheckIssue(
40
41
  path,
41
- getattr(exc, "lineno", 0) or 0,
42
- getattr(exc, "colno", 0) or 0,
42
+ getattr(exc, "lineno", None),
43
+ getattr(exc, "colno", None),
43
44
  self.id,
44
45
  "invalid JSON: %s" % exc,
45
46
  )
@@ -64,13 +65,15 @@ class AstSyntaxCheck(Check):
64
65
  except OSError:
65
66
  continue
66
67
  try:
67
- ast.parse(data.decode("utf-8-sig"), filename=path)
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(
71
74
  path,
72
- exc.lineno or 0,
73
- exc.offset or 0,
75
+ exc.lineno,
76
+ exc.offset,
74
77
  self.id,
75
78
  "invalid python syntax: %s" % (exc.msg or "syntax error"),
76
79
  )
@@ -78,7 +81,11 @@ class AstSyntaxCheck(Check):
78
81
  except ValueError as exc:
79
82
  issues.append(
80
83
  CheckIssue(
81
- path, 0, 0, self.id, "invalid python source: %s" % exc
84
+ path,
85
+ None,
86
+ None,
87
+ self.id,
88
+ "invalid python source: %s" % exc,
82
89
  )
83
90
  )
84
91
  return CheckResult(issues=issues)
@@ -111,7 +118,7 @@ class MergeConflictCheck(Check):
111
118
  line = lines[lineno - 1]
112
119
  if self._is_marker(line):
113
120
  issues.append(
114
- CheckIssue(path, lineno, 0, self.id, "conflict marker found")
121
+ CheckIssue(path, lineno, None, self.id, "conflict marker found")
115
122
  )
116
123
  return CheckResult(issues=issues)
117
124