feat(doctor): Add hooks-path-configured check

Verifies all clones have core.hooksPath set to .githooks.
Auto-fixable with 'gt doctor --fix'.

This ensures the pre-push hook is active on all clones,
blocking pushes to invalid branches (no internal PRs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gus
2026-01-05 00:00:58 -08:00
committed by Steve Yegge
parent 325f818e11
commit 7a7d558116

View File

@@ -231,6 +231,126 @@ func (c *GitExcludeConfiguredCheck) Fix(ctx *CheckContext) error {
return nil
}
// HooksPathConfiguredCheck verifies all clones have core.hooksPath set to .githooks.
// This ensures the pre-push hook blocks pushes to invalid branches (no internal PRs).
type HooksPathConfiguredCheck struct {
FixableCheck
unconfiguredClones []string
}
// NewHooksPathConfiguredCheck creates a new hooks path check.
func NewHooksPathConfiguredCheck() *HooksPathConfiguredCheck {
return &HooksPathConfiguredCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "hooks-path-configured",
CheckDescription: "Check core.hooksPath is set for all clones",
},
},
}
}
// Run checks if all clones have core.hooksPath configured.
func (c *HooksPathConfiguredCheck) Run(ctx *CheckContext) *CheckResult {
rigPath := ctx.RigPath()
if rigPath == "" {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "No rig specified",
}
}
c.unconfiguredClones = nil
// Check all clone locations
clonePaths := []string{
filepath.Join(rigPath, "mayor", "rig"),
filepath.Join(rigPath, "refinery", "rig"),
}
// Add crew clones
crewDir := filepath.Join(rigPath, "crew")
if entries, err := os.ReadDir(crewDir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
clonePaths = append(clonePaths, filepath.Join(crewDir, entry.Name()))
}
}
}
// Add polecat clones
polecatDir := filepath.Join(rigPath, "polecats")
if entries, err := os.ReadDir(polecatDir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
clonePaths = append(clonePaths, filepath.Join(polecatDir, entry.Name()))
}
}
}
for _, clonePath := range clonePaths {
// Skip if not a git repo
if _, err := os.Stat(filepath.Join(clonePath, ".git")); os.IsNotExist(err) {
continue
}
// Skip if no .githooks directory exists
if _, err := os.Stat(filepath.Join(clonePath, ".githooks")); os.IsNotExist(err) {
continue
}
// Check core.hooksPath
cmd := exec.Command("git", "-C", clonePath, "config", "--get", "core.hooksPath")
output, err := cmd.Output()
if err != nil || strings.TrimSpace(string(output)) != ".githooks" {
// Get relative path for cleaner output
relPath, _ := filepath.Rel(rigPath, clonePath)
if relPath == "" {
relPath = clonePath
}
c.unconfiguredClones = append(c.unconfiguredClones, clonePath)
}
}
if len(c.unconfiguredClones) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "All clones have hooks configured",
}
}
// Build details with relative paths
var details []string
for _, clonePath := range c.unconfiguredClones {
relPath, _ := filepath.Rel(rigPath, clonePath)
if relPath == "" {
relPath = clonePath
}
details = append(details, relPath)
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d clone(s) missing hooks configuration", len(c.unconfiguredClones)),
Details: details,
FixHint: "Run 'gt doctor --fix' to configure hooks",
}
}
// Fix configures core.hooksPath for all unconfigured clones.
func (c *HooksPathConfiguredCheck) Fix(ctx *CheckContext) error {
for _, clonePath := range c.unconfiguredClones {
cmd := exec.Command("git", "-C", clonePath, "config", "core.hooksPath", ".githooks")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to configure hooks for %s: %w", clonePath, err)
}
}
return nil
}
// WitnessExistsCheck verifies the witness directory structure exists.
type WitnessExistsCheck struct {
FixableCheck
@@ -750,6 +870,7 @@ func RigChecks() []Check {
return []Check{
NewRigIsGitRepoCheck(),
NewGitExcludeConfiguredCheck(),
NewHooksPathConfiguredCheck(),
NewWitnessExistsCheck(),
NewRefineryExistsCheck(),
NewMayorCloneExistsCheck(),