From 63b1808ba2cded27e789599b68ee145dfd031d33 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:11:01 +0000 Subject: [PATCH 1/2] fix(setup-hooks): work in a linked git worktree (#137) setup-hooks.sh checked for a .git directory and wrote the hook to /.git/hooks. In a linked worktree, .git is a file, so the script printed "not a git repository" and exited 1. Git also does not read hooks from that path in a linked worktree. The script now uses git rev-parse --git-dir to find the repository and git rev-parse --git-path hooks to find the hooks directory that git reads. That path is in the common git directory for a linked worktree, and it obeys core.hooksPath. tests/test-setup-hooks.sh makes a temporary repository with a linked worktree and runs the script in both. It fails on main and passes with this fix. Crew: Drew (Kiro Crew Issue Radar) --- scripts/setup-hooks.sh | 11 ++++- tests/test-setup-hooks.sh | 85 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100755 tests/test-setup-hooks.sh diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index 41db69f..3df5a1f 100755 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -5,13 +5,20 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -HOOKS_DIR="${REPO_ROOT}/.git/hooks" -if [[ ! -d "${REPO_ROOT}/.git" ]]; then +# In a linked worktree, .git is a file, so ask git for the git directory. +if ! git -C "${REPO_ROOT}" rev-parse --git-dir >/dev/null 2>&1; then echo "Error: not a git repository. Run from the repo root." >&2 exit 1 fi +# Use the hooks directory that git reads. In a linked worktree this is in the +# common git directory. It also obeys core.hooksPath. +HOOKS_DIR="$(git -C "${REPO_ROOT}" rev-parse --git-path hooks)" +if [[ "${HOOKS_DIR}" != /* ]]; then + HOOKS_DIR="${REPO_ROOT}/${HOOKS_DIR}" +fi + mkdir -p "${HOOKS_DIR}" # Install pre-commit hook diff --git a/tests/test-setup-hooks.sh b/tests/test-setup-hooks.sh new file mode 100755 index 0000000..c366d60 --- /dev/null +++ b/tests/test-setup-hooks.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# tests/test-setup-hooks.sh — Test for scripts/setup-hooks.sh. +# +# In a linked worktree (git worktree add), .git is a file, not a directory, +# and git reads hooks from the common git directory. This test makes a +# temporary repository with a linked worktree and makes sure that: +# - setup-hooks.sh exits 0 in the main checkout and in the linked worktree +# - the pre-commit hook is in the hooks directory that git uses +# - setup-hooks.sh exits 1 outside a git repository +# +# No AWS required. Run standalone or via CI (discovered by pack-tests.yml). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; BOLD='\033[1m'; NC='\033[0m' +PASS=0; FAIL=0 + +pass() { printf "${GREEN} \xE2\x9C\x93${NC} %s\n" "$1"; PASS=$((PASS + 1)); } +fail() { printf "${RED} \xE2\x9C\x97${NC} %s\n" "$1"; FAIL=$((FAIL + 1)); } + +printf "${BOLD}scripts/setup-hooks.sh${NC}\n" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# Isolate from the user's git configuration. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 +G=(git -c user.name=test -c user.email=test@example.invalid -c init.defaultBranch=main) + +MAIN="$WORK/main" +mkdir -p "$MAIN/scripts" +cp "$REPO_ROOT/scripts/setup-hooks.sh" "$REPO_ROOT/scripts/pre-commit" "$MAIN/scripts/" +"${G[@]}" -C "$MAIN" init -q +"${G[@]}" -C "$MAIN" add scripts +"${G[@]}" -C "$MAIN" commit -q -m init +"${G[@]}" -C "$MAIN" worktree add -q "$WORK/linked" -b linked + +# Main checkout. +if bash "$MAIN/scripts/setup-hooks.sh" >/dev/null 2>&1; then + pass "exits 0 in the main checkout" +else + fail "exits non-zero in the main checkout" +fi +if [[ -x "$MAIN/.git/hooks/pre-commit" ]]; then + pass "pre-commit hook installed in .git/hooks" +else + fail "pre-commit hook missing from .git/hooks" +fi + +# Linked worktree: remove the hook first, so the check below proves this run. +rm -f "$MAIN/.git/hooks/pre-commit" +if bash "$WORK/linked/scripts/setup-hooks.sh" >/dev/null 2>&1; then + pass "exits 0 in a linked worktree" +else + fail "exits non-zero in a linked worktree" +fi +hooks_dir="$(git -C "$WORK/linked" rev-parse --git-path hooks)" +[[ "$hooks_dir" = /* ]] || hooks_dir="$WORK/linked/$hooks_dir" +if [[ -x "$hooks_dir/pre-commit" ]]; then + pass "pre-commit hook installed in the hooks directory git uses" +else + fail "pre-commit hook missing from $hooks_dir" +fi +if [[ ! -e "$WORK/linked/.git/hooks" ]]; then + pass "no hooks directory created under the worktree .git file" +else + fail "hooks directory created under the worktree .git file" +fi + +# Not a git repository. +NOREPO="$WORK/norepo" +mkdir -p "$NOREPO/scripts" +cp "$REPO_ROOT/scripts/setup-hooks.sh" "$NOREPO/scripts/" +rc=0 +GIT_CEILING_DIRECTORIES="$WORK" bash "$NOREPO/scripts/setup-hooks.sh" >/dev/null 2>&1 || rc=$? +if [[ $rc -eq 1 ]]; then + pass "exits 1 outside a git repository" +else + fail "exits $rc outside a git repository (expected 1)" +fi + +printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" +[[ $FAIL -eq 0 ]] From bcda06ea2dead121eaaeb8d6fe5d4d9e0bac6f31 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:15:58 +0000 Subject: [PATCH 2/2] fix(setup-hooks): reject ancestor repos and allow hooksPath=scripts Make sure that the git worktree root is the repository root. If git finds only an ancestor repository, stop with exit 1 and do not install a hook into it. If core.hooksPath points to scripts/, do not copy the hook onto itself. Add tests for both cases. Crew: Drew (Kiro Crew Issue Radar) --- scripts/setup-hooks.sh | 19 +++++++++++++------ tests/test-setup-hooks.sh | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index 3df5a1f..3605182 100755 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -4,10 +4,14 @@ set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -# In a linked worktree, .git is a file, so ask git for the git directory. -if ! git -C "${REPO_ROOT}" rev-parse --git-dir >/dev/null 2>&1; then +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + +# In a linked worktree, .git is a file, so ask git for the worktree root. +# The root must be REPO_ROOT. If git finds only an ancestor repository (for +# example, a copy of this repo inside an unrelated checkout), do not install +# the hook into that repository. +TOPLEVEL="$(git -C "${REPO_ROOT}" rev-parse --show-toplevel 2>/dev/null || true)" +if [[ -z "${TOPLEVEL}" ]] || [[ "$(cd "${TOPLEVEL}" && pwd -P)" != "${REPO_ROOT}" ]]; then echo "Error: not a git repository. Run from the repo root." >&2 exit 1 fi @@ -21,8 +25,11 @@ fi mkdir -p "${HOOKS_DIR}" -# Install pre-commit hook -cp "${REPO_ROOT}/scripts/pre-commit" "${HOOKS_DIR}/pre-commit" +# Install pre-commit hook. If core.hooksPath points to scripts/, the hook is +# already in place, and cp would fail because the files are the same. +if [[ ! "${REPO_ROOT}/scripts/pre-commit" -ef "${HOOKS_DIR}/pre-commit" ]]; then + cp "${REPO_ROOT}/scripts/pre-commit" "${HOOKS_DIR}/pre-commit" +fi chmod +x "${HOOKS_DIR}/pre-commit" # Install git-secrets hooks (if git-secrets is available) diff --git a/tests/test-setup-hooks.sh b/tests/test-setup-hooks.sh index c366d60..aba3770 100755 --- a/tests/test-setup-hooks.sh +++ b/tests/test-setup-hooks.sh @@ -7,6 +7,9 @@ # - setup-hooks.sh exits 0 in the main checkout and in the linked worktree # - the pre-commit hook is in the hooks directory that git uses # - setup-hooks.sh exits 1 outside a git repository +# - setup-hooks.sh exits 1 when git finds only an ancestor repository, and +# installs no hook into that repository +# - setup-hooks.sh exits 0 when core.hooksPath points to scripts/ # # No AWS required. Run standalone or via CI (discovered by pack-tests.yml). set -euo pipefail @@ -81,5 +84,37 @@ else fail "exits $rc outside a git repository (expected 1)" fi +# Copy inside an unrelated repository: git finds only the ancestor. +OUTER="$WORK/outer" +"${G[@]}" init -q "$OUTER" +mkdir -p "$OUTER/vendor/lowkey/scripts" +cp "$REPO_ROOT/scripts/setup-hooks.sh" "$REPO_ROOT/scripts/pre-commit" "$OUTER/vendor/lowkey/scripts/" +rc=0 +bash "$OUTER/vendor/lowkey/scripts/setup-hooks.sh" >/dev/null 2>&1 || rc=$? +if [[ $rc -eq 1 ]]; then + pass "exits 1 when git finds only an ancestor repository" +else + fail "exits $rc when git finds only an ancestor repository (expected 1)" +fi +if [[ ! -e "$OUTER/.git/hooks/pre-commit" ]]; then + pass "no hook installed into the ancestor repository" +else + fail "hook installed into the ancestor repository" +fi + +# core.hooksPath points to the directory that holds the source hook. +"${G[@]}" -C "$MAIN" config core.hooksPath scripts +if bash "$MAIN/scripts/setup-hooks.sh" >/dev/null 2>&1; then + pass "exits 0 when core.hooksPath is scripts" +else + fail "exits non-zero when core.hooksPath is scripts" +fi +if [[ -x "$MAIN/scripts/pre-commit" ]]; then + pass "pre-commit hook in scripts/ is executable" +else + fail "pre-commit hook in scripts/ is not executable" +fi +"${G[@]}" -C "$MAIN" config --unset core.hooksPath + printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" [[ $FAIL -eq 0 ]]