Skip to content

Commit 770e128

Browse files
authored
Add git hooks checks for Node and Python (#22)
2 parents 338f34a + 03dc158 commit 770e128

3 files changed

Lines changed: 188 additions & 0 deletions

File tree

internal/check/githooks.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
)
10+
11+
var lookPath = exec.LookPath
12+
13+
type GitHooksCheck struct {
14+
Dir string
15+
Stack string // "node" or "python"
16+
}
17+
18+
func (c *GitHooksCheck) Name() string {
19+
switch c.Stack {
20+
case "node":
21+
return "Git hooks configured for Node"
22+
case "python":
23+
return "Git hooks configured for Python"
24+
default:
25+
return "Git hooks configured"
26+
}
27+
}
28+
29+
func (c *GitHooksCheck) Run(_ context.Context) Result {
30+
if !dirExists(filepath.Join(c.Dir, ".git")) {
31+
return Result{
32+
Name: c.Name(),
33+
Status: StatusSkipped,
34+
Message: "not a git repository (no .git directory)",
35+
}
36+
}
37+
38+
switch c.Stack {
39+
case "node":
40+
return c.runNode()
41+
case "python":
42+
return c.runPython()
43+
default:
44+
return Result{
45+
Name: c.Name(),
46+
Status: StatusSkipped,
47+
Message: "unknown stack type for git hooks check",
48+
}
49+
}
50+
}
51+
52+
func (c *GitHooksCheck) runNode() Result {
53+
huskyDir := filepath.Join(c.Dir, ".husky")
54+
if dirExists(huskyDir) {
55+
return Result{
56+
Name: c.Name(),
57+
Status: StatusPass,
58+
Message: ".husky directory exists; git hooks are configured",
59+
}
60+
}
61+
62+
return Result{
63+
Name: c.Name(),
64+
Status: StatusWarn,
65+
Message: ".husky directory not found; git hooks for Node are not configured",
66+
Fix: "set up Husky (or another git hooks tool) to run linting/tests before commits",
67+
}
68+
}
69+
70+
func (c *GitHooksCheck) runPython() Result {
71+
configPath := filepath.Join(c.Dir, ".pre-commit-config.yaml")
72+
_, err := os.Stat(configPath)
73+
configExists := err == nil
74+
75+
_, err = lookPath("pre-commit")
76+
preCommitInstalled := err == nil
77+
78+
if configExists && preCommitInstalled {
79+
return Result{
80+
Name: c.Name(),
81+
Status: StatusPass,
82+
Message: "pre-commit is installed and .pre-commit-config.yaml exists",
83+
}
84+
}
85+
86+
var details string
87+
if !configExists && !preCommitInstalled {
88+
details = ".pre-commit-config.yaml not found and pre-commit is not installed"
89+
} else if !configExists {
90+
details = ".pre-commit-config.yaml not found"
91+
} else {
92+
details = "pre-commit is not installed"
93+
}
94+
95+
return Result{
96+
Name: c.Name(),
97+
Status: StatusWarn,
98+
Message: fmt.Sprintf("git hooks for Python are not fully configured: %s", details),
99+
Fix: "install pre-commit and add a .pre-commit-config.yaml so hooks can run linting/tests before commits",
100+
}
101+
}
102+
103+
func dirExists(path string) bool {
104+
info, err := os.Stat(path)
105+
if err != nil {
106+
return false
107+
}
108+
return info.IsDir()
109+
}
110+

internal/check/githooks_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestGitHooksCheck_Node_PassAndWarn(t *testing.T) {
11+
dir := t.TempDir()
12+
13+
// ensure we are treated as a git repo
14+
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
15+
t.Fatalf("failed to create .git directory: %v", err)
16+
}
17+
18+
check := &GitHooksCheck{Dir: dir, Stack: "node"}
19+
20+
// Pass when .husky exists
21+
if err := os.Mkdir(filepath.Join(dir, ".husky"), 0o755); err != nil {
22+
t.Fatalf("failed to create .husky directory: %v", err)
23+
}
24+
result := check.Run(context.Background())
25+
if result.Status != StatusPass {
26+
t.Errorf("expected pass when .husky exists, got %v: %s", result.Status, result.Message)
27+
}
28+
29+
// Warn when .husky is missing
30+
if err := os.RemoveAll(filepath.Join(dir, ".husky")); err != nil {
31+
t.Fatalf("failed to remove .husky directory: %v", err)
32+
}
33+
result = check.Run(context.Background())
34+
if result.Status != StatusWarn {
35+
t.Errorf("expected warn when .husky missing, got %v: %s", result.Status, result.Message)
36+
}
37+
}
38+
39+
func TestGitHooksCheck_Python_PassAndWarn(t *testing.T) {
40+
dir := t.TempDir()
41+
42+
// ensure we are treated as a git repo
43+
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
44+
t.Fatalf("failed to create .git directory: %v", err)
45+
}
46+
47+
// override lookPath so tests don't depend on environment
48+
origLookPath := lookPath
49+
defer func() { lookPath = origLookPath }()
50+
51+
check := &GitHooksCheck{Dir: dir, Stack: "python"}
52+
53+
// Pass when config exists and pre-commit is "installed"
54+
if err := os.WriteFile(filepath.Join(dir, ".pre-commit-config.yaml"), []byte("repos: []\n"), 0o644); err != nil {
55+
t.Fatalf("failed to write .pre-commit-config.yaml: %v", err)
56+
}
57+
lookPath = func(string) (string, error) {
58+
return "/usr/bin/pre-commit", nil
59+
}
60+
61+
result := check.Run(context.Background())
62+
if result.Status != StatusPass {
63+
t.Errorf("expected pass when pre-commit and config present, got %v: %s", result.Status, result.Message)
64+
}
65+
66+
// Warn when either piece is missing (simulate missing pre-commit)
67+
lookPath = func(string) (string, error) {
68+
return "", os.ErrNotExist
69+
}
70+
71+
result = check.Run(context.Background())
72+
if result.Status != StatusWarn {
73+
t.Errorf("expected warn when pre-commit missing, got %v: %s", result.Status, result.Message)
74+
}
75+
}
76+

internal/check/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ func Build(stack detector.DetectedStack) []Check {
1717
cs = append(cs, &BinaryCheck{Binary: "node"})
1818
cs = append(cs, &BinaryCheck{Binary: "npm"})
1919
cs = append(cs, &NodeVersionCheck{Dir: "."})
20+
cs = append(cs, &GitHooksCheck{Dir: ".", Stack: "node"})
2021
}
2122
if stack.Python {
2223
cs = append(cs, &BinaryCheck{Binary: "python3"})
2324
cs = append(cs, &BinaryCheck{Binary: "pip"})
25+
cs = append(cs, &GitHooksCheck{Dir: ".", Stack: "python"})
2426
}
2527
if stack.Java {
2628
cs = append(cs, &BinaryCheck{Binary: "java"})

0 commit comments

Comments
 (0)