From ec083b3ddd5f487c36cdbca124bf1f074225116a Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:14:10 +0900 Subject: [PATCH 1/6] Migrate pre-push hooks to writeShellApplication Split the monolithic Go pre-push wrapper into separate betterleaks and typos hooks using writeShellApplication. This preserves the existing logic while adopting Git 2.54.0 config-based hook style. The original Go-based git-hooks-pre-push package is removed as it is no longer needed. Assisted-by: Gemini:gemini-3.1 --- home-manager/git.nix | 44 +++++++- pkgs/local/git-hooks-pre-push/main.go | 117 ---------------------- pkgs/local/git-hooks-pre-push/package.nix | 64 ------------ 3 files changed, 40 insertions(+), 185 deletions(-) delete mode 100644 pkgs/local/git-hooks-pre-push/main.go delete mode 100644 pkgs/local/git-hooks-pre-push/package.nix diff --git a/home-manager/git.nix b/home-manager/git.nix index 2aec415c..a0bfc8a7 100644 --- a/home-manager/git.nix +++ b/home-manager/git.nix @@ -187,10 +187,46 @@ 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.writeShellApplication { + name = "git-hook-pre-push-no-leaks"; + runtimeInputs = with pkgs; [ + gitMinimal + unstable.betterleaks + ]; + text = '' + remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) + email=$(git config user.email) + while read -r local_ref _local_oid _remote_ref _remote_oid; do + betterleaks --verbose git --log-opts="--author=$email $remote_branch..$local_ref" + done + ''; + } + ); + }; + + pre-push-no-typos = { + event = "pre-push"; + command = lib.getExe ( + pkgs.writeShellApplication { + name = "git-hook-pre-push-no-typos"; + runtimeInputs = with pkgs; [ + gitMinimal + unstable.typos + coreutils + ]; + text = '' + remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) + email=$(git config user.email) + while read -r local_ref _local_oid remote_ref _remote_oid; do + git log --author="$email" --patch --unified=0 "$remote_branch..$local_ref" | typos --config "${../typos.toml}" - + basename "$remote_ref" | typos --config "${../typos.toml}" - + done + ''; + } + ); }; }; }; diff --git a/pkgs/local/git-hooks-pre-push/main.go b/pkgs/local/git-hooks-pre-push/main.go deleted file mode 100644 index 66ff1f7a..00000000 --- a/pkgs/local/git-hooks-pre-push/main.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "log" - "os" - "os/exec" - "path" - "strings" - - "github.com/kachick/dotfiles/internal/githooks" - pipeline "github.com/mattn/go-pipeline" -) - -var ( - TyposConfigPath string -) - -// Spec of Git: https://git-scm.com/docs/githooks#_pre_push -func main() { - log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) - - remoteDefaultBranch, err := getRemoteDefaultBranch() - if err != nil { - log.Fatalf("Can't get default branch of the remote repository: %+v", err) - } - email, err := getEmail() - if err != nil { - log.Fatalf("Can't get git email: %+v", err) - } - - scanner := bufio.NewScanner(os.Stdin) - linters := map[string]githooks.Linter{} - lineNumber := 0 - for scanner.Scan() { - line := scanner.Text() - lineNumber += 1 - lintersForEntry, err := initializeLinters(line, remoteDefaultBranch, email) - if err != nil { - fmt.Println("Error:", err) - } - for desc, linter := range lintersForEntry { - linters[fmt.Sprintf("L%d:%s:%s", lineNumber, line, desc)] = linter - } - } - - if err := scanner.Err(); err != nil { - fmt.Println("Error reading input:", err) - os.Exit(1) - } - - if err := githooks.RunLinters(linters); err != nil { - log.Fatalf("Failed to run global hook: %+v", err) - } -} - -// Filtering with author email for large repository such as NixOS/nixpkgs -func initializeLinters(line string, remoteBranch string, email string) (map[string]githooks.Linter, error) { - fields := strings.Fields(line) - if len(fields) != 4 { - return nil, fmt.Errorf("parsing error for given line: %s", line) - } - - localRef := fields[0] - // localOid := fields[1] - remoteRef := fields[2] - // remoteOid := fields[3] - - 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)) - out, err := cmd.CombinedOutput() - log.Println(strings.Join(cmd.Args, " ")) - log.Println(string(out)) - return err - }}, - "prevent typos in log and diff": githooks.Linter{Tag: "typos", Script: func() error { - out, err := pipeline.CombinedOutput( - // --patch displays diff - // --unified=0(-U0) trims excess lines from the diff - // ref: https://github.com/gitleaks/gitleaks/blob/4b541044e817274bad3407128bb226740295857c/sources/git.go#L33 - []string{"git", "log", "--author=" + email, "--patch", "--unified=0", fmt.Sprintf("%s..%s", remoteBranch, localRef)}, - []string{"typos", "--config", TyposConfigPath, "-"}, - ) - log.Println(string(out)) - return err - }}, - "prevent typos in branch name": githooks.Linter{Tag: "typos", 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 - cmd.Stdin = strings.NewReader(path.Base(remoteRef)) - out, err := cmd.CombinedOutput() - log.Println(string(out)) - return err - }}, - }, nil -} - -func getRemoteDefaultBranch() (string, error) { - cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD") - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get remote default branch: %w", err) - } - return strings.TrimSpace(string(out)), nil -} - -func getEmail() (string, error) { - cmd := exec.Command("git", "config", "user.email") - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get git email: %w", err) - } - return strings.TrimSpace(string(out)), nil -} diff --git a/pkgs/local/git-hooks-pre-push/package.nix b/pkgs/local/git-hooks-pre-push/package.nix deleted file mode 100644 index fe46c93d..00000000 --- a/pkgs/local/git-hooks-pre-push/package.nix +++ /dev/null @@ -1,64 +0,0 @@ -{ - pkgs, - lib, - makeWrapper, - ... -}: - -let - inherit (pkgs.unstable) buildGo126Module; -in -buildGo126Module (finalAttrs: { - pname = "git-hooks-pre-push"; - version = "0.0.1"; - - nativeBuildInputs = [ - makeWrapper - ]; - - wrapperPath = lib.makeBinPath ( - with pkgs; - [ - gitMinimal - unstable.typos - unstable.betterleaks - ] - ); - - postFixup = '' - wrapProgram $out/bin/${finalAttrs.meta.mainProgram} \ - --prefix PATH : "${finalAttrs.wrapperPath}" - ''; - - vendorHash = "sha256-kc5iRgfLP9mRyFppV02yA+G1BIJK9MD+L79GUW/uKD4="; - src = - with lib.fileset; - toSource { - root = ../../../.; - fileset = unions [ - ../../../go.mod - ../../../go.sum - ../../../internal - ./. - ]; - }; - - subPackages = [ - "pkgs/local/${finalAttrs.pname}" - ]; - - env.CGO_ENABLED = 0; - - passthru.shared-gomod = true; - - ldflags = [ - "-s" - "-w" - "-X main.TyposConfigPath=${../../../typos.toml}" - ]; - - meta = { - description = "GH-540 and GH-699"; - mainProgram = finalAttrs.pname; - }; -}) From ec83be90eb97f58c7ebfb6be298e1bd7f6fbbfc1 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:14:47 +0900 Subject: [PATCH 2/6] Handle branch deletions in pre-push hooks Skip execution when the local OID is null (all zeros), which indicates a branch deletion. This prevents errors from git log when attempting to scan non-existent references. Assisted-by: Gemini:gemini-3.1 --- home-manager/git.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/home-manager/git.nix b/home-manager/git.nix index a0bfc8a7..ed5d9d94 100644 --- a/home-manager/git.nix +++ b/home-manager/git.nix @@ -199,7 +199,10 @@ in text = '' remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) email=$(git config user.email) - while read -r local_ref _local_oid _remote_ref _remote_oid; do + while read -r local_ref local_oid _remote_ref _remote_oid; do + if [ "$local_ref" = "(delete)" ] || [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then + continue + fi betterleaks --verbose git --log-opts="--author=$email $remote_branch..$local_ref" done ''; @@ -220,8 +223,14 @@ in text = '' remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) email=$(git config user.email) - while read -r local_ref _local_oid remote_ref _remote_oid; do + while read -r local_ref local_oid remote_ref _remote_oid; do + if [ "$local_ref" = "(delete)" ] || [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then + continue + fi + # Filtering with author email for large repository such as NixOS/nixpkgs + # prevent typos in log and diff git log --author="$email" --patch --unified=0 "$remote_branch..$local_ref" | typos --config "${../typos.toml}" - + # prevent typos in branch name basename "$remote_ref" | typos --config "${../typos.toml}" - done ''; From 9b5d41b601bd28982fc806f5875681dfd175ecd1 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:19:36 +0900 Subject: [PATCH 3/6] Split pre-push hooks using Go wrapper with subcommands Restore the Go-based pre-push hook wrapper and enhance it to support subcommands. This allows splitting the hooks in git.nix for individual skipping while maintaining the robustness of a Go implementation. The Go wrapper now: - Accepts a subcommand ('betterleaks' or 'typos') to filter checks. - Handles branch deletions by skipping null OIDs. Assisted-by: Gemini:gemini-3.1 --- home-manager/git.nix | 45 +------- pkgs/local/git-hooks-pre-push/main.go | 128 ++++++++++++++++++++++ pkgs/local/git-hooks-pre-push/package.nix | 64 +++++++++++ 3 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 pkgs/local/git-hooks-pre-push/main.go create mode 100644 pkgs/local/git-hooks-pre-push/package.nix diff --git a/home-manager/git.nix b/home-manager/git.nix index ed5d9d94..8bce535d 100644 --- a/home-manager/git.nix +++ b/home-manager/git.nix @@ -189,53 +189,12 @@ in pre-push-no-leaks = { event = "pre-push"; - command = lib.getExe ( - pkgs.writeShellApplication { - name = "git-hook-pre-push-no-leaks"; - runtimeInputs = with pkgs; [ - gitMinimal - unstable.betterleaks - ]; - text = '' - remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) - email=$(git config user.email) - while read -r local_ref local_oid _remote_ref _remote_oid; do - if [ "$local_ref" = "(delete)" ] || [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then - continue - fi - betterleaks --verbose git --log-opts="--author=$email $remote_branch..$local_ref" - done - ''; - } - ); + command = "${lib.getExe pkgs.local.git-hooks-pre-push} betterleaks"; }; pre-push-no-typos = { event = "pre-push"; - command = lib.getExe ( - pkgs.writeShellApplication { - name = "git-hook-pre-push-no-typos"; - runtimeInputs = with pkgs; [ - gitMinimal - unstable.typos - coreutils - ]; - text = '' - remote_branch=$(git symbolic-ref refs/remotes/origin/HEAD) - email=$(git config user.email) - while read -r local_ref local_oid remote_ref _remote_oid; do - if [ "$local_ref" = "(delete)" ] || [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then - continue - fi - # Filtering with author email for large repository such as NixOS/nixpkgs - # prevent typos in log and diff - git log --author="$email" --patch --unified=0 "$remote_branch..$local_ref" | typos --config "${../typos.toml}" - - # prevent typos in branch name - basename "$remote_ref" | typos --config "${../typos.toml}" - - done - ''; - } - ); + command = "${lib.getExe pkgs.local.git-hooks-pre-push} typos"; }; }; }; diff --git a/pkgs/local/git-hooks-pre-push/main.go b/pkgs/local/git-hooks-pre-push/main.go new file mode 100644 index 00000000..2458caef --- /dev/null +++ b/pkgs/local/git-hooks-pre-push/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "os/exec" + "path" + "strings" + + "github.com/kachick/dotfiles/internal/githooks" + pipeline "github.com/mattn/go-pipeline" +) + +var ( + TyposConfigPath string +) + +// Spec of Git: https://git-scm.com/docs/githooks#_pre_push +func main() { + if len(os.Args) < 2 { + log.Fatalf("Usage: %s ", os.Args[0]) + } + subcommand := os.Args[1] + + log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) + + remoteDefaultBranch, err := getRemoteDefaultBranch() + if err != nil { + log.Fatalf("Can't get default branch of the remote repository: %+v", err) + } + email, err := getEmail() + if err != nil { + log.Fatalf("Can't get git email: %+v", err) + } + + scanner := bufio.NewScanner(os.Stdin) + linters := map[string]githooks.Linter{} + lineNumber := 0 + for scanner.Scan() { + line := scanner.Text() + lineNumber += 1 + lintersForEntry, err := initializeLinters(line, remoteDefaultBranch, email) + if err != nil { + fmt.Println("Error:", err) + } + for desc, linter := range lintersForEntry { + if linter.Tag == subcommand { + linters[fmt.Sprintf("L%d:%s:%s", lineNumber, line, desc)] = linter + } + } + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading input:", err) + os.Exit(1) + } + + if err := githooks.RunLinters(linters); err != nil { + log.Fatalf("Failed to run global hook: %+v", err) + } +} + +// Filtering with author email for large repository such as NixOS/nixpkgs +func initializeLinters(line string, remoteBranch string, email string) (map[string]githooks.Linter, error) { + fields := strings.Fields(line) + if len(fields) != 4 { + return nil, fmt.Errorf("parsing error for given line: %s", line) + } + + localRef := fields[0] + localOid := fields[1] + remoteRef := fields[2] + // remoteOid := fields[3] + + 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)) + out, err := cmd.CombinedOutput() + log.Println(strings.Join(cmd.Args, " ")) + log.Println(string(out)) + return err + }}, + "prevent typos in log and diff": githooks.Linter{Tag: "typos", Script: func() error { + out, err := pipeline.CombinedOutput( + // --patch displays diff + // --unified=0(-U0) trims excess lines from the diff + // ref: https://github.com/gitleaks/gitleaks/blob/4b541044e817274bad3407128bb226740295857c/sources/git.go#L33 + []string{"git", "log", "--author=" + email, "--patch", "--unified=0", fmt.Sprintf("%s..%s", remoteBranch, localRef)}, + []string{"typos", "--config", TyposConfigPath, "-"}, + ) + log.Println(string(out)) + return err + }}, + "prevent typos in branch name": githooks.Linter{Tag: "typos", 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 + cmd.Stdin = strings.NewReader(path.Base(remoteRef)) + out, err := cmd.CombinedOutput() + log.Println(string(out)) + return err + }}, + }, nil +} + +func getRemoteDefaultBranch() (string, error) { + cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD") + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get remote default branch: %w", err) + } + return strings.TrimSpace(string(out)), nil +} + +func getEmail() (string, error) { + cmd := exec.Command("git", "config", "user.email") + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get git email: %w", err) + } + return strings.TrimSpace(string(out)), nil +} diff --git a/pkgs/local/git-hooks-pre-push/package.nix b/pkgs/local/git-hooks-pre-push/package.nix new file mode 100644 index 00000000..fe46c93d --- /dev/null +++ b/pkgs/local/git-hooks-pre-push/package.nix @@ -0,0 +1,64 @@ +{ + pkgs, + lib, + makeWrapper, + ... +}: + +let + inherit (pkgs.unstable) buildGo126Module; +in +buildGo126Module (finalAttrs: { + pname = "git-hooks-pre-push"; + version = "0.0.1"; + + nativeBuildInputs = [ + makeWrapper + ]; + + wrapperPath = lib.makeBinPath ( + with pkgs; + [ + gitMinimal + unstable.typos + unstable.betterleaks + ] + ); + + postFixup = '' + wrapProgram $out/bin/${finalAttrs.meta.mainProgram} \ + --prefix PATH : "${finalAttrs.wrapperPath}" + ''; + + vendorHash = "sha256-kc5iRgfLP9mRyFppV02yA+G1BIJK9MD+L79GUW/uKD4="; + src = + with lib.fileset; + toSource { + root = ../../../.; + fileset = unions [ + ../../../go.mod + ../../../go.sum + ../../../internal + ./. + ]; + }; + + subPackages = [ + "pkgs/local/${finalAttrs.pname}" + ]; + + env.CGO_ENABLED = 0; + + passthru.shared-gomod = true; + + ldflags = [ + "-s" + "-w" + "-X main.TyposConfigPath=${../../../typos.toml}" + ]; + + meta = { + description = "GH-540 and GH-699"; + mainProgram = finalAttrs.pname; + }; +}) From 9fd1895883bf3114464259e34a378cfe323e32bb Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:24:26 +0900 Subject: [PATCH 4/6] Use flag package and document branch deletion handling Improve the Go hook wrapper by using the standard 'flag' package for argument parsing. Also, add detailed comments explaining the branch deletion guard logic (null OID check), which prevents errors during branch deletions or initial pushes where references may not exist. Assisted-by: Gemini:gemini-3.1 --- pkgs/local/git-hooks-pre-push/main.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkgs/local/git-hooks-pre-push/main.go b/pkgs/local/git-hooks-pre-push/main.go index 2458caef..556da0dc 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,10 +20,11 @@ var ( // Spec of Git: https://git-scm.com/docs/githooks#_pre_push func main() { - if len(os.Args) < 2 { + flag.Parse() + subcommand := flag.Arg(0) + if subcommand == "" { log.Fatalf("Usage: %s ", os.Args[0]) } - subcommand := os.Args[1] log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) @@ -74,6 +76,11 @@ func initializeLinters(line string, remoteBranch string, email string) (map[stri 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 } From 81936eeb03c3195b712b0eba3bdac01686209083 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:32:19 +0900 Subject: [PATCH 5/6] Semantically split pre-push hooks and document trade-offs Split pre-push-no-typos into log and branch specific hooks for better granularity and individual skip-ability. Key changes: - Update Go wrapper to support 'typos-log' and 'typos-branch' tags. - Add a performance note in Go code acknowledging redundant Git calls, prioritizing manageability over micro-optimization. - Update git.nix to register three distinct semantic hooks. Assisted-by: Gemini:gemini-3.1 --- home-manager/git.nix | 9 +++++++-- pkgs/local/git-hooks-pre-push/main.go | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/home-manager/git.nix b/home-manager/git.nix index 8bce535d..bcae515f 100644 --- a/home-manager/git.nix +++ b/home-manager/git.nix @@ -192,9 +192,14 @@ in command = "${lib.getExe pkgs.local.git-hooks-pre-push} betterleaks"; }; - pre-push-no-typos = { + pre-push-no-typos-in-log = { event = "pre-push"; - command = "${lib.getExe pkgs.local.git-hooks-pre-push} typos"; + 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 556da0dc..3dc9604a 100644 --- a/pkgs/local/git-hooks-pre-push/main.go +++ b/pkgs/local/git-hooks-pre-push/main.go @@ -23,11 +23,16 @@ func main() { flag.Parse() subcommand := flag.Arg(0) if subcommand == "" { - log.Fatalf("Usage: %s ", os.Args[0]) + log.Fatalf("Usage: %s ", os.Args[0]) } 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) @@ -93,7 +98,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 @@ -104,7 +109,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 From e88e3933ed1c0011078a6bc8b67a1661aa887c06 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 3 Jun 2026 13:36:29 +0900 Subject: [PATCH 6/6] Refactor Go hook wrapper with FlagSet and better Usage Implement Go best practices for subcommand handling by using flag.NewFlagSet. This provides a more robust structure for individual command parsing and automatic help support. Also, customize flag.Usage to provide a clear overview of available semantic subcommands. Assisted-by: Gemini:gemini-3.1 --- pkgs/local/git-hooks-pre-push/main.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pkgs/local/git-hooks-pre-push/main.go b/pkgs/local/git-hooks-pre-push/main.go index 3dc9604a..a2137670 100644 --- a/pkgs/local/git-hooks-pre-push/main.go +++ b/pkgs/local/git-hooks-pre-push/main.go @@ -20,12 +20,25 @@ var ( // Spec of Git: https://git-scm.com/docs/githooks#_pre_push func main() { - flag.Parse() - subcommand := flag.Arg(0) - if subcommand == "" { - log.Fatalf("Usage: %s ", os.Args[0]) + 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: