Skip to content

Commit

Permalink
feat: implement hierarchical Git hook configuration and execution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lvrach committed Sep 2, 2024
1 parent 0770b26 commit 253fa99
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .git-hooks/pre-commit.d/echo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
echo "echo from .git-hooks/pre-commit.d/echo.sh"
47 changes: 47 additions & 0 deletions commands/add.go
Original file line number Diff line number Diff line change
@@ -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/[email protected] 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
}
61 changes: 61 additions & 0 deletions commands/config.go
Original file line number Diff line number Diff line change
@@ -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
}
77 changes: 77 additions & 0 deletions commands/hook.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
32 changes: 32 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 253fa99

Please sign in to comment.