From 0fc39faa90f97db24043017a845f1754b4bb8b84 Mon Sep 17 00:00:00 2001 From: sommerfeld Date: Tue, 19 May 2026 16:50:52 +0100 Subject: feat(git): per-clone hook override at .git/hooks-local/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an untracked per-clone override layer to the hook dispatcher. Lookup order is now: 1. /hooks-local/ — untracked, per-clone, ignored by git 2. /.githooks/ — tracked, shared with teammates Use case: a shared repo ships a .githooks/pre-commit you want to locally replace without modifying the tracked file. Drop your hook in .git/hooks-local/ (chmod +x) and the dispatcher will run it instead — the global commit-msg trailer-strip and pre-push gate still run on top. If neither override exists, only the global user-level logic runs. --- dot_config/git/hooks/_dispatch.sh | 44 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) (limited to 'dot_config') diff --git a/dot_config/git/hooks/_dispatch.sh b/dot_config/git/hooks/_dispatch.sh index 7dcd89c..0b1135d 100644 --- a/dot_config/git/hooks/_dispatch.sh +++ b/dot_config/git/hooks/_dispatch.sh @@ -1,16 +1,26 @@ #!/bin/sh -# Sourced by every hook in this directory. Runs the per-repo hook of the -# same name from `/.githooks/` if it exists, then returns -# control so the calling user-level hook can do its own work after. +# Sourced by every hook in this directory. Runs the per-repo hook of +# the same name and then returns control so the calling user-level +# hook can do its own work after. # -# Repos opt in by just dropping `.githooks/` (executable) in -# the working tree — no per-repo `core.hooksPath` setting, no stubs. -# If the per-repo hook exits non-zero we abort with that status so git -# sees the failure. +# Lookup order (first executable file wins): +# 1. `/hooks-local/` — untracked per-clone override, +# lives inside .git/ so it stays personal. Use this when you want +# to replace a tracked .githooks/ on a shared repo without +# affecting teammates. Create with e.g.: +# $ mkdir -p "$(git rev-parse --git-dir)/hooks-local" +# $ $EDITOR "$(git rev-parse --git-dir)/hooks-local/pre-commit" +# $ chmod +x "$(git rev-parse --git-dir)/hooks-local/pre-commit" +# 2. `/.githooks/` — tracked, the project's +# shared hook. The intended default for opting a repo in. +# +# Either an empty file with exit 0 or no hook at all means "skip the +# project layer entirely"; the user-level hook still runs its own +# global logic afterwards. # # GIT_HOOK_DISPATCHED guards against re-entry: if some legacy repo has # its own `.githooks/` that ends with `exec "$HOME/.config/..."` -# (the old pattern), we won't dispatch back into it a second time. +# (the old stub pattern), we won't dispatch back into it a second time. # shellcheck shell=sh dispatch_repo_hook() { @@ -19,13 +29,17 @@ dispatch_repo_hook() { [ -n "${GIT_HOOK_DISPATCHED:-}" ] && return 0 + gitdir=$(git rev-parse --git-dir 2>/dev/null) || return 0 root=$(git rev-parse --show-toplevel 2>/dev/null) || return 0 - repo_hook="$root/.githooks/$hookname" - [ -x "$repo_hook" ] || return 0 - GIT_HOOK_DISPATCHED=1 "$repo_hook" "$@" - rc=$? - if [ "$rc" -ne 0 ]; then - exit "$rc" - fi + for candidate in "$gitdir/hooks-local/$hookname" "$root/.githooks/$hookname"; do + if [ -x "$candidate" ]; then + GIT_HOOK_DISPATCHED=1 "$candidate" "$@" + rc=$? + if [ "$rc" -ne 0 ]; then + exit "$rc" + fi + return 0 + fi + done } -- cgit v1.3.1