diffimpactscout 0.1.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.
Files changed (45) hide show
  1. {diffimpactscout-0.1.0/src/diffimpactscout.egg-info → diffimpactscout-0.3.0}/PKG-INFO +32 -10
  2. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/README.md +30 -8
  3. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/setup.py +2 -2
  4. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/__init__.py +1 -1
  5. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/base.py +43 -2
  6. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/base.py +14 -10
  7. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/eslint.py +2 -2
  8. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/prettier.py +2 -2
  9. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/repo_checks.py +36 -6
  10. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/ruff.py +20 -5
  11. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/syntax.py +10 -6
  12. diffimpactscout-0.3.0/src/diffimpactscout/cli.py +435 -0
  13. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/config.py +4 -1
  14. diffimpactscout-0.3.0/src/diffimpactscout/detect.py +91 -0
  15. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/guard.py +85 -8
  16. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/impact.py +8 -3
  17. diffimpactscout-0.3.0/src/diffimpactscout/impact/reporter.py +191 -0
  18. diffimpactscout-0.3.0/src/diffimpactscout/profiles/frontend.json +14 -0
  19. diffimpactscout-0.1.0/src/diffimpactscout/profiles/plain.json → diffimpactscout-0.3.0/src/diffimpactscout/profiles/generic.json +1 -1
  20. diffimpactscout-0.3.0/src/diffimpactscout/profiles/python.json +14 -0
  21. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0/src/diffimpactscout.egg-info}/PKG-INFO +32 -10
  22. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/SOURCES.txt +4 -1
  23. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/tests/test_fixture_smoke.py +2 -0
  24. diffimpactscout-0.1.0/src/diffimpactscout/cli.py +0 -220
  25. diffimpactscout-0.1.0/src/diffimpactscout/impact/reporter.py +0 -107
  26. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/LICENSE +0 -0
  27. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/pyproject.toml +0 -0
  28. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/setup.cfg +0 -0
  29. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/__main__.py +0 -0
  30. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/__init__.py +0 -0
  31. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/checks/hygiene.py +0 -0
  32. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/env.py +0 -0
  33. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/gitrun.py +0 -0
  34. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/__init__.py +0 -0
  35. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/cache.py +0 -0
  36. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/diff_parser.py +0 -0
  37. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/python_analyzer.py +0 -0
  38. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/impact/route_linker.py +0 -0
  39. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/launcher.py +0 -0
  40. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/django.json +0 -0
  41. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/profiles/fastapi.json +0 -0
  42. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout/scope.py +0 -0
  43. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/dependency_links.txt +0 -0
  44. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/entry_points.txt +0 -0
  45. {diffimpactscout-0.1.0 → diffimpactscout-0.3.0}/src/diffimpactscout.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffimpactscout
3
- Version: 0.1.0
3
+ Version: 0.3.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
7
- Requires-Python: >=3.6
7
+ Requires-Python: >=3.8
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Dynamic: author
@@ -30,7 +30,7 @@ An incremental pre-push guard and AST blast-radius impact analyzer for git repos
30
30
  - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
31
31
  - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
32
32
  - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
33
- - Profiles for `plain`, `django`, and `fastapi` projects.
33
+ - Profiles for `generic`, `django`, `fastapi`, `python`, and `frontend` projects.
34
34
  - Configurable pre-push hook installation that never silently clobbers an existing hook.
35
35
 
36
36
  ## Core idea
@@ -50,7 +50,7 @@ The change-set also handles the full breadth of realistic push shapes: direct pu
50
50
  pip install diffimpactscout
51
51
  ```
52
52
 
53
- The package is pure Python (standard library only at runtime) and requires Python 3.6 or newer and the `git` CLI on `PATH`.
53
+ The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the `git` CLI on `PATH`.
54
54
 
55
55
  Optional tools are only needed for the checks that use them: `ruff`, `eslint` (via `npx`), and `prettier` (via `npx`). If one is missing, the affected check emits a warning and the guard continues; it never blocks a push because a tool is absent.
56
56
 
@@ -70,7 +70,21 @@ diffimpactscout impact
70
70
  diffimpactscout install-hooks
71
71
  ```
72
72
 
