diff --git a/home-manager/git.nix b/home-manager/git.nix index 2aec415c..bcae515f 100644 --- a/home-manager/git.nix +++ b/home-manager/git.nix @@ -187,10 +187,19 @@ in command = "${lib.getExe pkgs.unstable.typos} --config '${../typos.toml}' "; }; - # TODO: Split and run for each tool. Currently optimized for each setup, but the maintenance cost is not small - pre-push-all = { - event = "pre-push"; # Git does not provide hooks for renaming branch, so using in checkout phase is not enough - command = lib.getExe pkgs.local.git-hooks-pre-push; + pre-push-no-leaks = { + event = "pre-push"; + command = "${lib.getExe pkgs.local.git-hooks-pre-push} betterleaks"; + }; + + pre-push-no-typos-in-log = { + event = "pre-push"; + command = "${lib.getExe pkgs.local.git-hooks-pre-push} typos-log"; + }; + + pre-push-no-typos-in-branch = { + event = "pre-push"; + command = "${lib.getExe pkgs.local.git-hooks-pre-push} typos-branch"; }; }; }; diff --git a/pkgs/local/git-hooks-pre-push/main.go b/pkgs/local/git-hooks-pre-push/main.go index 66ff1f7a..a2137670 100644 --- a/pkgs/local/git-hooks-pre-push/main.go +++ b/pkgs/local/git-hooks-pre-push/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "flag" "fmt" "log" "os" @@ -19,8 +20,32 @@ var ( // Spec of Git: https://git-scm.com/docs/githooks#_pre_push func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Subcommands:\n") + fmt.Fprintf(os.Stderr, " betterleaks Prevent secrets in log and diff\n") + fmt.Fprintf(os.Stderr, " typos-log Prevent typos in log and diff\n") + fmt.Fprintf(os.Stderr, " typos-branch Prevent typos in branch name\n") + } + + if len(os.Args) < 2 { + flag.Usage() + os.Exit(1) + } + + subcommand := os.Args[1] + // Using NewFlagSet for subcommands is a Go best practice. + // ExitOnError automatically handles -h and unknown flags. + subCmd := flag.NewFlagSet(subcommand, flag.ExitOnError) + subCmd.Parse(os.Args[2:]) + log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) + // Performance note: + // Splitting into separate hooks results in multiple executions of this program, + // leading to redundant calls to "git config user.email" and "git symbolic-ref". + // However, we prioritize manageability and individual skip-ability (e.g., skipping branch name typos + // while keeping log checks) over micro-performance, as the overhead is negligible for humans. remoteDefaultBranch, err := getRemoteDefaultBranch() if err != nil { log.Fatalf("Can't get default branch of the remote repository: %+v", err) @@ -41,7 +66,9 @@ func main() { fmt.Println("Error:", err) } for desc, linter := range lintersForEntry { - linters[fmt.Sprintf("L%d:%s:%s", lineNumber, line, desc)] = linter + if linter.Tag == subcommand { + linters[fmt.Sprintf("L%d:%s:%s", lineNumber, line, desc)] = linter + } } } @@ -63,10 +90,19 @@ func initializeLinters(line string, remoteBranch string, email string) (map[stri } localRef := fields[0] - // localOid := fields[1] + localOid := fields[1] remoteRef := fields[2] // remoteOid := fields[3] + // Handle branch deletions and initial pushes. + // - localOid == 00...0: The branch is being deleted, so there's no local history to scan. + // - localRef == "(delete)": Some environments/tools use this as a placeholder for deletions. + // Skipping these prevents "git log" from failing with "fatal: bad revision" when the reference is gone. + // This also fixes issues where pushing an empty repository or first-time branch creation might trigger errors. + if localRef == "(delete)" || localOid == "0000000000000000000000000000000000000000" { + return nil, nil + } + return map[string]githooks.Linter{ "prevent secrets in log and diff": githooks.Linter{Tag: "betterleaks", Script: func() error { cmd := exec.Command("betterleaks", "--verbose", "git", fmt.Sprintf("--log-opts=--author=%s %s..%s", email, remoteBranch, localRef)) @@ -75,7 +111,7 @@ func initializeLinters(line string, remoteBranch string, email string) (map[stri log.Println(string(out)) return err }}, - "prevent typos in log and diff": githooks.Linter{Tag: "typos", Script: func() error { + "prevent typos in log and diff": githooks.Linter{Tag: "typos-log", Script: func() error { out, err := pipeline.CombinedOutput( // --patch displays diff // --unified=0(-U0) trims excess lines from the diff @@ -86,7 +122,7 @@ func initializeLinters(line string, remoteBranch string, email string) (map[stri log.Println(string(out)) return err }}, - "prevent typos in branch name": githooks.Linter{Tag: "typos", Script: func() error { + "prevent typos in branch name": githooks.Linter{Tag: "typos-branch", Script: func() error { cmd := exec.Command("typos", "--config", TyposConfigPath, "-") // Git ref is not a filepath, but avoiding a typos limitation for slash included strings // See https://github.com/crate-ci/typos/issues/758 for details