Files
learningcircuit--local-deep…/.pre-commit-hooks/_commit_analysis.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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