From 253fa99f8e2c1f7f5228e5009daec180ee4f0da5 Mon Sep 17 00:00:00 2001 From: Leonidas Vrachnis Date: Mon, 2 Sep 2024 22:35:23 +0200 Subject: [PATCH] feat: implement hierarchical Git hook configuration and execution This commit introduces support for configuring and running Git hooks in a hierarchical manner. It includes: - Global hook configuration in ~/.git-hooks - Local repository-specific hooks in $GIT_DIR/.git-hooks - Backwards compatibility with standard Git hooks The system now supports all Git hook types and allows for flexible, multi-level hook management. --- .git-hooks/pre-commit.d/echo.sh | 2 + commands/add.go | 47 ++++++++++++++++++++ commands/config.go | 61 ++++++++++++++++++++++++++ commands/hook.go | 77 +++++++++++++++++++++++++++++++++ go.mod | 11 +++++ go.sum | 8 ++++ main.go | 32 ++++++++++++++ 7 files changed, 238 insertions(+) create mode 100755 .git-hooks/pre-commit.d/echo.sh create mode 100644 commands/add.go create mode 100644 commands/config.go create mode 100644 commands/hook.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.git-hooks/pre-commit.d/echo.sh b/.git-hooks/pre-commit.d/echo.sh new file mode 100755 index 0000000..86df5ac --- /dev/null +++ b/.git-hooks/pre-commit.d/echo.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "echo from .git-hooks/pre-commit.d/echo.sh" \ No newline at end of file diff --git a/commands/add.go b/commands/add.go new file mode 100644 index 0000000..510e5dd --- /dev/null +++ b/commands/add.go @@ -0,0 +1,47 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/urfave/cli/v2" +) + +var Add = &cli.Command{ + Name: "add", + Usage: "adds a new git hook", + Subcommands: []*cli.Command{ + { + Name: "gitleaks", + Usage: "pre-commit hook to run gitleaks detect", + Action: func(c *cli.Context) error { + return addGitLeaks() + }, + }, + }, +} + +func addGitLeaks() error { + hooksDir := filepath.Join(os.Getenv("HOME"), ".git-hooks", "pre-commit.d") + + // Create the pre-commit.d directory if it doesn't exist + err := os.MkdirAll(hooksDir, 0755) + if err != nil { + return fmt.Errorf("failed to create pre-commit.d directory: %w", err) + } + + // Create the gitleaks script + scriptPath := filepath.Join(hooksDir, "gitleaks") + script := `#!/bin/bash +go run github.com/zricethezav/gitleaks/v8@v8.18.4 protect --staged +` + + err = os.WriteFile(scriptPath, []byte(script), 0755) + if err != nil { + return fmt.Errorf("failed to create gitleaks script: %w", err) + } + + fmt.Printf("Gitleaks pre-commit hook installed at: %s\n", scriptPath) + return nil +} diff --git a/commands/config.go b/commands/config.go new file mode 100644 index 0000000..b3c3608 --- /dev/null +++ b/commands/config.go @@ -0,0 +1,61 @@ +package commands + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/urfave/cli/v2" +) + +var Config = &cli.Command{ + Name: "config", + Usage: "", + Action: func(c *cli.Context) error { + return configureGitHooks() + }, +} + +var gitHooks = []string{ + "applypatch-msg", "pre-applypatch", "post-applypatch", + "pre-commit", "pre-merge-commit", "prepare-commit-msg", "commit-msg", "post-commit", + "pre-rebase", "post-checkout", "post-merge", "pre-push", "pre-receive", + "update", "proc-receive", "post-receive", "post-update", "reference-transaction", + "push-to-checkout", "pre-auto-gc", "post-rewrite", "sendemail-validate", + "fsmonitor-watchman", "p4-changelist", "p4-prepare-changelist", "p4-post-changelist", "p4-pre-submit", + "post-index-change", +} + +func configureGitHooks() error { + hooksDir := filepath.Join(os.Getenv("HOME"), ".git-hooks") + + // Create the directory if it doesn't exist + err := os.MkdirAll(hooksDir, 0755) + if err != nil { + return fmt.Errorf("failed to create hooks directory: %w", err) + } + + // Create a script for each Git hook + for _, hook := range gitHooks { + scriptContent := fmt.Sprintf(`#!/bin/sh +git-hooks hook %s "$@" +`, hook) + + scriptPath := filepath.Join(hooksDir, hook) + err = os.WriteFile(scriptPath, []byte(scriptContent), 0755) + if err != nil { + return fmt.Errorf("failed to create script for %s: %w", hook, err) + } + } + + // Configure Git to use the directory + cmd := exec.Command("git", "config", "--global", "core.hooksPath", hooksDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to configure Git hooks: %w", err) + } + + fmt.Printf("Git hooks configured to use directory: %s\n", hooksDir) + fmt.Println("Scripts created for all Git hooks") + return nil +} diff --git a/commands/hook.go b/commands/hook.go new file mode 100644 index 0000000..3c45033 --- /dev/null +++ b/commands/hook.go @@ -0,0 +1,77 @@ +package commands + +import ( + "os" + "os/exec" + "path/filepath" + + "github.com/urfave/cli/v2" +) + +var Hooks = &cli.Command{ + Name: "hook", + Usage: "", + Action: func(c *cli.Context) error { + if c.NArg() == 0 { + return cli.ShowAppHelp(c) + } + + hookName := c.Args().First() + return executeHook(hookName) + }, +} + +func executeHook(hookName string) error { + // 1. Execute global scripts + err := executeScriptsInDir(filepath.Join(os.Getenv("HOME"), ".git-hooks", hookName+".d")) + if err != nil { + return err + } + + err = executeScriptsInDir(filepath.Join(".git-hooks", hookName+".d")) + if err != nil { + return err + } + + // 3. Execute standard Git hook for backwards compatibility + gitDir := os.Getenv("GIT_DIR") + if gitDir == "" { + gitDir = ".git" + } + return executeStandardGitHook(filepath.Join(gitDir, "hooks", hookName)) +} + +func executeScriptsInDir(dir string) error { + files, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil // Directory doesn't exist, which is fine + } + return err + } + + for _, file := range files { + if !file.IsDir() { + scriptPath := filepath.Join(dir, file.Name()) + if err := executeScript(scriptPath); err != nil { + return err + } + } + } + + return nil +} + +func executeScript(path string) error { + cmd := exec.Command(path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func executeStandardGitHook(path string) error { + if _, err := os.Stat(path); err == nil { + return executeScript(path) + } + return nil // Hook doesn't exist, which is fine +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..603808a --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/lvrach/git-hooks + +go 1.22.4 + +require github.com/urfave/cli/v2 v2.27.4 + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..46bd4d8 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a8a2948 --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "os" + + "github.com/lvrach/git-hooks/commands" + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "git-hooks-manager", + Usage: "Manage and execute Git hooks", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "configure", + Usage: "Configure Git to use this tool for hooks", + }, + }, + Commands: []*cli.Command{ + commands.Config, + commands.Hooks, + commands.Add, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +}