From fd651feabd78a170ff2eb1310ddc5e21f1669ff6 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Sun, 18 Jan 2026 13:41:34 -0500 Subject: [PATCH 1/5] feat: allow user to input which hooks to install --- internal/commands/root.go | 77 +++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/internal/commands/root.go b/internal/commands/root.go index 81ba164..f7cd15e 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/devbytes-cloud/freight/internal/blueprint" "github.com/devbytes-cloud/freight/internal/config" @@ -15,6 +16,14 @@ import ( "github.com/spf13/cobra" ) +var allowHooks = map[string]struct{}{ + "pre-commit": {}, + "prepare-commit-msg": {}, + "commit-msg": {}, + "post-commit": {}, + "post-checkout": {}, +} + // Execute runs the root command and handles any errors that occur during execution. func Execute() { if err := NewRootCmd().Execute(); err != nil { @@ -39,7 +48,20 @@ func NewRootCmd() *cobra.Command { if err := validate.GitDirs(); err != nil { cmd.PrintErrln(err) } - if err := setupHooks(); err != nil { + + ah, err := cmd.Flags().GetStringSlice("allow") + if err != nil { + cmd.PrintErrln(err) + os.Exit(1) + } + + vh, err := validateAllowHooks(ah) + if err != nil { + cmd.PrintErrln(err) + os.Exit(1) + } + + if err := setupHooks(vh); err != nil { cmd.PrintErrln(err) os.Exit(1) } @@ -59,6 +81,7 @@ func NewRootCmd() *cobra.Command { } initCmd.Flags().BoolP("config-force", "c", false, "If you wish to force write the config") + initCmd.Flags().StringSliceP("allow", "a", []string{}, "Allow specific Git hooks to be installed. Valid options are pre-commit, prepare-commit-msg, commit-msg, post-commit, post-checkout") rootCmd.AddCommand(initCmd) rootCmd.AddCommand(versionCommand()) @@ -66,27 +89,34 @@ func NewRootCmd() *cobra.Command { } // setupHooks initializes and writes the Git hooks. -func setupHooks() error { +func setupHooks(allowedHooks map[string]struct{}) error { pterm.DefaultSection.Println("Generating .git/hooks") + pterm.Debug.Printfln("Allowed hooks: %v", allowedHooks) pterm.Info.Println("Writing Commit Hooks") gitHooks := githooks.NewGitHooks() for _, v := range gitHooks.Commit { - if err := writeConfig(&v); err != nil { - pterm.Error.Println("✖ Hook write failed for: ", v.Name, err.Error()) - return err + if _, ok := allowedHooks[v.Name]; ok { + if err := writeConfig(&v); err != nil { + pterm.Error.Println("✖ Hook write failed for: ", v.Name, err.Error()) + return err + } + pterm.Success.Println("✔ Hook written:", v.Name) + } else { + pterm.Warning.Println("Skipping hook:", v.Name, "not allowed") } - pterm.Success.Println("✔ Hook written:", v.Name) - } - pterm.Info.Println("Writing Checkout Hooks") for _, v := range gitHooks.Checkout { - if err := writeConfig(&v); err != nil { - pterm.Error.Println("✖ Hook write failed for: ", v.Name, err.Error()) - return err + if _, ok := allowedHooks[v.Name]; ok { + if err := writeConfig(&v); err != nil { + pterm.Error.Println("✖ Hook write failed for: ", v.Name, err.Error()) + return err + } + pterm.Success.Println("✔ Hook written:", v.Name) + } else { + pterm.Warning.Println("Skipping hook:", v.Name, "not allowed") } - pterm.Success.Println("✔ Hook written:", v.Name) } return nil @@ -142,3 +172,26 @@ func installBinary() error { pterm.Success.Println("✔ Installed conductor successfully") return nil } + +// validateAllowHooks validates the provided allow hooks and returns a map of valid hooks. +func validateAllowHooks(allow []string) (map[string]struct{}, error) { + if len(allow) == 0 { + pterm.Debug.Println("No hooks provided, using default allowed hooks") + return allowHooks, nil + } + + inputHooks := map[string]struct{}{} + var invalidHooks []string + for _, v := range allow { + if _, ok := allowHooks[v]; !ok { + invalidHooks = append(invalidHooks, v) + } + inputHooks[v] = struct{}{} + } + + if len(invalidHooks) > 0 { + return nil, fmt.Errorf("invalid hook types: %s", strings.Join(invalidHooks, ", ")) + } + + return inputHooks, nil +} From 81404025ffe4563c9810850f67e43939185a709f Mon Sep 17 00:00:00 2001 From: David Dymko Date: Sun, 18 Jan 2026 13:42:58 -0500 Subject: [PATCH 2/5] feat: allow user to input which hooks to install --- internal/commands/root.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/root.go b/internal/commands/root.go index f7cd15e..e637a55 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -49,19 +49,19 @@ func NewRootCmd() *cobra.Command { cmd.PrintErrln(err) } - ah, err := cmd.Flags().GetStringSlice("allow") + userAllow, err := cmd.Flags().GetStringSlice("allow") if err != nil { cmd.PrintErrln(err) os.Exit(1) } - vh, err := validateAllowHooks(ah) + validatedAllow, err := validateAllowHooks(userAllow) if err != nil { cmd.PrintErrln(err) os.Exit(1) } - if err := setupHooks(vh); err != nil { + if err := setupHooks(validatedAllow); err != nil { cmd.PrintErrln(err) os.Exit(1) } From 54a0efd74f418e6d165823238436d1abbb094004 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Sun, 18 Jan 2026 14:02:52 -0500 Subject: [PATCH 3/5] test: add tests to validate allow hooks --- internal/commands/root_test.go | 97 ++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/commands/root_test.go diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go new file mode 100644 index 0000000..800282b --- /dev/null +++ b/internal/commands/root_test.go @@ -0,0 +1,97 @@ +package commands + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type hookStructure struct { + inputHooks []string + expectedHooks map[string]struct{} + expectedErr bool + expectedErrMgs error +} + +func TestValidateAllowHooks(t *testing.T) { + testData := map[string]hookStructure{ + "no input hooks": { + expectedErr: false, + inputHooks: []string{}, + expectedHooks: map[string]struct{}{ + "pre-commit": {}, + "prepare-commit-msg": {}, + "commit-msg": {}, + "post-commit": {}, + "post-checkout": {}, + }, + }, + "only pre-commit hook": { + expectedErr: false, + inputHooks: []string{"pre-commit"}, + expectedHooks: map[string]struct{}{ + "pre-commit": {}, + }, + }, + "invalid hook name hook": { + expectedErr: true, + inputHooks: []string{"invalid hook name"}, + expectedErrMgs: fmt.Errorf("invalid hook types: invalid hook name"), + expectedHooks: nil, + }, + "multiple valid hooks": { + expectedErr: false, + inputHooks: []string{"pre-commit", "commit-msg", "post-checkout"}, + expectedHooks: map[string]struct{}{ + "pre-commit": {}, + "commit-msg": {}, + "post-checkout": {}, + }, + }, + "multiple invalid hooks": { + expectedErr: true, + inputHooks: []string{"invalid1", "invalid2"}, + expectedErrMgs: fmt.Errorf("invalid hook types: invalid1, invalid2"), + expectedHooks: nil, + }, + "mix of valid and invalid hooks": { + expectedErr: true, + inputHooks: []string{"pre-commit", "invalid-hook"}, + expectedErrMgs: fmt.Errorf("invalid hook types: invalid-hook"), + expectedHooks: nil, + }, + "all valid hooks explicitly provided": { + expectedErr: false, + inputHooks: []string{"pre-commit", "prepare-commit-msg", "commit-msg", "post-commit", "post-checkout"}, + expectedHooks: map[string]struct{}{ + "pre-commit": {}, + "prepare-commit-msg": {}, + "commit-msg": {}, + "post-commit": {}, + "post-checkout": {}, + }, + }, + "duplicate valid hooks": { + expectedErr: false, + inputHooks: []string{"pre-commit", "pre-commit"}, + expectedHooks: map[string]struct{}{ + "pre-commit": {}, + }, + }, + } + + for name, test := range testData { + t.Run(name, func(t *testing.T) { + resp, err := validateAllowHooks(test.inputHooks) + + if test.expectedErr { + assert.Error(t, err) + assert.EqualError(t, err, test.expectedErrMgs.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expectedHooks, resp) + } + }) + } +} From d8ef6f66b6ef72fc2335fa952504e5d2aa421554 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Sun, 18 Jan 2026 14:09:25 -0500 Subject: [PATCH 4/5] doc: add allow flag for init --- README.md | 6 ++++++ docs/readme.md | 5 +++++ internal/commands/root.go | 2 +- website/docs/cli/init.md | 14 ++++++++++++++ website/docs/installation.md | 7 +++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b8652c..7586f4b 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ freight init ``` This installs the **Conductor** binary and creates a starter **Railcar** manifest (`railcar.json`). +By default, Freight installs all supported Git hooks. You can use the `--allow` (or `-a`) flag to specify only the hooks you want: +```bash +freight init --allow pre-commit,commit-msg +``` +Valid hooks are: `pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`, and `post-checkout`. + ### 3. Verify Add a script to your `railcar.json` and watch it run on your next commit! diff --git a/docs/readme.md b/docs/readme.md index 2853221..cf1c190 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -39,6 +39,10 @@ make build-all ``` # Inside any existing Git repository freight init # installs conductor + railcar.json + rewired hooks + +# Optionally install only specific hooks +freight init --allow pre-commit,post-checkout + git add . && git commit -m "test" # your new hooks will now fire ``` @@ -57,6 +61,7 @@ Need to overwrite an existing `railcar.json`? Global flags: * `-c, --config-force` – overwrite an existing `railcar.json` +* `-a, --allow` – specific Git hooks to install (default: all). Valid options: `pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`, `post-checkout` * `-h, --help` – display help --- diff --git a/internal/commands/root.go b/internal/commands/root.go index e637a55..662cae3 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -81,7 +81,7 @@ func NewRootCmd() *cobra.Command { } initCmd.Flags().BoolP("config-force", "c", false, "If you wish to force write the config") - initCmd.Flags().StringSliceP("allow", "a", []string{}, "Allow specific Git hooks to be installed. Valid options are pre-commit, prepare-commit-msg, commit-msg, post-commit, post-checkout") + initCmd.Flags().StringSliceP("allow", "a", []string{}, "Specific Git hooks to install (default: all). Valid options: pre-commit, prepare-commit-msg, commit-msg, post-commit, post-checkout") rootCmd.AddCommand(initCmd) rootCmd.AddCommand(versionCommand()) diff --git a/website/docs/cli/init.md b/website/docs/cli/init.md index 41daced..86187c4 100644 --- a/website/docs/cli/init.md +++ b/website/docs/cli/init.md @@ -14,13 +14,27 @@ The `init` command sets up Freight in your local repository. It ensures all nece ## Flags - `-c, --config-force`: Overwrite an existing `railcar.json` file if it already exists. +- `-a, --allow`: Specific Git hooks to install (default: all). Valid options: `pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`, `post-checkout`. + + When this flag is used, Freight will **only** rewire the hooks you explicitly specify. Any existing hooks in your `.git/hooks` directory that are NOT in the allow list will remain untouched. This is useful if you want to use Freight alongside other hook managers or if you only want to manage a subset of hooks with Freight. ## Examples +Basic initialization: ```bash freight init ``` +Initialize with specific hooks (comma-separated): +```bash +freight init --allow pre-commit,commit-msg +``` + +Initialize with specific hooks (multiple flags): +```bash +freight init -a pre-commit -a post-checkout +``` + **Output:** ```text ✔ Extracting conductor binary... diff --git a/website/docs/installation.md b/website/docs/installation.md index 4fd56db..09d3094 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -40,6 +40,13 @@ This command performs the following actions: - Generates a starter **Railcar** manifest (`railcar.json`). - Rewires your `.git/hooks` to point to the Conductor. +By default, Freight installs all supported Git hooks. You can use the `--allow` flag to specify only the hooks you want: +```bash +freight init --allow pre-commit,commit-msg +``` + +This is particularly useful for **incremental adoption**. If you already have a complex set of hooks and only want to move `pre-commit` to Freight for now, you can do so without affecting your other hooks. + :::tip Pro-Tip For total team portability, **commit the `conductor` binary** directly to your repository. This ensures that every team member (and your CI/CD pipeline) can execute hooks immediately without needing to install the `freight` CLI tool themselves. ::: From 293598dff42efd231efb502fb165470f3f2a719c Mon Sep 17 00:00:00 2001 From: David Dymko Date: Sun, 18 Jan 2026 14:11:36 -0500 Subject: [PATCH 5/5] doc: minor tweaks --- README.md | 42 ++++++--- docs/readme.md | 245 ------------------------------------------------- 2 files changed, 30 insertions(+), 257 deletions(-) delete mode 100644 docs/readme.md diff --git a/README.md b/README.md index 7586f4b..440c440 100644 --- a/README.md +++ b/README.md @@ -7,49 +7,65 @@ Read the full documentation at [freightapp.co](https://freightapp.co). -Freight streamlines Git workflows by rewiring every Git hook in your repository to a single **Conductor** binary. All logic is defined in a declarative **Railcar** manifest (`railcar.json`), ensuring your hooks are portable, fast, and easy to manage. +Freight streamlines Git workflows by rewiring every Git hook in your repository to a single **Conductor** binary. All +logic is defined in a declarative **Railcar** manifest (`railcar.json`), ensuring your hooks are portable, fast, and +easy to manage. ## Why Freight? ### 🚀 Zero Runtime Dependencies -Unlike Husky (which requires Node.js) or pre-commit (which requires Python), Freight is a single, static Go binary. Your developers don't need to install a specific runtime just to run Git hooks. + +Unlike Husky (which requires Node.js) or pre-commit (which requires Python), Freight is a single, static Go binary. Your +developers don't need to install a specific runtime just to run Git hooks. ### 📦 Unified Configuration -Manage every hook—from `pre-commit` to `post-merge`—in one `railcar.json` manifest. No more messy `.git/hooks` directory filled with ad-hoc scripts. + +Manage every hook—from `pre-commit` to `post-merge`—in one `railcar.json` manifest. No more messy `.git/hooks` directory +filled with ad-hoc scripts. ### 🛠️ Built for Portability + Freight's 'Conductor/Railcar' architecture ensures that your hooks work identically across Windows, macOS, and Linux. ### 🥊 Freight vs. Husky -| Feature | Freight | Husky | -|---------|---------|-------| -| **Runtime** | None (Static Binary) | Node.js | -| **Setup** | `freight init` | `npm install` | -| **Config** | Single JSON file | Multiple files/package.json | -| **Portability** | High (Binary included) | Moderate (Requires Node) | + +| Feature | Freight | Husky | +|-----------------|------------------------|-----------------------------| +| **Runtime** | None (Static Binary) | Node.js | +| **Setup** | `freight init` | `npm install` | +| **Config** | Single JSON file | Multiple files/package.json | +| **Portability** | High (Binary included) | Moderate (Requires Node) | --- ## Quick Start ### 1. Install + - **Homebrew (macOS):** `brew install --cask devbytes-cloud/tap/freight` -- **Precompiled Binaries:** `[GitHub Releases](https://github.com/devbytes-cloud/freight/releases)` +- **Precompiled Binaries:** [GitHub Releases](https://github.com/devbytes-cloud/freight/releases) ### 2. Setup + Run the following command in your Git repository: + ```bash freight init ``` + This installs the **Conductor** binary and creates a starter **Railcar** manifest (`railcar.json`). -By default, Freight installs all supported Git hooks. You can use the `--allow` (or `-a`) flag to specify only the hooks you want: +By default, Freight installs all supported Git hooks. You can use the `--allow` (or `-a`) flag to specify only the hooks +you want: + ```bash freight init --allow pre-commit,commit-msg ``` + Valid hooks are: `pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`, and `post-checkout`. ### 3. Verify + Add a script to your `railcar.json` and watch it run on your next commit! --- @@ -57,7 +73,9 @@ Add a script to your `railcar.json` and watch it run on your next commit! ## Architecture: Conductor & Railcar Freight operates on a simple, powerful metaphor: -- **The Conductor:** A tiny, high-performance binary placed at your repo root. It is the single entry point for all Git hooks. + +- **The Conductor:** A tiny, high-performance binary placed at your repo root. It is the single entry point for all Git + hooks. - **The Railcar:** A `railcar.json` manifest that defines exactly what the Conductor should execute for each hook. When a Git hook fires, the Conductor extracts the logic from the Railcar and executes it with precision. diff --git a/docs/readme.md b/docs/readme.md deleted file mode 100644 index cf1c190..0000000 --- a/docs/readme.md +++ /dev/null @@ -1,245 +0,0 @@ -# Freight - -## Project Overview - -Freight is a Go-based CLI tool that streamlines Git workflows by rewiring every Git hook in your repository to call a -single binary named `conductor` (placed at the repo root).`conductor` reads a JSON configuration file (`railcar.json`) -that lists the shell commands you want to run for each -hook. All logic therefore lives in one easy-to-version file instead of a dozen ad-hoc hook scripts. - ---- - -## Features - -* **One-step hook bootstrap** – `freight init` installs `conductor`, generates `railcar.json`, and rewrites every Git - hook in one go. -* **Declarative configuration** – add, remove, or reorder hook commands by editing JSON. -* **Cross-platform binaries** – pre-built for Linux, macOS, and Windows. -* **Positional-argument support** – hooks such as `commit-msg` or `pre-push` automatically pass their arguments to your - commands (via `${HOOK_INPUT}`). -* **Zero vendor lock-in** – all generated files live inside your repo; deleting them restores the default Git behaviour. - ---- - -## Installation - -### From Source - -``` -git clone https://github.com/devbytes-cloud/freight.git -cd freight -go mod tidy -make build-all -``` - ---- - -## Quick Start - -``` -# Inside any existing Git repository -freight init # installs conductor + railcar.json + rewired hooks - -# Optionally install only specific hooks -freight init --allow pre-commit,post-checkout - -git add . && git commit -m "test" # your new hooks will now fire -``` - -Need to overwrite an existing `railcar.json`? -`freight init --config-force` - ---- - -## Command-line Reference - -| Command | Description | -|----------------|---------------------------------------------| -| `freight init` | Bootstrap Freight in the current repository | -| `freight help` | Show global or command-specific help | - -Global flags: - -* `-c, --config-force` – overwrite an existing `railcar.json` -* `-a, --allow` – specific Git hooks to install (default: all). Valid options: `pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`, `post-checkout` -* `-h, --help` – display help - ---- - -## How It Works (under the hood) - -1. **Bootstrap (`freight init`)** - * Places a self-contained `conductor` binary at your repo root. - * Generates a starter `railcar.json`. - * Replaces every file in `.git/hooks/` with a tiny wrapper script that simply executes `conductor` with the hook - name and original arguments. - -2. **Hook trigger** – Git fires `pre-commit`, `commit-msg`, etc. - * The wrapper calls `conductor`. - * `conductor` loads `railcar.json`, finds the matching section, and runs each configured action. - * Any non-zero exit in a pre-hook aborts the Git operation. - ---- - -## `railcar.json` Syntax - -Hierarchical structure - -* **config** – top level -* **\** – e.g. `commit-operations`, `checkout-operations` -* **\** – e.g. `pre-commit`, `commit-msg`, `post-checkout` -* **actions array** – each item needs: - * `name` – label for readability - * `command` – shell snippet to run - -Example starter file: - -``` -{ - "config": { - "commit-operations": { - "pre-commit": [ - { "name": "echo", "command": "echo conductor is running!" } - ], - "prepare-commit-msg": [], - "commit-msg": [], - "post-commit": [] - }, - "checkout-operations": { - "post-checkout": [] - } - } -} -``` - -### Referencing Hook Arguments - -Hooks that receive parameters expose them in two interchangeable ways: - -| Placeholder | Meaning (example: `commit-msg`) | -|-----------------|---------------------------------| -| `${HOOK_INPUT}` | Alias for the parameter (`$1`) | - -Use whichever style you prefer: - -``` -{ - "commit-msg": [ - { "name": "validate", "command": "grep -E '^(feat|fix): ' ${HOOK_INPUT}" }, - { "name": "print", "command": "echo 'MSG file → ${HOOK_INPUT}'" } - ] -} -``` - -### Real-world Examples - -* Run tests before committing - -``` -{ - "pre-commit": [ - { "name": "tests", "command": "go test ./..." } - ] - } -``` - -* Enforce Conventional Commits format - -``` -{ - "commit-msg": [ - { "name": "conventional", "command": "npx commitlint --edit $1" } - ] - } -``` - -* Verify tags before pushing - -``` -{ - "pre-push": [ - { "name": "verify-tags", "command": "./scripts/check_tags.sh $@" } - ] - } -``` - -Notes - -* **Order** – actions execute sequentially in array order. -* **Shell chaining** – combine commands (`go vet ./... && go test ./...`). -* **Environment vars** – standard shell expansion works (`FOO=bar ./script.sh`). -* **Idempotency** – `freight init` never overwrites `railcar.json` unless `--config-force` is supplied. - ---- - -## Supported Git Hooks & Execution Order - -``` -Commit : pre-commit → prepare-commit-msg → commit-msg → post-commit -Merge : pre-merge-commit → post-merge -Rebase : pre-rebase → post-rewrite -Push : pre-push → update → post-update → post-receive -Checkout : pre-checkout → post-checkout -ApplyPatch : applypatch-msg → pre-applypatch → post-applypatch -``` - ---- - -## Troubleshooting - -| Issue | Fix | -|------------------------------|-----------------------------------------------------------------------------------| -| Permission denied on hooks | `chmod +x ./conductor` (and ensure hooks are executable) | -| Hook seems to do nothing | Check `.git/hooks/` – it should contain the wrapper that calls `conductor`. | -| Command not found | Ensure the command exists in `$PATH` or use an absolute path in `railcar.json`. | -| Need to debug a failing hook | Run the failing hook script manually or add `set -x` inside your action command. | - ---- - -## Contributing - -``` -git clone https://github.com/yourusername/freight.git -git checkout -b my-feature -# make changes -git commit -s -m "feat: awesome contribution" -git push origin my-feature -``` - -Open a Pull Request—thank you! - -### Build & Release - -### 1. Release & Distribution (GoReleaser + ) `go:embed` - -- The repository contains a that builds **platform-specific `conductor` binaries** for Linux, macOS and Windows (amd64, - arm, arm64). `.goreleaser.yaml` -- These binaries are dropped into `assets/dist/` and then **embedded directly into the main `freight` executable** via - Go’s mechanism (). `//go:embed``assets/embed.go` -- At runtime, `freight init` extracts the correct pre-built `conductor` for the user’s OS/CPU. -- CGO is disabled () so binaries are fully statically linked and portable. `CGO_ENABLED=0` - -### 2. Hook-Generation Template - -- Git hooks are produced from a **single script template** (`internal/githooks/gitHookTemplate`) so every generated hook - is tiny, consistent, and easy to audit. -- Unit tests in verify that every hook file is generated with the expected path and template. `githooks_test.go` - -A short note in the README can highlight the attention to reliability and test coverage. - -### 3. Testing - -- The project ships with a Go test suite () that covers hook generation, path handling, and validation helpers. - `go test ./...` -- Mentioning this encourages contributors to run tests before submitting pull requests. - -### 4. Make Targets - -- If you already have a , consider listing other useful targets (`make test`, `make lint`, etc.) so newcomers can find - them quickly. `make build-all` - ---- - -## License - -BSD-style. See `LICENSE` for full text. \ No newline at end of file