7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
233 lines
6.5 KiB
Python
233 lines
6.5 KiB
Python
"""Shared utility module for pre-commit hooks analyzing staged files."""
|
|
|
|
import subprocess
|
|
from dataclasses import dataclass, field
|
|
from pathlib import PurePosixPath
|
|
|
|
|
|
@dataclass
|
|
class StagedFileInfo:
|
|
path: str
|
|
added: int
|
|
removed: int
|
|
is_new: bool
|
|
category: str
|
|
|
|
|
|
@dataclass
|
|
class CommitAnalysis:
|
|
source_files: list[StagedFileInfo] = field(default_factory=list)
|
|
test_files: list[StagedFileInfo] = field(default_factory=list)
|
|
doc_files: list[StagedFileInfo] = field(default_factory=list)
|
|
|
|
@property
|
|
def new_source_files(self):
|
|
return [f for f in self.source_files if f.is_new]
|
|
|
|
@property
|
|
def total_source_added(self):
|
|
return sum(f.added for f in self.source_files)
|
|
|
|
@property
|
|
def has_tests(self):
|
|
return len(self.test_files) > 0
|
|
|
|
@property
|
|
def has_docs(self):
|
|
return len(self.doc_files) > 0
|
|
|
|
|
|
# Directories/patterns excluded from "source" classification
|
|
_EXCLUDED_DIRS = {
|
|
"migrations",
|
|
"alembic",
|
|
"config",
|
|
"settings",
|
|
"scripts",
|
|
".pre-commit-hooks",
|
|
"examples",
|
|
"docs",
|
|
}
|
|
|
|
_EXCLUDED_BASENAMES = {"__init__.py", "conftest.py"}
|
|
|
|
|
|
def _run_git(args):
|
|
"""Run a git command and return stdout lines.
|
|
|
|
Returns empty list if git command fails (e.g., not in a git repo).
|
|
"""
|
|
result = subprocess.run(
|
|
["git"] + args,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
return []
|
|
return result.stdout.strip().splitlines() if result.stdout.strip() else []
|
|
|
|
|
|
def _parse_numstat(lines):
|
|
"""Parse git diff --numstat output into {filepath: (added, removed)}."""
|
|
stats = {}
|
|
for line in lines:
|
|
parts = line.split("\t")
|
|
if len(parts) != 3:
|
|
continue
|
|
added_str, removed_str, filepath = parts
|
|
# Binary files show '-' for counts
|
|
added = int(added_str) if added_str != "-" else 0
|
|
removed = int(removed_str) if removed_str != "-" else 0
|
|
stats[filepath] = (added, removed)
|
|
return stats
|
|
|
|
|
|
def classify_file(path):
|
|
"""Classify a file path into a category string.
|
|
|
|
Returns one of: source, test, doc, init, conftest, migration, config,
|
|
hook, script, other
|
|
"""
|
|
p = PurePosixPath(path)
|
|
basename = p.name
|
|
parts_set = set(p.parts)
|
|
|
|
# Doc files
|
|
if p.suffix == ".md":
|
|
return "doc"
|
|
|
|
# JavaScript files
|
|
if p.suffix == ".js":
|
|
# Vitest test files: tests/js/**/*.test.js
|
|
if "tests" in parts_set and basename.endswith(".test.js"):
|
|
return "test"
|
|
# Source: under src/.../static/js/
|
|
if "static" in parts_set and "js" in parts_set and parts_set & {"src"}:
|
|
# Skip vendored / third-party
|
|
if "vendor" in parts_set or "lib" in parts_set:
|
|
return "other"
|
|
return "source"
|
|
return "other"
|
|
|
|
# Non-Python files are "other"
|
|
if p.suffix != ".py":
|
|
return "other"
|
|
|
|
# Specific basename exclusions
|
|
if basename == "__init__.py":
|
|
return "init"
|
|
if basename == "conftest.py":
|
|
return "conftest"
|
|
|
|
# Test files: under tests/ or matching test_*.py / *_test.py
|
|
if (
|
|
"tests" in parts_set
|
|
or basename.startswith("test_")
|
|
or basename.endswith("_test.py")
|
|
):
|
|
return "test"
|
|
|
|
# Check excluded directory patterns
|
|
for part in p.parts:
|
|
if part.lower() in _EXCLUDED_DIRS:
|
|
return _dir_to_category(part.lower())
|
|
|
|
# Source files: under src/
|
|
if len(p.parts) > 0 and p.parts[0] == "src":
|
|
return "source"
|
|
|
|
return "other"
|
|
|
|
|
|
def _dir_to_category(dirname):
|
|
"""Map excluded directory names to categories."""
|
|
mapping = {
|
|
"migrations": "migration",
|
|
"alembic": "migration",
|
|
"config": "config",
|
|
"settings": "config",
|
|
"scripts": "script",
|
|
".pre-commit-hooks": "hook",
|
|
"examples": "other",
|
|
"docs": "doc",
|
|
}
|
|
return mapping.get(dirname, "other")
|
|
|
|
|
|
def suggest_test_path(source_path):
|
|
"""Suggest a test file path for a given source file.
|
|
|
|
Python: src/local_deep_research/web/api.py -> tests/web/test_api.py
|
|
JS: src/local_deep_research/web/static/js/components/foo.js
|
|
-> tests/js/components/foo.test.js
|
|
"""
|
|
p = PurePosixPath(source_path)
|
|
|
|
# JavaScript: mirror tests/js/<subpath under static/js/>
|
|
if p.suffix == ".js":
|
|
parts = list(p.parts)
|
|
if "static" in parts and "js" in parts:
|
|
js_idx = parts.index("js", parts.index("static"))
|
|
sub_parts = parts[js_idx + 1 :]
|
|
if sub_parts:
|
|
stem = PurePosixPath(sub_parts[-1]).stem
|
|
# Convert snake_case → kebab-case to match existing test naming
|
|
stem = stem.replace("_", "-")
|
|
test_name = f"{stem}.test.js"
|
|
test_parts = ["tests", "js"] + sub_parts[:-1] + [test_name]
|
|
return str(PurePosixPath(*test_parts))
|
|
return f"tests/js/{p.stem.replace('_', '-')}.test.js"
|
|
|
|
parts = list(p.parts)
|
|
|
|
# Strip leading src/ and package name (e.g., src/local_deep_research/)
|
|
if parts and parts[0] == "src":
|
|
parts = parts[1:]
|
|
if parts and not parts[0].startswith("test"):
|
|
parts = parts[1:] # Remove package name like local_deep_research
|
|
|
|
# Build test path
|
|
if parts:
|
|
test_name = f"test_{parts[-1]}"
|
|
test_parts = ["tests"] + parts[:-1] + [test_name]
|
|
else:
|
|
test_parts = ["tests", f"test_{p.name}"]
|
|
|
|
return str(PurePosixPath(*test_parts))
|
|
|
|
|
|
def analyze_commit():
|
|
"""Analyze staged files and return a CommitAnalysis."""
|
|
# Get list of staged files with their status
|
|
status_lines = _run_git(["diff", "--cached", "--name-status"])
|
|
new_files = set()
|
|
for line in status_lines:
|
|
parts = line.split("\t")
|
|
if len(parts) >= 2 and parts[0].startswith("A"):
|
|
new_files.add(parts[-1])
|
|
|
|
# Get line counts
|
|
numstat_lines = _run_git(["diff", "--cached", "--numstat"])
|
|
stats = _parse_numstat(numstat_lines)
|
|
|
|
analysis = CommitAnalysis()
|
|
|
|
for filepath, (added, removed) in stats.items():
|
|
category = classify_file(filepath)
|
|
info = StagedFileInfo(
|
|
path=filepath,
|
|
added=added,
|
|
removed=removed,
|
|
is_new=filepath in new_files,
|
|
category=category,
|
|
)
|
|
|
|
if category == "source":
|
|
analysis.source_files.append(info)
|
|
elif category == "test":
|
|
analysis.test_files.append(info)
|
|
elif category == "doc":
|
|
analysis.doc_files.append(info)
|
|
|
|
return analysis
|