Files
keyhunter/cmd/hook.go
salvacybersec aa8daf8de2 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
2026-04-05 23:58:44 +03:00

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
}