Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions scripts/setup-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOOKS_DIR="${REPO_ROOT}/.git/hooks"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"

if [[ ! -d "${REPO_ROOT}/.git" ]]; then
# 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

# 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)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid copying the hook onto itself

When core.hooksPath is configured as scripts—including through a global configuration—this resolves HOOKS_DIR to the directory already containing the source hook. The later cp scripts/pre-commit scripts/pre-commit exits with “are the same file” under set -e, so setup fails for a valid hooks-path configuration that this change intends to support. Detect an identical source and destination and only ensure the existing hook is executable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bcda06e. When the source hook and the destination are the same file (-ef), the script does not copy and only runs chmod +x. A new test case sets core.hooksPath=scripts; it fails on 63b1808 and passes on bcda06e.

if [[ "${HOOKS_DIR}" != /* ]]; then
HOOKS_DIR="${REPO_ROOT}/${HOOKS_DIR}"
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)
Expand Down
120 changes: 120 additions & 0 deletions tests/test-setup-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/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
# - 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

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

# 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 ]]
Loading