73
- `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
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)).
74
+
75
+ ## Supported setups
76
+
77
+ `install-hooks` detects the project stack on first run and writes a `.diffimpactscout.json` that fits it:
78
+
79
+ | Detection signal | Profile | Guard checks | Impact globs |
80
+ | --- | --- | --- | --- |
81
+ | `manage.py` + `settings.py`/`wsgi.py` | django | default + `ruff`, `ruff-format` | urls, templates, frontend |
82
+ | `fastapi` in deps or `main.py` | fastapi | default + `ruff`, `ruff-format` | `**/*.py` routes + frontend globs |
83
+ | `package.json` + `src`/`app` `*.ts`/`*.js` | frontend | default + `eslint`, `prettier` | templates + frontend globs |
84
+ | `*.py` present | python | default + `ruff`, `ruff-format` | Python module references |
85
+ | anything else (or no signal) | generic | default from `init` | none |
86
+
87
+ Other ecosystems (Go, Rust, Java, Ruby, ...) are detected and reported as "planned for future releases" in the preview; the generic profile still installs the universal guard checks. Add a language linter with an [external check](#configuration).
74
88
 
75
89
  ## Commands
76
90
 
@@ -82,7 +96,7 @@ Writes a `.diffimpactscout.json` into the current directory. If the file already
82
96
 
83
97
  | Flag | Description |
84
98
  | --- | --- |
85
- | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
99
+ | `--profile {generic,django,fastapi,python,frontend}` | Seed defaults from the named profile (default: `generic`). |
86
100
 
87
101
  ### `diffimpactscout guard`
88
102
 
@@ -114,8 +128,14 @@ Runs a single check by id (see [Checks reference](#checks-reference)). With no f
114
128
 
115
129
  Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
116
130
 
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.
132
+
117
133
  | Flag | Description |
118
134
  | --- | --- |
135
+ | `--profile {generic,django,fastapi,python,frontend}` | Pre-select the setup profile; skips the stack question. |
136
+ | `--blocking {warn,strict}` | Blocking mode for guard checks (default: `warn`). |
137
+ | `--yes`, `-y` | Accept defaults and skip prompts (non-interactive). |
138
+ | `--reconfigure` | Rewrite an existing `.diffimpactscout.json`. |
119
139
  | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
120
140
  | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
121
141
 
@@ -156,7 +176,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
156
176
  "blocking": "warn"
157
177
  },
158
178
  "impact": {
159
- "profile": "plain",
179
+ "profile": "generic",
160
180
  "urls_globs": [],
161
181
  "template_globs": [],
162
182
  "frontend_globs": [],
@@ -173,7 +193,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
173
193
  | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
174
194
  | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
175
195
  | `guard` | `blocking` | `"warn"` (default) or `"strict"`. In strict mode every check marked blocking can fail the run. |
176
- | `impact` | `profile` | `plain`, `django`, or `fastapi`; selects route extraction plus default globs. |
196
+ | `impact` | `profile` | `generic`, `django`, or `fastapi`; selects route extraction plus default globs. |
177
197
  | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
178
198
  | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
179
199
  | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
@@ -188,9 +208,11 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
188
208
 
189
209
  ### Profiles
190
210
 
191
- - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
211
+ - `generic`: no route or cross-layer globs; impact analysis reports only Python references.
212
+ - `python`: same impact globs as `generic`; guard checks append `ruff` and `ruff-format`.
192
213
  - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
193
214
  - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
215
+ - `frontend`: template globs `**/*.html` and frontend globs `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `eslint` and `prettier`.
194
216
 
195
217
  ### Blocking behavior
196
218
 
@@ -284,7 +306,7 @@ All checks are incremental: file-scoped checks run on files in the change-set, a
284
306
 
285
307
  ## Requirements
286
308
 
287
- - Python 3.6 or newer.
309
+ - Python 3.8 or newer.
288
310
  - Standard library only at runtime; no third-party dependencies.
289
311
  - The `git` CLI on `PATH`.
290
312
  - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
@@ -13,7 +13,7 @@ An incremental pre-push guard and AST blast-radius impact analyzer for git repos
13
13
  - Python AST symbol map: detects changed classes, functions, methods, module fields, and class fields, including deletions and renames.
14
14
  - Cross-layer impact linking: Python symbols to Django `{% url %}` template tags and frontend `http.get()/post()`-style calls.
15
15
  - Symbol cache (`.impact_analysis_cache.json`) so repeated runs skip unchanged files.
16
- - Profiles for `plain`, `django`, and `fastapi` projects.
16
+ - Profiles for `generic`, `django`, `fastapi`, `python`, and `frontend` projects.
17
17
  - Configurable pre-push hook installation that never silently clobbers an existing hook.
18
18
 
19
19
  ## Core idea
@@ -33,7 +33,7 @@ The change-set also handles the full breadth of realistic push shapes: direct pu
33
33
  pip install diffimpactscout
