ghostbuster-cli 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.
- ghostbuster_cli-0.4.0/.github/FUNDING.yml +4 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/.github/workflows/publish.yml +2 -3
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/CHANGELOG.md +10 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/PKG-INFO +20 -1
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/README.md +19 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/pyproject.toml +1 -1
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/__init__.py +1 -1
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/cli/scan.py +28 -1
- ghostbuster_cli-0.4.0/src/ghostbuster/core/git_diff.py +80 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/scanner.py +15 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_cli.py +1 -1
- ghostbuster_cli-0.4.0/tests/test_git_diff.py +114 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/.gitignore +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/.pre-commit-hooks.yaml +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/CONTRIBUTING.md +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/LICENSE +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/assets/npm_package_icon.png +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/assets/readme_header.png +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/assets/terminal_icon.png +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/__main__.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/cli/__init__.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/cli/app.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/cli/bust.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/cli/display.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/config.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/__init__.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/dead_imports.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/models.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/orphan_files.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/phantom_env.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/scoring.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/core/zombie_code.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/fixers/__init__.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/fixers/env_fixer.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/fixers/gitignore_fixer.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/src/ghostbuster/fixers/import_fixer.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/__init__.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/conftest.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_dead_imports.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_fixers.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_node_scanner.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_orphan_files.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_phantom_env.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_scoring.py +0 -0
- {ghostbuster_cli-0.3.0 → ghostbuster_cli-0.4.0}/tests/test_zombie_code.py +0 -0
|
@@ -3,9 +3,6 @@ name: Publish to PyPI
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
-
push:
|
|
7
|
-
tags:
|
|
8
|
-
- "v*"
|
|
9
6
|
|
|
10
7
|
jobs:
|
|
11
8
|
pypi-publish:
|
|
@@ -33,3 +30,5 @@ jobs:
|
|
|
33
30
|
|
|
34
31
|
- name: Publish package distributions to PyPI
|
|
35
32
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
33
|
+
with:
|
|
34
|
+
skip-existing: true
|
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Git Diff / Incremental Fast Scan Mode (`ghostbuster scan --diff` / `-d`)**:
|
|
15
|
+
- Scan only modified, staged, and untracked files instead of the entire project tree.
|
|
16
|
+
- Added `--diff-base` flag to compare working branch against a target ref (e.g. `ghostbuster scan --diff-base origin/main`).
|
|
17
|
+
- Ideal for monorepos and GitHub Actions PR validation.
|
|
18
|
+
- Zero false positives: global reference verification remains intact for zombie code.
|
|
19
|
+
|
|
10
20
|
## [0.3.0] - 2026-08-29
|
|
11
21
|
|
|
12
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ghostbuster-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adewanggar/ghostbuster-cli
|
|
6
6
|
Project-URL: Documentation, https://github.com/adewanggar/ghostbuster-cli#readme
|
|
@@ -128,6 +128,18 @@ ghostbuster scan ./my-project
|
|
|
128
128
|
ghostbuster scan -v
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
### Incremental / Git Diff Scan
|
|
132
|
+
|
|
133
|
+
Scan only files that were modified, staged, or untracked in your working tree:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Scan modified files only (fast mode)
|
|
137
|
+
ghostbuster scan --diff
|
|
138
|
+
|
|
139
|
+
# Compare working branch against main/master
|
|
140
|
+
ghostbuster scan --diff-base origin/main
|
|
141
|
+
```
|
|
142
|
+
|
|
131
143
|
### Filtered Scan
|
|
132
144
|
|
|
133
145
|
```bash
|
|
@@ -243,6 +255,13 @@ pip install pytest ruff mypy
|
|
|
243
255
|
pytest tests/ -v
|
|
244
256
|
```
|
|
245
257
|
|
|
258
|
+
## Support & Sponsor
|
|
259
|
+
|
|
260
|
+
If Ghostbuster helped clean up your codebase or saved CI minutes, consider supporting the project:
|
|
261
|
+
|
|
262
|
+
[](https://ko-fi.com/adewanggar)
|
|
263
|
+
[](https://saweria.co/adewanggar)
|
|
264
|
+
|
|
246
265
|
## License
|
|
247
266
|
|
|
248
267
|
MIT (c) Ghostbuster Contributors
|
|
@@ -90,6 +90,18 @@ ghostbuster scan ./my-project
|
|
|
90
90
|
ghostbuster scan -v
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Incremental / Git Diff Scan
|
|
94
|
+
|
|
95
|
+
Scan only files that were modified, staged, or untracked in your working tree:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Scan modified files only (fast mode)
|
|
99
|
+
ghostbuster scan --diff
|
|
100
|
+
|
|
101
|
+
# Compare working branch against main/master
|
|
102
|
+
ghostbuster scan --diff-base origin/main
|
|
103
|
+
```
|
|
104
|
+
|
|
93
105
|
### Filtered Scan
|
|
94
106
|
|
|
95
107
|
```bash
|
|
@@ -205,6 +217,13 @@ pip install pytest ruff mypy
|
|
|
205
217
|
pytest tests/ -v
|
|
206
218
|
```
|
|
207
219
|
|
|
220
|
+
## Support & Sponsor
|
|
221
|
+
|
|
222
|
+
If Ghostbuster helped clean up your codebase or saved CI minutes, consider supporting the project:
|
|
223
|
+
|
|
224
|
+
[](https://ko-fi.com/adewanggar)
|
|
225
|
+
[](https://saweria.co/adewanggar)
|
|
226
|
+
|
|
208
227
|
## License
|
|
209
228
|
|
|
210
229
|
MIT (c) Ghostbuster Contributors
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ghostbuster-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -23,6 +23,7 @@ from ghostbuster.cli.display import (
|
|
|
23
23
|
print_scan_result_markdown,
|
|
24
24
|
print_scanning_start,
|
|
25
25
|
)
|
|
26
|
+
from ghostbuster.core.git_diff import get_changed_files
|
|
26
27
|
from ghostbuster.core.models import GhostCategory
|
|
27
28
|
from ghostbuster.core.scanner import create_default_orchestrator
|
|
28
29
|
|
|
@@ -55,6 +56,17 @@ def scan(
|
|
|
55
56
|
"-c",
|
|
56
57
|
help="Only scan specific categories (dead-import, orphan-file, zombie-code, phantom-env).",
|
|
57
58
|
),
|
|
59
|
+
diff: bool = typer.Option(
|
|
60
|
+
False,
|
|
61
|
+
"--diff",
|
|
62
|
+
"-d",
|
|
63
|
+
help="Incremental fast scan: only scan modified, staged, and untracked files.",
|
|
64
|
+
),
|
|
65
|
+
diff_base: str | None = typer.Option(
|
|
66
|
+
None,
|
|
67
|
+
"--diff-base",
|
|
68
|
+
help="Compare changed files against a specific git ref/branch (e.g. main, origin/main, HEAD~1).",
|
|
69
|
+
),
|
|
58
70
|
verbose: bool = typer.Option(
|
|
59
71
|
False,
|
|
60
72
|
"--verbose",
|
|
@@ -74,6 +86,8 @@ def scan(
|
|
|
74
86
|
|
|
75
87
|
Examples:
|
|
76
88
|
ghostbuster scan
|
|
89
|
+
ghostbuster scan --diff
|
|
90
|
+
ghostbuster scan --diff-base origin/main
|
|
77
91
|
ghostbuster scan ./my-project --verbose
|
|
78
92
|
ghostbuster scan --format json --category dead-import
|
|
79
93
|
"""
|
|
@@ -91,10 +105,19 @@ def scan(
|
|
|
91
105
|
)
|
|
92
106
|
raise typer.Exit(1)
|
|
93
107
|
|
|
108
|
+
# Handle diff mode
|
|
109
|
+
changed_files: set[Path] | None = None
|
|
110
|
+
if diff or diff_base:
|
|
111
|
+
changed_files = get_changed_files(path, base_ref=diff_base)
|
|
112
|
+
|
|
94
113
|
# Show banner for rich format
|
|
95
114
|
if format == OutputFormat.rich:
|
|
96
115
|
print_banner()
|
|
97
116
|
print_scanning_start(str(path), _get_scanner_names(category))
|
|
117
|
+
if diff or diff_base:
|
|
118
|
+
console.print(
|
|
119
|
+
f" [cyan]Diff mode active ({len(changed_files or set())} changed files analyzed)[/cyan]\n"
|
|
120
|
+
)
|
|
98
121
|
|
|
99
122
|
# Run the scan
|
|
100
123
|
try:
|
|
@@ -105,7 +128,11 @@ def scan(
|
|
|
105
128
|
if format == OutputFormat.rich
|
|
106
129
|
else _nullcontext()
|
|
107
130
|
):
|
|
108
|
-
result = orchestrator.run(
|
|
131
|
+
result = orchestrator.run(
|
|
132
|
+
path,
|
|
133
|
+
categories=category,
|
|
134
|
+
changed_files=changed_files if (diff or diff_base) else None,
|
|
135
|
+
)
|
|
109
136
|
|
|
110
137
|
except Exception as exc:
|
|
111
138
|
print_error(f"Scan failed: {exc}", "Run with --debug flag for full traceback.")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Git Diff helper - detects changed and untracked files in git repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_changed_files(path: Path, base_ref: str | None = None) -> set[Path]:
|
|
10
|
+
"""Get the set of changed, added, and untracked files in a git repository.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
path: Root path of the project.
|
|
14
|
+
base_ref: Optional git reference/branch to compare against (e.g. 'main', 'origin/main', 'HEAD~1').
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A set of absolute Paths for all modified/added/untracked files.
|
|
18
|
+
"""
|
|
19
|
+
changed: set[Path] = set()
|
|
20
|
+
|
|
21
|
+
# Verify directory is a git repository
|
|
22
|
+
try:
|
|
23
|
+
check_git = subprocess.run(
|
|
24
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
25
|
+
cwd=path,
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
check=False,
|
|
29
|
+
)
|
|
30
|
+
if check_git.returncode != 0:
|
|
31
|
+
return changed
|
|
32
|
+
except (OSError, FileNotFoundError):
|
|
33
|
+
return changed
|
|
34
|
+
|
|
35
|
+
commands: list[list[str]] = []
|
|
36
|
+
|
|
37
|
+
if base_ref:
|
|
38
|
+
# Compare current working state or branch against base_ref
|
|
39
|
+
commands.append(["git", "diff", "--name-only", "--diff-filter=ACMR", base_ref])
|
|
40
|
+
else:
|
|
41
|
+
# Unstaged changes in working tree
|
|
42
|
+
commands.append(["git", "diff", "--name-only", "--diff-filter=ACMR"])
|
|
43
|
+
# Staged changes in index
|
|
44
|
+
commands.append(["git", "diff", "--name-only", "--cached", "--diff-filter=ACMR"])
|
|
45
|
+
|
|
46
|
+
# Also capture untracked files from git status
|
|
47
|
+
commands.append(["git", "status", "--porcelain"])
|
|
48
|
+
|
|
49
|
+
for cmd in commands:
|
|
50
|
+
try:
|
|
51
|
+
res = subprocess.run(
|
|
52
|
+
cmd,
|
|
53
|
+
cwd=path,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
if res.returncode == 0:
|
|
59
|
+
for line in res.stdout.splitlines():
|
|
60
|
+
line = line.strip()
|
|
61
|
+
if not line:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Parse git status --porcelain output: '?? path/to/file' or 'M path/to/file'
|
|
65
|
+
if cmd[1] == "status":
|
|
66
|
+
parts = line.split(maxsplit=1)
|
|
67
|
+
if len(parts) == 2:
|
|
68
|
+
rel_file = parts[1].strip('"')
|
|
69
|
+
file_path = (path / rel_file).resolve()
|
|
70
|
+
if file_path.exists():
|
|
71
|
+
changed.add(file_path)
|
|
72
|
+
else:
|
|
73
|
+
# Parse git diff --name-only output
|
|
74
|
+
file_path = (path / line).resolve()
|
|
75
|
+
if file_path.exists():
|
|
76
|
+
changed.add(file_path)
|
|
77
|
+
except OSError:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
return changed
|
|
@@ -52,6 +52,7 @@ class ScanOrchestrator:
|
|
|
52
52
|
self,
|
|
53
53
|
path: Path,
|
|
54
54
|
categories: list[str] | None = None,
|
|
55
|
+
changed_files: set[Path] | None = None,
|
|
55
56
|
) -> ScanResult:
|
|
56
57
|
"""Run all registered scanners (or filtered subset) on the given path.
|
|
57
58
|
|
|
@@ -59,6 +60,8 @@ class ScanOrchestrator:
|
|
|
59
60
|
path: Root directory to scan.
|
|
60
61
|
categories: Optional list of category names to filter scanners.
|
|
61
62
|
If None, all scanners are run.
|
|
63
|
+
changed_files: Optional set of changed file Paths from git diff.
|
|
64
|
+
If provided, results will be filtered to these files.
|
|
62
65
|
|
|
63
66
|
Returns:
|
|
64
67
|
Aggregated ScanResult with all ghosts found and a score.
|
|
@@ -86,6 +89,18 @@ class ScanOrchestrator:
|
|
|
86
89
|
)
|
|
87
90
|
)
|
|
88
91
|
|
|
92
|
+
# In diff mode, filter ghosts to only those originating in changed files
|
|
93
|
+
if changed_files is not None:
|
|
94
|
+
resolved_changed = {p.resolve() for p in changed_files}
|
|
95
|
+
filtered_ghosts: list[Ghost] = []
|
|
96
|
+
for g in all_ghosts:
|
|
97
|
+
if g.file_path and g.file_path.resolve() in resolved_changed:
|
|
98
|
+
filtered_ghosts.append(g)
|
|
99
|
+
elif not g.file_path:
|
|
100
|
+
# Keep findings without a specific file path (general warnings)
|
|
101
|
+
filtered_ghosts.append(g)
|
|
102
|
+
all_ghosts = filtered_ghosts
|
|
103
|
+
|
|
89
104
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
90
105
|
score = calculate_score(all_ghosts)
|
|
91
106
|
|
|
@@ -17,7 +17,7 @@ class TestCLI:
|
|
|
17
17
|
result = runner.invoke(app, ["--version"])
|
|
18
18
|
assert result.exit_code == 0
|
|
19
19
|
assert "ghostbuster" in result.output
|
|
20
|
-
assert "0.
|
|
20
|
+
assert "0.4.0" in result.output
|
|
21
21
|
|
|
22
22
|
def test_help_flag(self) -> None:
|
|
23
23
|
"""--help should print help text."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Tests for Git Diff / Fast Scan mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from ghostbuster.cli.app import app
|
|
11
|
+
from ghostbuster.core.git_diff import get_changed_files
|
|
12
|
+
from ghostbuster.core.models import Ghost, GhostCategory, Severity
|
|
13
|
+
from ghostbuster.core.scanner import ScanOrchestrator
|
|
14
|
+
|
|
15
|
+
runner = CliRunner()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestGitDiffHelper:
|
|
19
|
+
"""Tests for get_changed_files."""
|
|
20
|
+
|
|
21
|
+
def test_non_git_directory_returns_empty_set(self, tmp_path: Path) -> None:
|
|
22
|
+
"""Should return empty set for a non-git directory."""
|
|
23
|
+
changed = get_changed_files(tmp_path)
|
|
24
|
+
assert len(changed) == 0
|
|
25
|
+
|
|
26
|
+
def test_detects_untracked_and_modified_files(self, tmp_path: Path) -> None:
|
|
27
|
+
"""Should detect untracked and modified files in a git repo."""
|
|
28
|
+
# Initialize git repo in tmp_path
|
|
29
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
30
|
+
subprocess.run(
|
|
31
|
+
["git", "config", "user.email", "test@example.com"],
|
|
32
|
+
cwd=tmp_path,
|
|
33
|
+
capture_output=True,
|
|
34
|
+
check=True,
|
|
35
|
+
)
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["git", "config", "user.name", "Test User"],
|
|
38
|
+
cwd=tmp_path,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Create committed file
|
|
44
|
+
file1 = tmp_path / "initial.py"
|
|
45
|
+
file1.write_text("x = 1\n", encoding="utf-8")
|
|
46
|
+
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
|
|
47
|
+
subprocess.run(
|
|
48
|
+
["git", "commit", "-m", "init"], cwd=tmp_path, capture_output=True, check=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Modify file1 and create untracked file2
|
|
52
|
+
file1.write_text("x = 2\n", encoding="utf-8")
|
|
53
|
+
file2 = tmp_path / "untracked.py"
|
|
54
|
+
file2.write_text("y = 3\n", encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
changed = get_changed_files(tmp_path)
|
|
57
|
+
assert file1.resolve() in changed
|
|
58
|
+
assert file2.resolve() in changed
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestScanOrchestratorDiffFilter:
|
|
62
|
+
"""Tests for ScanOrchestrator filtering with changed_files."""
|
|
63
|
+
|
|
64
|
+
def test_filters_ghosts_to_changed_files(self, tmp_path: Path) -> None:
|
|
65
|
+
"""Should only return ghosts originating in changed_files."""
|
|
66
|
+
file_a = tmp_path / "a.py"
|
|
67
|
+
file_b = tmp_path / "b.py"
|
|
68
|
+
file_a.write_text("pass\n", encoding="utf-8")
|
|
69
|
+
file_b.write_text("pass\n", encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
ghosts = [
|
|
72
|
+
Ghost(
|
|
73
|
+
category=GhostCategory.ZOMBIE_CODE,
|
|
74
|
+
name="unused_a",
|
|
75
|
+
message="unused in a",
|
|
76
|
+
file_path=file_a,
|
|
77
|
+
severity=Severity.LOW,
|
|
78
|
+
),
|
|
79
|
+
Ghost(
|
|
80
|
+
category=GhostCategory.ZOMBIE_CODE,
|
|
81
|
+
name="unused_b",
|
|
82
|
+
message="unused in b",
|
|
83
|
+
file_path=file_b,
|
|
84
|
+
severity=Severity.LOW,
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
class MockScanner:
|
|
89
|
+
name = "mock"
|
|
90
|
+
|
|
91
|
+
def scan(self, path: Path) -> list[Ghost]:
|
|
92
|
+
return ghosts
|
|
93
|
+
|
|
94
|
+
orchestrator = ScanOrchestrator()
|
|
95
|
+
orchestrator.register(MockScanner())
|
|
96
|
+
|
|
97
|
+
# Only file_a changed
|
|
98
|
+
result = orchestrator.run(tmp_path, changed_files={file_a})
|
|
99
|
+
assert len(result.ghosts) == 1
|
|
100
|
+
assert result.ghosts[0].name == "unused_a"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestScanCliDiffFlag:
|
|
104
|
+
"""Tests for CLI --diff flag."""
|
|
105
|
+
|
|
106
|
+
def test_cli_diff_option_help(self) -> None:
|
|
107
|
+
"""--help should include --diff and --diff-base."""
|
|
108
|
+
import re
|
|
109
|
+
|
|
110
|
+
result = runner.invoke(app, ["scan", "--help"])
|
|
111
|
+
assert result.exit_code == 0
|
|
112
|
+
clean_output = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", result.output)
|
|
113
|
+
assert "--diff" in clean_output
|
|
114
|
+
assert "--diff-base" in clean_output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|