feat(07-05): implement keyhunter hook install/uninstall with embedded pre-commit script
- cmd/hook.go: install/uninstall subcommands with --force flag - cmd/hook_script.sh: embedded via go:embed, runs keyhunter scan on staged files - KEYHUNTER-HOOK v1 marker prevents accidental deletion of non-owned hooks - Backup existing hooks on --force install - cmd/hook_test.go: 10 tests covering fresh install, non-repo, force/backup, overwrite, uninstall lifecycle - Remove hookCmd stub from cmd/stubs.go
This commit is contained in:
108
cmd/hook.go
Normal file
108
cmd/hook.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//go:embed hook_script.sh
|
||||
var hookScript string
|
||||
|
||||
// hookMarker identifies a KeyHunter-managed hook. Uninstall refuses to
|
||||
// delete pre-commit files that don't contain this marker unless --force.
|
||||
const hookMarker = "KEYHUNTER-HOOK v1"
|
||||
|
||||
var (
|
||||
hookForce bool
|
||||
)
|
||||
|
||||
var hookCmd = &cobra.Command{
|
||||
Use: "hook",
|
||||
Short: "Install or manage git pre-commit hooks",
|
||||
}
|
||||
|
||||
var hookInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install the keyhunter pre-commit hook into .git/hooks/",
|
||||
RunE: runHookInstall,
|
||||
}
|
||||
|
||||
var hookUninstallCmd = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Remove the keyhunter pre-commit hook",
|
||||
RunE: runHookUninstall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
hookInstallCmd.Flags().BoolVar(&hookForce, "force", false, "overwrite any existing pre-commit hook without prompt")
|
||||
hookUninstallCmd.Flags().BoolVar(&hookForce, "force", false, "delete pre-commit even if it is not KeyHunter-managed")
|
||||
hookCmd.AddCommand(hookInstallCmd)
|
||||
hookCmd.AddCommand(hookUninstallCmd)
|
||||
}
|
||||
|
||||
func hookPath() (string, error) {
|
||||
gitDir := ".git"
|
||||
info, err := os.Stat(gitDir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return "", fmt.Errorf("not a git repository (no .git/ in current directory)")
|
||||
}
|
||||
return filepath.Join(gitDir, "hooks", "pre-commit"), nil
|
||||
}
|
||||
|
||||
func runHookInstall(cmd *cobra.Command, args []string) error {
|
||||
target, err := hookPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
existing, _ := os.ReadFile(target)
|
||||
if strings.Contains(string(existing), hookMarker) {
|
||||
// Already ours — overwrite silently to update script.
|
||||
} else if !hookForce {
|
||||
return fmt.Errorf("pre-commit hook already exists at %s (use --force to overwrite; a .bak backup will be kept)", target)
|
||||
} else {
|
||||
backup := target + ".bak." + time.Now().Format("20060102150405")
|
||||
if err := os.Rename(target, backup); err != nil {
|
||||
return fmt.Errorf("backing up existing hook: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Backed up existing hook to %s\n", backup)
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return fmt.Errorf("creating hooks dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(target, []byte(hookScript), 0o755); err != nil {
|
||||
return fmt.Errorf("writing hook: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Installed pre-commit hook at %s\n", target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHookUninstall(cmd *cobra.Command, args []string) error {
|
||||
target, err := hookPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "No pre-commit hook to remove.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading hook: %w", err)
|
||||
}
|
||||
if !strings.Contains(string(data), hookMarker) && !hookForce {
|
||||
return fmt.Errorf("pre-commit at %s is not KeyHunter-managed (use --force to remove anyway)", target)
|
||||
}
|
||||
if err := os.Remove(target); err != nil {
|
||||
return fmt.Errorf("removing hook: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Removed pre-commit hook at %s\n", target)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user