feat(hooks): add git hooks management and auto-fix on pre-commit
This commit is contained in:
7 files changed
+201
-20
No files matched your search
@@ -1,5 +1,16 @@
|
||||
## 0.1.1
|
||||
|
||||
```
|
||||
## v0.1.1
|
||||
|
||||
✨ Added
|
||||
- Added git hooks management CLI
|
||||
- Added emoji option to CLI and config to control emoji prefixes
|
||||
|
||||
📝 Documentation
|
||||
- Recommended installation using pipx and uv
|
||||
```
|
||||
|
||||
## v0.1.1
|
||||
|
||||
✨ Added
|
||||
|
||||
@@ -378,6 +378,17 @@ Alternatively, pass `--pre-commit-template` and/or `--pre-push-template` to inst
|
||||
|
||||
Note: If a hook already exists, DevTools creates a timestamped backup in `.git/hooks/` with the `.bak` suffix before overwriting, e.g., `pre-commit.20250101123000.bak`.
|
||||
|
||||
Auto-fix behavior (pre-commit):
|
||||
|
||||
- If initial checks fail, DevTools attempts to auto-fix issues by running `scripts/format.py` and `scripts/lintfix.py` if those files exist in your repo.
|
||||
- Any changes made are automatically staged (`git add -A`), and checks are re-run. The commit will fail only if the re-run still fails.
|
||||
- You can run these scripts manually as well:
|
||||
|
||||
```bash
|
||||
python3 scripts/format.py
|
||||
python3 scripts/lintfix.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
@@ -38,8 +38,10 @@ class CommitGenGitService(GitService):
|
||||
|
||||
def commit_changes(self, message: str, sign: bool = False) -> None:
|
||||
"""Commit changes with the given message."""
|
||||
if not self.commit(message, sign=sign):
|
||||
raise Exception("Failed to commit changes")
|
||||
ok, stdout, stderr, code = self.commit_verbose(message, sign=sign)
|
||||
if not ok:
|
||||
details = stderr or stdout or f"exit code {code}"
|
||||
raise Exception(f"Failed to commit changes: {details}")
|
||||
|
||||
def push_changes(self) -> None:
|
||||
"""Push changes to the current branch."""
|
||||
|
||||
+37
-16
@@ -15,7 +15,7 @@ console = Console()
|
||||
|
||||
HOOK_TEMPLATES = {
|
||||
"pre-commit": """#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -u
|
||||
|
||||
echo "Running pre-commit checks..."
|
||||
|
||||
@@ -50,11 +50,11 @@ run_lint() {
|
||||
# Python
|
||||
if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then
|
||||
if command -v ruff >/dev/null 2>&1; then
|
||||
ruff .
|
||||
ruff . || return 1
|
||||
return 0
|
||||
elif command -v black >/dev/null 2>&1; then
|
||||
echo "ruff not found; running black --check instead"
|
||||
black --check .
|
||||
black --check . || return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -68,7 +68,7 @@ run_lint() {
|
||||
# Go
|
||||
if [ -f "go.mod" ]; then
|
||||
if command -v golangci-lint >/dev/null 2>&1; then
|
||||
golangci-lint run
|
||||
golangci-lint run || return 1
|
||||
return 0
|
||||
fi
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
@@ -76,9 +76,9 @@ run_lint() {
|
||||
if [ -n "$fmt_out" ]; then
|
||||
echo "Files not formatted (gofmt):"
|
||||
echo "$fmt_out"
|
||||
exit 1
|
||||
return 1
|
||||
fi
|
||||
go vet ./...
|
||||
go vet ./... || return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -102,15 +102,15 @@ run_tests() {
|
||||
# Node.js/TypeScript
|
||||
if [ -f "package.json" ]; then
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm test -s --if-present || true
|
||||
npm test -s --if-present || return 1
|
||||
return 0
|
||||
fi
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
pnpm -s test || true
|
||||
pnpm -s test || return 1
|
||||
return 0
|
||||
fi
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
yarn -s test || true
|
||||
yarn -s test || return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -118,31 +118,31 @@ run_tests() {
|
||||
# Python
|
||||
if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then
|
||||
if command -v pytest >/dev/null 2>&1; then
|
||||
pytest -q
|
||||
pytest -q || return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Rust
|
||||
if [ -f "Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
|
||||
cargo test -q
|
||||
cargo test -q || return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Go
|
||||
if [ -f "go.mod" ] && command -v go >/dev/null 2>&1; then
|
||||
go test ./...
|
||||
go test ./... || return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Java
|
||||
if [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
|
||||
if [ -x "./gradlew" ]; then
|
||||
./gradlew -q test || true
|
||||
./gradlew -q test || return 1
|
||||
return 0
|
||||
fi
|
||||
if command -v mvn >/dev/null 2>&1; then
|
||||
mvn -q test || true
|
||||
mvn -q test || return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -150,8 +150,29 @@ run_tests() {
|
||||
echo "No recognized test runner found; skipping tests"
|
||||
}
|
||||
|
||||
run_lint
|
||||
run_tests
|
||||
run_checks() {
|
||||
lint_failed=0
|
||||
test_failed=0
|
||||
run_lint || lint_failed=1
|
||||
run_tests || test_failed=1
|
||||
if [ $lint_failed -ne 0 ] || [ $test_failed -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Initial check; if it fails, try to auto-fix using local scripts if present
|
||||
if ! run_checks; then
|
||||
echo "Initial checks failed; attempting auto-fix via scripts/format.py and scripts/lintfix.py if present"
|
||||
if [ -f "scripts/format.py" ]; then
|
||||
if command -v python3 >/dev/null 2>&1; then python3 scripts/format.py || true; else python scripts/format.py || true; fi
|
||||
fi
|
||||
if [ -f "scripts/lintfix.py" ]; then
|
||||
if command -v python3 >/dev/null 2>&1; then python3 scripts/lintfix.py || true; else python scripts/lintfix.py || true; fi
|
||||
fi
|
||||
if command -v git >/dev/null 2>&1; then git add -A; fi
|
||||
run_checks
|
||||
fi
|
||||
|
||||
""",
|
||||
"pre-push": """#!/usr/bin/env bash
|
||||
|
||||
+17
-2
@@ -76,8 +76,23 @@ class GitService:
|
||||
if sign:
|
||||
args.append("-S")
|
||||
args.extend(["-m", message])
|
||||
stdout, stderr, _ = self._run_git_command(args)
|
||||
return not stderr
|
||||
stdout, stderr, returncode = self._run_git_command(args)
|
||||
return returncode == 0
|
||||
|
||||
def commit_verbose(
|
||||
self, message: str, sign: bool = False
|
||||
) -> Tuple[bool, str, str, int]:
|
||||
"""Create a commit and return success and raw outputs.
|
||||
|
||||
Returns:
|
||||
(ok, stdout, stderr, returncode)
|
||||
"""
|
||||
args = ["commit"]
|
||||
if sign:
|
||||
args.append("-S")
|
||||
args.extend(["-m", message])
|
||||
stdout, stderr, returncode = self._run_git_command(args)
|
||||
return returncode == 0, stdout, stderr, returncode
|
||||
|
||||
def get_repo_name(self) -> str:
|
||||
"""Get the repository name."""
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-ecosystem formatter runner.
|
||||
|
||||
Best-effort: formats sources if tools exist. Safe to run repeatedly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def has_file(*names: str) -> bool:
|
||||
return any((Path.cwd() / name).exists() for name in names)
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> int:
|
||||
try:
|
||||
return subprocess.run(cmd, check=False).returncode
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Node/TS: prettier write
|
||||
if has_file("package.json"):
|
||||
if shutil.which("npm"):
|
||||
run(["npx", "--yes", "prettier", "--write", "."]) # type: ignore
|
||||
elif shutil.which("pnpm"):
|
||||
run(["pnpm", "dlx", "prettier", "--write", "."]) # type: ignore
|
||||
elif shutil.which("yarn"):
|
||||
run(["npx", "--yes", "prettier", "--write", "."]) # type: ignore
|
||||
|
||||
# Python: ruff format or black
|
||||
if has_file("pyproject.toml", "requirements.txt"):
|
||||
if shutil.which("ruff"):
|
||||
run(["ruff", "format", "."]) # type: ignore
|
||||
elif shutil.which("black"):
|
||||
run(["black", "."]) # type: ignore
|
||||
|
||||
# Rust: cargo fmt
|
||||
if has_file("Cargo.toml") and shutil.which("cargo"):
|
||||
run(["cargo", "fmt", "--all"]) # type: ignore
|
||||
|
||||
# Go: gofmt -w
|
||||
if has_file("go.mod") and shutil.which("go"):
|
||||
# Format in-place; gofmt returns 0 even if files changed
|
||||
run(["gofmt", "-w", "."]) # type: ignore
|
||||
|
||||
# Java: Gradle/Maven format plugins are not standard; skip
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import shutil
|
||||
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-ecosystem lint fixer.
|
||||
|
||||
Best-effort: applies automatic fixes if tools exist. Safe to run repeatedly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def has_file(*names: str) -> bool:
|
||||
return any((Path.cwd() / name).exists() for name in names)
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> int:
|
||||
try:
|
||||
return subprocess.run(cmd, check=False).returncode
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import shutil
|
||||
|
||||
# Node/TS: eslint --fix and prettier --write
|
||||
if has_file("package.json"):
|
||||
if shutil.which("npx"):
|
||||
run(["npx", "--yes", "eslint", "--fix", "."]) # type: ignore
|
||||
run(["npx", "--yes", "prettier", "--write", "."]) # type: ignore
|
||||
elif shutil.which("pnpm"):
|
||||
run(["pnpm", "dlx", "eslint", "--fix", "."]) # type: ignore
|
||||
run(["pnpm", "dlx", "prettier", "--write", "."]) # type: ignore
|
||||
|
||||
# Python: ruff --fix or autoflake + isort + black
|
||||
if has_file("pyproject.toml", "requirements.txt"):
|
||||
if shutil.which("ruff"):
|
||||
run(["ruff", "check", "--fix", "."]) # type: ignore
|
||||
else:
|
||||
if shutil.which("autoflake"):
|
||||
run(["autoflake", "-r", "--in-place", "--remove-all-unused-imports", "."]) # type: ignore
|
||||
if shutil.which("isort"):
|
||||
run(["isort", "."]) # type: ignore
|
||||
if shutil.which("black"):
|
||||
run(["black", "."]) # type: ignore
|
||||
|
||||
# Go: go fmt and go fix
|
||||
if has_file("go.mod") and shutil.which("go"):
|
||||
run(["go", "fmt", "./..."]) # type: ignore
|
||||
run(["go", "fix", "./..."]) # type: ignore
|
||||
|
||||
# Rust: cargo clippy --fix (requires nightly for some fixes; allow failure)
|
||||
if has_file("Cargo.toml") and shutil.which("cargo"):
|
||||
run(["cargo", "clippy", "--fix", "-Z", "unstable-options", "--allow-dirty", "--allow-staged"]) # type: ignore
|
||||
|
||||
# Java: no standard auto-fix; skip
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user