- 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
109 lines
3.0 KiB
Go
109 lines
3.0 KiB
Go
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
|
|
}
|