34
34
  ```
35
35
 
36
- The package is pure Python (standard library only at runtime) and requires Python 3.6 or newer and the `git` CLI on `PATH`.
36
+ The package is pure Python (standard library only at runtime) and requires Python 3.8 or newer and the `git` CLI on `PATH`.
37
37
 
38
38
  Optional tools are only needed for the checks that use them: `ruff`, `eslint` (via `npx`), and `prettier` (via `npx`). If one is missing, the affected check emits a warning and the guard continues; it never blocks a push because a tool is absent.
39
39
 
@@ -53,7 +53,21 @@ diffimpactscout impact
53
53
  diffimpactscout install-hooks
54
54
  ```
55
55
 
56
- `init` accepts `--profile plain|django|fastapi` to seed profile-appropriate settings.
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)).
57
+
58
+ ## Supported setups
59
+
60
+ `install-hooks` detects the project stack on first run and writes a `.diffimpactscout.json` that fits it:
61
+
62
+ | Detection signal | Profile | Guard checks | Impact globs |
63
+ | --- | --- | --- | --- |
64
+ | `manage.py` + `settings.py`/`wsgi.py` | django | default + `ruff`, `ruff-format` | urls, templates, frontend |
65
+ | `fastapi` in deps or `main.py` | fastapi | default + `ruff`, `ruff-format` | `**/*.py` routes + frontend globs |
66
+ | `package.json` + `src`/`app` `*.ts`/`*.js` | frontend | default + `eslint`, `prettier` | templates + frontend globs |
67
+ | `*.py` present | python | default + `ruff`, `ruff-format` | Python module references |
68
+ | anything else (or no signal) | generic | default from `init` | none |
69
+
70
+ Other ecosystems (Go, Rust, Java, Ruby, ...) are detected and reported as "planned for future releases" in the preview; the generic profile still installs the universal guard checks. Add a language linter with an [external check](#configuration).
57
71
 
58
72
  ## Commands
59
73
 
@@ -65,7 +79,7 @@ Writes a `.diffimpactscout.json` into the current directory. If the file already
65
79
 
66
80
  | Flag | Description |
67
81
  | --- | --- |
68
- | `--profile {plain,django,fastapi}` | Seed defaults from the named profile (default: `plain`). |
82
+ | `--profile {generic,django,fastapi,python,frontend}` | Seed defaults from the named profile (default: `generic`). |
69
83
 
70
84
  ### `diffimpactscout guard`
71
85
 
@@ -97,8 +111,14 @@ Runs a single check by id (see [Checks reference](#checks-reference)). With no f
97
111
 
98
112
  Installs a `pre-push` git hook that runs `diffimpactscout guard` on push.
99
113
 
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.
115
+
100
116
  | Flag | Description |
101
117
  | --- | --- |
118
+ | `--profile {generic,django,fastapi,python,frontend}` | Pre-select the setup profile; skips the stack question. |
119
+ | `--blocking {warn,strict}` | Blocking mode for guard checks (default: `warn`). |
120
+ | `--yes`, `-y` | Accept defaults and skip prompts (non-interactive). |
121
+ | `--reconfigure` | Rewrite an existing `.diffimpactscout.json`. |
102
122
  | `--force` | Overwrite an existing `pre-push` hook that DiffImpactScout did not install. |
103
123
  | `--uninstall` | Remove the DiffImpactScout `pre-push` hook. |
104
124
 
@@ -139,7 +159,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
139
159
  "blocking": "warn"
140
160
  },
141
161
  "impact": {
142
- "profile": "plain",
162
+ "profile": "generic",
143
163
  "urls_globs": [],
144
164
  "template_globs": [],
145
165
  "frontend_globs": [],
@@ -156,7 +176,7 @@ DiffImpactScout is configured by a `.diffimpactscout.json` file in the repositor
156
176
  | top-level | `use_gitignore` | When `true`, also excludes paths ignored by `git check-ignore`. |
157
177
  | `guard` | `checks` | Ordered list of check entries; each `{"id": ...}` may add `args`, `blocking`, and `always_block` overrides. |
158
178
  | `guard` | `blocking` | `"warn"` (default) or `"strict"`. In strict mode every check marked blocking can fail the run. |
159
- | `impact` | `profile` | `plain`, `django`, or `fastapi`; selects route extraction plus default globs. |
179
+ | `impact` | `profile` | `generic`, `django`, or `fastapi`; selects route extraction plus default globs. |
160
180
  | `impact` | `urls_globs` / `template_globs` / `frontend_globs` | Glob patterns for route files, Django templates, and frontend sources. |
161
181
  | `impact` | `cache_file` | Path of the symbol cache (relative to the repo root). |
162
182
  | `impact` | `fast_mode`, `threads` | Reserved defaults from `init`; fast mode is currently selected with the `impact --fast` flag. |
@@ -171,9 +191,11 @@ External checks run `command` (each `{file}` placeholder is replaced with the fi
171
191
 
172
192
  ### Profiles
173
193
 
174
- - `plain`: no route or cross-layer globs; impact analysis reports only Python references.
194
+ - `generic`: no route or cross-layer globs; impact analysis reports only Python references.
195
+ - `python`: same impact globs as `generic`; guard checks append `ruff` and `ruff-format`.
175
196
  - `django`: route files `**/urls.py`, templates `**/templates/**/*.html`, frontend `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `ruff` and `ruff-format`.
176
197
  - `fastapi`: routes extracted from `**/*.py`, same frontend globs; guard checks append `ruff` and `ruff-format`.
198
+ - `frontend`: template globs `**/*.html` and frontend globs `**/src/**/*.ts` and `**/app/**/*.js`; guard checks append `eslint` and `prettier`.
177
199
 
178
200
  ### Blocking behavior
179
201
 
@@ -267,7 +289,7 @@ All checks are incremental: file-scoped checks run on files in the change-set, a
267
289
 
268
290
  ## Requirements
269
291
 
270
- - Python 3.6 or newer.
292
+ - Python 3.8 or newer.
271
293
  - Standard library only at runtime; no third-party dependencies.
272
294
  - The `git` CLI on `PATH`.
273
295
  - Optional: `ruff`, `eslint`, and `prettier` for their respective checks (missing tools degrade to warnings).
@@ -7,13 +7,13 @@ with io.open("README.md", encoding="utf-8") as fh:
7
7
 
8
8
  setup(
9
9
  name="diffimpactscout",
10
- version="0.1.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",
14
14
  license="MIT",
15
15
  author="DiffImpactScout contributors",
16
- python_requires=">=3.6",
16
+ python_requires=">=3.8",
17
17
  packages=find_packages("src"),
18
18
  package_dir={"": "src"},
19
19
  package_data={"diffimpactscout": ["profiles/*.json"]},
@@ -1,3 +1,3 @@
1
1
  """diffimpactscout: incremental pre-push guard and AST blast-radius impact analyzer."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
@@ -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 = 10
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 10) most-recent ones by committer date, unlike the
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
- 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)
@@ -18,6 +18,21 @@ from diffimpactscout.checks.base import (
18
18
  _HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
19
19
 
20
20
 
21
+ def _split_hunks(text):
22
+ hunks = []
23
+ current = []
24
+ for line in text.split("\n"):
25
+ if line.startswith("@@"):
26
+ if current:
27
+ hunks.append("\n".join(current))
28
+ current = [line]
29
+ else:
30
+ current.append(line)
31
+ if current:
32
+ hunks.append("\n".join(current))
33
+ return hunks
34
+
35
+
21
36
  def _truncate(text, limit=30):
22
37
  lines = text.split("\n")
23
38
  if len(lines) <= limit:
@@ -55,7 +70,7 @@ def _filter_diff_by_lines(diff, changed):
55
70
  i += 1
56
71
  out = list(header)
57
72
  body = "\n".join(lines[i:])
58
- for hunk in re.split(r"(?=^@@)", body, flags=re.MULTILINE):
73
+ for hunk in _split_hunks(body):
59
74
  if not hunk.startswith("@@"):
60
75
  continue
61
76
  m = _HUNK_RE.match(hunk)
@@ -142,8 +157,8 @@ class RuffCheck(Check):
142
157
  issues.append(
143
158
  CheckIssue(
144
159
  path,
145
- row or 0,
146
- location.get("column") or 0,
160
+ row,
161
+ location.get("column"),
147
162
  violation.get("code") or self.id,
148
163
  violation.get("message") or "",
149
164
  )
@@ -200,8 +215,8 @@ class RuffFormatCheck(Check):
200
215
  issues.append(
201
216
  CheckIssue(
202
217
  path,
203
- 0,
204
- 0,
218
+ None,
219
+ None,
205
220
  self.id,
206
221
  "formatting issue in changed lines:\n%s\nFix with: ruff format %s"
207
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", 0) or 0,
42
- getattr(exc, "colno", 0) or 0,
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 or 0,
73
- exc.offset or 0,
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, 0, 0, self.id, "invalid python source: %s" % exc
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, 0, self.id, "conflict marker found")
118
+ CheckIssue(path, lineno, None, self.id, "conflict marker found")
115
119
  )
116
120
  return CheckResult(issues=issues)
117
121