Skip to content

Commit 694fd9d

Browse files
committed
fix: use git ls-files for file discovery, fix layer violations and untracked files (PR #1232)
- Use git ls-files instead of rglob to avoid scanning node_modules/dist - Fix get_layer() to return None for unknown layers and skip violations - Include untracked .ts/.tsx files in pre-commit check trigger
1 parent 5cb0506 commit 694fd9d

4 files changed

Lines changed: 142 additions & 40 deletions

File tree

.github/hooks/scripts/stop_hook.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ def run_command(cmd: List[str], cwd: Optional[Path] = None) -> Tuple[int, str]:
3232

3333

3434
def has_uncommitted_changes(repo_root: Path) -> bool:
35-
"""Check if there are uncommitted changes."""
35+
"""Check if there are uncommitted changes (tracked files only)."""
3636
code, output = run_command(["git", "status", "--porcelain"], repo_root)
3737
if code == 0 and output:
38-
# Ignore all untracked files (marked with ??) - only track staged/modified
38+
# Filter to only tracked files (staged/modified, not untracked with ??)
3939
lines = [
4040
line
4141
for line in output.split("\n")
@@ -45,6 +45,19 @@ def has_uncommitted_changes(repo_root: Path) -> bool:
4545
return False
4646

4747

48+
def has_untracked_ts_files(repo_root: Path) -> bool:
49+
"""Check if there are untracked TypeScript files."""
50+
code, output = run_command(["git", "status", "--porcelain"], repo_root)
51+
if code == 0 and output:
52+
for line in output.split("\n"):
53+
if line.strip().startswith("??"):
54+
# Extract filename from untracked entry (format: "?? path/to/file")
55+
filename = line.strip()[3:].strip()
56+
if filename.endswith((".ts", ".tsx")):
57+
return True
58+
return False
59+
60+
4861
def has_staged_changes(repo_root: Path) -> bool:
4962
"""Check if there are staged but uncommitted changes."""
5063
code, output = run_command(["git", "diff", "--cached", "--name-only"], repo_root)
@@ -88,8 +101,11 @@ def main() -> int:
88101
print(json.dumps({}))
89102
return 0
90103

91-
# Check for uncommitted TypeScript changes
92-
if has_uncommitted_changes(repo_root) and check_ts_files_changed(repo_root):
104+
# Check for uncommitted TypeScript changes (including new untracked TS files)
105+
ts_work_present = (
106+
has_uncommitted_changes(repo_root) and check_ts_files_changed(repo_root)
107+
) or has_untracked_ts_files(repo_root)
108+
if ts_work_present:
93109
# There are uncommitted TS changes - remind about pre-commit checks
94110
response = {
95111
"hookSpecificOutput": {

analysis/complexity_analysis.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pathlib
77
import re
8+
import subprocess
89
from dataclasses import dataclass
910
from typing import Dict, List, Optional
1011

@@ -108,6 +109,40 @@ def should_analyze_file(
108109
return True
109110

110111

112+
def get_tracked_source_files(
113+
repo_root: pathlib.Path, extensions: List[str]
114+
) -> List[pathlib.Path]:
115+
"""Get source files tracked by git (respects .gitignore automatically)."""
116+
try:
117+
result = subprocess.run(
118+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
119+
cwd=repo_root,
120+
capture_output=True,
121+
text=True,
122+
timeout=30,
123+
)
124+
if result.returncode != 0:
125+
return []
126+
127+
files = []
128+
for line in result.stdout.splitlines():
129+
line = line.strip()
130+
if line and any(line.endswith(ext) for ext in extensions):
131+
filepath = repo_root / line
132+
if filepath.exists():
133+
files.append(filepath)
134+
return files
135+
except (subprocess.TimeoutExpired, FileNotFoundError):
136+
# Fall back to rglob if git is not available
137+
gitignore = load_gitignore(repo_root)
138+
files = []
139+
for ext in extensions:
140+
for filepath in repo_root.rglob(f"*{ext}"):
141+
if should_analyze_file(filepath, repo_root, gitignore):
142+
files.append(filepath)
143+
return files
144+
145+
111146
def analyze_python_file(
112147
filepath: pathlib.Path, repo_root: pathlib.Path
113148
) -> Optional[FileComplexity]:
@@ -249,16 +284,8 @@ def analyze_typescript_file(
249284
def find_source_files(
250285
repo_root: pathlib.Path, extensions: List[str]
251286
) -> List[pathlib.Path]:
252-
"""Find all source files with given extensions."""
253-
gitignore = load_gitignore(repo_root)
254-
files = []
255-
256-
for ext in extensions:
257-
for filepath in repo_root.rglob(f"*{ext}"):
258-
if should_analyze_file(filepath, repo_root, gitignore):
259-
files.append(filepath)
260-
261-
return files
287+
"""Find all source files with given extensions using git ls-files."""
288+
return get_tracked_source_files(repo_root, extensions)
262289

263290

264291
def analyze_complexity(repo_root: pathlib.Path) -> dict:

analysis/debt_indicators.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pathlib
1111
import re
12+
import subprocess
1213
from dataclasses import dataclass
1314
from typing import Dict, List, Optional
1415

@@ -128,6 +129,40 @@ def should_analyze_file(
128129
return True
129130

130131

132+
def get_tracked_source_files(
133+
repo_root: pathlib.Path, extensions: List[str]
134+
) -> List[pathlib.Path]:
135+
"""Get source files tracked by git (respects .gitignore automatically)."""
136+
try:
137+
result = subprocess.run(
138+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
139+
cwd=repo_root,
140+
capture_output=True,
141+
text=True,
142+
timeout=30,
143+
)
144+
if result.returncode != 0:
145+
return []
146+
147+
files = []
148+
for line in result.stdout.splitlines():
149+
line = line.strip()
150+
if line and any(line.endswith(ext) for ext in extensions):
151+
filepath = repo_root / line
152+
if filepath.exists():
153+
files.append(filepath)
154+
return files
155+
except (subprocess.TimeoutExpired, FileNotFoundError):
156+
# Fall back to rglob if git is not available
157+
gitignore = load_gitignore(repo_root)
158+
files = []
159+
for ext in extensions:
160+
for filepath in repo_root.rglob(f"*{ext}"):
161+
if should_analyze_file(filepath, repo_root, gitignore):
162+
files.append(filepath)
163+
return files
164+
165+
131166
def find_debt_markers(
132167
filepath: pathlib.Path, repo_root: pathlib.Path
133168
) -> List[DebtMarker]:
@@ -341,20 +376,13 @@ def analyze_debt(repo_root: pathlib.Path) -> dict:
341376
Returns:
342377
Dictionary with all debt indicators
343378
"""
344-
gitignore = load_gitignore(repo_root)
345-
346379
all_markers: List[DebtMarker] = []
347380
large_files: List[LargeFile] = []
348381
long_functions: List[LongFunction] = []
349382

350-
# Find all source files
383+
# Find all source files using git ls-files (respects .gitignore)
351384
extensions = [".py", ".ts", ".js", ".tsx", ".jsx"]
352-
source_files = []
353-
354-
for ext in extensions:
355-
for filepath in repo_root.rglob(f"*{ext}"):
356-
if should_analyze_file(filepath, repo_root, gitignore):
357-
source_files.append(filepath)
385+
source_files = get_tracked_source_files(repo_root, extensions)
358386

359387
# Analyze each file
360388
for filepath in source_files:

analysis/dependency_analysis.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import pathlib
1414
import re
15+
import subprocess
1516
from dataclasses import dataclass, field
1617
from typing import Dict, List, Optional, Set
1718

@@ -106,6 +107,40 @@ def should_analyze_file(
106107
return True
107108

108109

110+
def get_tracked_source_files(
111+
repo_root: pathlib.Path, extensions: List[str]
112+
) -> List[pathlib.Path]:
113+
"""Get source files tracked by git (respects .gitignore automatically)."""
114+
try:
115+
result = subprocess.run(
116+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
117+
cwd=repo_root,
118+
capture_output=True,
119+
text=True,
120+
timeout=30,
121+
)
122+
if result.returncode != 0:
123+
return []
124+
125+
files = []
126+
for line in result.stdout.splitlines():
127+
line = line.strip()
128+
if line and any(line.endswith(ext) for ext in extensions):
129+
filepath = repo_root / line
130+
if filepath.exists():
131+
files.append(filepath)
132+
return files
133+
except (subprocess.TimeoutExpired, FileNotFoundError):
134+
# Fall back to rglob if git is not available
135+
gitignore = load_gitignore(repo_root)
136+
files = []
137+
for ext in extensions:
138+
for filepath in repo_root.rglob(f"*{ext}"):
139+
if should_analyze_file(filepath, repo_root, gitignore):
140+
files.append(filepath)
141+
return files
142+
143+
109144
def extract_imports_typescript(
110145
filepath: pathlib.Path, repo_root: pathlib.Path
111146
) -> Set[str]:
@@ -178,17 +213,11 @@ def resolve_import_path(
178213

179214
def build_dependency_graph(repo_root: pathlib.Path) -> Dict[str, ModuleInfo]:
180215
"""Build a dependency graph of all TypeScript modules."""
181-
gitignore = load_gitignore(repo_root)
182216
modules: Dict[str, ModuleInfo] = {}
183217

184-
# Find all TypeScript/JavaScript files
218+
# Find all TypeScript/JavaScript files using git ls-files (respects .gitignore)
185219
extensions = [".ts", ".tsx", ".js", ".jsx"]
186-
source_files = []
187-
188-
for ext in extensions:
189-
for filepath in repo_root.rglob(f"*{ext}"):
190-
if should_analyze_file(filepath, repo_root, gitignore):
191-
source_files.append(filepath)
220+
source_files = get_tracked_source_files(repo_root, extensions)
192221

193222
# First pass: extract imports
194223
for filepath in source_files:
@@ -303,33 +332,35 @@ def compute_layer_violations(modules: Dict[str, ModuleInfo]) -> List[dict]:
303332
"managers",
304333
]
305334

306-
def get_layer(path: str) -> int:
307-
"""Get the layer index for a module path."""
335+
def get_layer(path: str) -> Optional[int]:
336+
"""Get the layer index for a module path. Returns None for unknown layers."""
308337
parts = path.lower().split("/")
309338
for i, layer in enumerate(layer_order):
310339
if layer in parts:
311340
return i
312-
return len(layer_order) # Unknown = highest layer
341+
return None # Unknown layer
313342

314343
violations = []
315344
for module_path, module_info in modules.items():
316345
module_layer = get_layer(module_path)
346+
# Skip modules with unknown layers
347+
if module_layer is None:
348+
continue
317349

318350
for imported_path in module_info.imports:
319351
imported_layer = get_layer(imported_path)
352+
# Skip imports with unknown layers
353+
if imported_layer is None:
354+
continue
320355

321356
# Lower layer importing from higher layer is a violation
322357
if module_layer < imported_layer:
323358
violations.append(
324359
{
325360
"from": module_path,
326-
"from_layer": layer_order[module_layer]
327-
if module_layer < len(layer_order)
328-
else "unknown",
361+
"from_layer": layer_order[module_layer],
329362
"imports": imported_path,
330-
"imports_layer": layer_order[imported_layer]
331-
if imported_layer < len(layer_order)
332-
else "unknown",
363+
"imports_layer": layer_order[imported_layer],
333364
}
334365
)
335366

0 commit comments

Comments
 (0)