diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..c30e5a9 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/README.md b/README.md index 09ed486..0744d91 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ hooks. = { args = [ ]; # extra args passed before matched filenames files = ".*"; # ERE regex (bash [[ =~ ]]) tested against each candidate path exclude = "^$"; # ERE regex; "^$" (default) excludes nothing - stages = [ "pre-commit" ]; # subset of [ "pre-commit" "pre-push" ] + stages = [ "pre-commit" ]; # subset of [ "pre-commit" "pre-push" "commit-msg" ] pass_filenames = true; # append matched files as trailing args always_run = false; # run once with no file filtering/passing path = [ ]; # packages whose bin/ dirs are prepended to PATH for entry @@ -83,6 +83,11 @@ hooks. = { (e.g. `cargo` finding `cargo-clippy`) -- `entry` itself is always invoked by absolute path regardless of `path`. +A `commit-msg`-staged hook receives the path to the temporary file +containing the commit message as its sole file. `files`/`exclude`/ +`pass_filenames` apply to that single path the same way they apply to real +filenames in `pre-commit`/`pre-push`. + ## Presets `presets` is a small built-in catalog of common tool configs -- plain `hooks.`-shaped attrsets you can reference directly or override: @@ -99,9 +104,14 @@ nixhooks.mkHooks { See [lib/presets.nix](./lib/presets.nix) for the full list of presets. ## CI/CD -### Tangled pipelines -`mkHooks` can generate a Tangled Spindle pipeline: +nixhooks can also autogenerate CI workflows based on your selected hooks. + +Hooks staged only for `commit-msg` (e.g. the `commitlint` preset) are +excluded from the generated CI pipeline's `run-hooks` invocation as CI has +no commit message to check. A hook declaring multiple stages including +`commit-msg` still runs in CI for its other stage(s). +### Tangled pipelines ```nix nixhooks.mkHooks { hooks = { /* ... */ }; @@ -130,8 +140,6 @@ This writes a `.tangled/workflows/hooks.yml` in the same manner as `shellHook` like `install-hooks`, so you'll need to trigger it manually. ### GitHub Actions -`mkHooks` can generate a GitHub Actions workflow, the same way: - ```nix nixhooks.mkHooks { hooks = { /* ... */ }; diff --git a/flake.nix b/flake.nix index 5289f8c..bfced9b 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,7 @@ nixhooksLib = import ./default.nix {inherit pkgs;}; hooks = nixhooksLib.mkHooks { hooks = { + inherit (nixhooksLib.presets) commitlint; shellcheck = nixhooksLib.presets.shellcheck // {stages = ["pre-push"];}; alejandra = nixhooksLib.presets.alejandra // {stages = ["pre-push"];}; bats = nixhooksLib.presets.bats // {stages = ["pre-push"];}; diff --git a/lib/default.nix b/lib/default.nix index 4b68f37..4f974bf 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -10,6 +10,7 @@ runtimeLib = builtins.readFile ./hooks-runtime.sh; driverPreCommit = builtins.readFile ./drivers/pre-commit.sh; driverPrePush = builtins.readFile ./drivers/pre-push.sh; + driverCommitMsg = builtins.readFile ./drivers/commit-msg.sh; driverRunHooks = builtins.readFile ./drivers/run-hooks.sh; # lets install-hooks tell a nixhooks-installed hook and other hooks apart @@ -52,6 +53,9 @@ prePushHook = mkStageScript "pre-push-hook" driverPrePush ( scriptGen.genStageCalls "pre-push" normalized ); + commitMsgHook = mkStageScript "commit-msg-hook" driverCommitMsg ( + scriptGen.genStageCalls "commit-msg" normalized + ); runHooks = mkStageScript "run-hooks" driverRunHooks (scriptGen.genAllCalls normalized); installHooks = pkgs.writeShellScriptBin "install-hooks" '' @@ -79,6 +83,7 @@ install_one "pre-commit" "${preCommitHook}/bin/pre-commit-hook" install_one "pre-push" "${prePushHook}/bin/pre-push-hook" + install_one "commit-msg" "${commitMsgHook}/bin/commit-msg-hook" ''; tangledPipeline = pkgs.writeText "hooks.yml" '' @@ -132,6 +137,7 @@ { pre-commit-hook = preCommitHook; pre-push-hook = prePushHook; + commit-msg-hook = commitMsgHook; run-hooks = runHooks; install-hooks = installHooks; } diff --git a/lib/drivers/commit-msg.sh b/lib/drivers/commit-msg.sh new file mode 100644 index 0000000..5e19423 --- /dev/null +++ b/lib/drivers/commit-msg.sh @@ -0,0 +1,5 @@ +# shellcheck shell=bash + +nixhooks_collect_commitmsg_files "$1" +nixhooks_generated_hooks +nixhooks_summary diff --git a/lib/hook-spec.nix b/lib/hook-spec.nix index bf1151c..478fd86 100644 --- a/lib/hook-spec.nix +++ b/lib/hook-spec.nix @@ -13,6 +13,7 @@ validStages = [ "pre-commit" "pre-push" + "commit-msg" ]; # names are passed through as shell-escaped string arguments but must exclude ',' and whitespace/control characters diff --git a/lib/hooks-runtime.sh b/lib/hooks-runtime.sh index 5477273..05f668f 100644 --- a/lib/hooks-runtime.sh +++ b/lib/hooks-runtime.sh @@ -87,6 +87,10 @@ nixhooks_collect_repo_files() { done < <(git ls-files -z) } +nixhooks_collect_commitmsg_files() { + NIXHOOKS_FILES=("$1") +} + nixhooks_collect_prepush_files() { local remote_sha="$1" local_sha="$2" NIXHOOKS_FILES=() diff --git a/lib/presets.nix b/lib/presets.nix index a28c21d..e0b1c57 100644 --- a/lib/presets.nix +++ b/lib/presets.nix @@ -34,6 +34,12 @@ files = "\\.(md|txt)$"; }; + commitlint = { + entry = "${pkgs.commitlint}/bin/commitlint"; + args = ["--edit"]; + stages = ["commit-msg"]; + }; + shfmt = { entry = "${pkgs.shfmt}/bin/shfmt"; args = ["-d"]; diff --git a/lib/script-gen.nix b/lib/script-gen.nix index da2aacf..fb50e15 100644 --- a/lib/script-gen.nix +++ b/lib/script-gen.nix @@ -28,6 +28,7 @@ in { # calls for a single stage genStageCalls = stage: hooks: genCalls (hooksForStage stage hooks); - # calls for every enabled hook regardless of stage, - genAllCalls = genCalls; + # calls for CI, commit-msg hooks are filtered out + genAllCalls = hooks: + genCalls (lib.filterAttrs (_: h: builtins.any (s: s != "commit-msg") h.stages) hooks); } diff --git a/tests/hooks-runtime.bats b/tests/hooks-runtime.bats index 1c8c5b4..3643a10 100644 --- a/tests/hooks-runtime.bats +++ b/tests/hooks-runtime.bats @@ -1,8 +1,4 @@ -# Unit tests for lib/hooks-runtime.sh -- the hand-written, non-generated -# bash that carries the actual git-protocol correctness (no-HEAD, zero-sha, -# file matching). The Nix-generated glue (script-gen.nix, install-hooks) is -# exercised by building the derivations instead; that's a much lower-risk -# surface (string templating) and isn't duplicated here. +# Unit tests for lib/hooks-runtime.sh setup() { NIXHOOKS_LIB="${NIXHOOKS_LIB:-$BATS_TEST_DIRNAME/../lib}" @@ -30,7 +26,7 @@ teardown() { rm -rf "$TEST_REPO" } -# --- run_hook: file matching / exclude / always_run / pass_filenames ------ +# run_hook: file matching / exclude / always_run / pass_filenames ------ @test "run_hook passes only files matching 'files' to the entry" { NIXHOOKS_FILES=("a.sh" "b.txt") @@ -70,7 +66,15 @@ teardown() { [ -z "$(cat "$RECORD_FILE")" ] } -# --- run_hook: path_prefix ------------------------------------------------- +@test "run_hook appends the collected commit-msg file as a trailing arg" { + NIXHOOKS_FILES=("/tmp/some/COMMIT_EDITMSG") + run_hook commitlint '.*' '^$' 1 0 "$RECORDER" '' --edit + run cat "$RECORD_FILE" + [[ "$output" == *"--edit"* ]] + [[ "$output" == *"/tmp/some/COMMIT_EDITMSG"* ]] +} + +# run_hook: path_prefix @test "run_hook prepends path_prefix to PATH before invoking the entry" { local extra_bin_dir="$TEST_REPO/extra_bin" @@ -108,7 +112,7 @@ EOF [ "$NIXHOOKS_FAILED" -eq 1 ] } -# --- run_hook: SKIP and failure aggregation ------------------------------- +# run_hook: SKIP and failure aggregation @test "run_hook honors SKIP and never invokes the entry" { _nixhooks_skip=("myhook") @@ -143,7 +147,7 @@ EOF [ "$status" -eq 1 ] } -# --- file discovery -------------------------------------------------------- +# file discovery @test "precommit file collection works with no HEAD yet (initial commit)" { echo hi >a.txt @@ -173,6 +177,14 @@ EOF [ "${#NIXHOOKS_FILES[@]}" -eq 2 ] } +@test "commitmsg file collection sets NIXHOOKS_FILES to the given message file path" { + local msg_file="$TEST_REPO/COMMIT_EDITMSG" + echo "feat: add thing" >"$msg_file" + nixhooks_collect_commitmsg_files "$msg_file" + [ "${#NIXHOOKS_FILES[@]}" -eq 1 ] + [ "${NIXHOOKS_FILES[0]}" = "$msg_file" ] +} + @test "prepush file collection falls back to the whole repo for a new branch (zero remote sha)" { echo hi >a.txt git add a.txt