Created
April 2, 2026 23:20
-
-
Save ivy/36674310695d5d078a2ca9a25324113d to your computer and use it in GitHub Desktop.
Reproduce a bug in lefthook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # reproduce-bug.sh | |
| # | |
| # Reproduces the bug where `lefthook run` auto-installs hooks into the | |
| # directory returned by `git rev-parse --git-path hooks` without checking | |
| # whether `core.hooksPath` is set, silently overwriting global hooks. | |
| # | |
| # Runs entirely inside a throwaway Docker container. | |
| [[ -n "${DEBUG:-}" ]] && set -o xtrace | |
| set -o errexit | |
| set -o errtrace | |
| set -o nounset | |
| set -o pipefail | |
| readonly IMAGE_TAG="lefthook-bug-repro" | |
| readonly CONTAINER_NAME="lefthook-bug-repro-run" | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| readonly SCRIPT_DIR | |
| # --------------------------------------------------------------------------- | |
| # Cleanup | |
| # --------------------------------------------------------------------------- | |
| cleanup() { | |
| echo "" | |
| echo "--- Cleaning up Docker resources ---" | |
| docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true | |
| docker rmi --force "${IMAGE_TAG}" 2>/dev/null || true | |
| } | |
| trap cleanup EXIT | |
| # --------------------------------------------------------------------------- | |
| # Build the Docker image | |
| # --------------------------------------------------------------------------- | |
| build_image() { | |
| echo "--- Building Docker image ${IMAGE_TAG} ---" | |
| docker build \ | |
| --file - \ | |
| --tag "${IMAGE_TAG}" \ | |
| "${SCRIPT_DIR}" \ | |
| <<'DOCKERFILE' | |
| FROM golang:1.26 | |
| WORKDIR /src | |
| COPY . . | |
| RUN go build -o /usr/local/bin/lefthook . | |
| RUN git config --global init.defaultBranch main | |
| DOCKERFILE | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Run the reproduction inside a container | |
| # --------------------------------------------------------------------------- | |
| run_reproduction() { | |
| echo "" | |
| echo "--- Running reproduction inside container ---" | |
| docker run \ | |
| --name "${CONTAINER_NAME}" \ | |
| --rm \ | |
| "${IMAGE_TAG}" \ | |
| bash -c ' | |
| set -o errexit | |
| set -o nounset | |
| set -o pipefail | |
| GLOBAL_HOOKS_DIR="/root/.config/git/hooks" | |
| GLOBAL_PREPUSH="${GLOBAL_HOOKS_DIR}/pre-push" | |
| MARKER="global pre-push hook" | |
| # --------------------------------------------------------------- | |
| # Step 1: Set up a global hooks directory with a custom pre-push hook | |
| # --------------------------------------------------------------- | |
| echo ">>> Step 1: Setting up global hooks directory with pre-push hook" | |
| mkdir -p "${GLOBAL_HOOKS_DIR}" | |
| cat > "${GLOBAL_PREPUSH}" << HOOK | |
| #!/bin/sh | |
| echo "${MARKER}" | |
| HOOK | |
| chmod +x "${GLOBAL_PREPUSH}" | |
| git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}" | |
| echo " Global pre-push hook contents before test:" | |
| cat "${GLOBAL_PREPUSH}" | |
| echo "" | |
| # --------------------------------------------------------------- | |
| # Step 2: Create a test repo with lefthook config (pre-commit only) | |
| # --------------------------------------------------------------- | |
| echo ">>> Step 2: Creating test repo with lefthook config" | |
| cd /tmp && git init test-repo && cd test-repo | |
| git config user.email "test@test.com" | |
| git config user.name "Test" | |
| cat > lefthook.yml << EOF | |
| pre-commit: | |
| commands: | |
| hello: | |
| run: echo "lefthook" | |
| EOF | |
| # --------------------------------------------------------------- | |
| # Step 3: Force-install lefthook (user explicitly opts in) | |
| # --------------------------------------------------------------- | |
| # This installs a lefthook dispatcher for pre-commit into the global | |
| # hooks directory. The pre-push hook is untouched. | |
| echo "" | |
| echo ">>> Step 3: Running lefthook install --force" | |
| lefthook install --force 2>&1 || true | |
| echo "" | |
| echo " Global pre-push after install:" | |
| cat "${GLOBAL_PREPUSH}" | |
| echo "" | |
| # Make an initial commit to establish a checksum baseline. | |
| git add . | |
| git commit -m "initial commit" 2>&1 || true | |
| # --------------------------------------------------------------- | |
| # Step 4: Add pre-push to lefthook config (creates checksum mismatch) | |
| # --------------------------------------------------------------- | |
| # Now the user adds a pre-push hook to lefthook.yml. On the next commit, | |
| # syncHooks detects the config change and calls createHooksIfNeeded, | |
| # which writes a pre-push dispatcher into the global hooks directory, | |
| # overwriting the custom pre-push hook. | |
| echo "" | |
| echo ">>> Step 4: Adding pre-push to lefthook config + committing" | |
| # Ensure the config file timestamp changes (checkHooksSynchronized | |
| # compares Unix seconds, and Docker can run fast enough to stay | |
| # within the same second). | |
| sleep 1 | |
| cat >> lefthook.yml << EOF | |
| pre-push: | |
| commands: | |
| check: | |
| run: echo "push check" | |
| EOF | |
| git add . | |
| git commit -m "add pre-push hook" 2>&1 || true | |
| # --------------------------------------------------------------- | |
| # Step 5: Check whether the global pre-push hook was overwritten | |
| # --------------------------------------------------------------- | |
| echo "" | |
| echo ">>> Step 5: Checking global pre-push hook" | |
| echo " Global pre-push contents after test:" | |
| cat "${GLOBAL_PREPUSH}" | |
| echo "" | |
| if grep -q "${MARKER}" "${GLOBAL_PREPUSH}"; then | |
| echo "" | |
| echo "============================================" | |
| echo " PASS - Global pre-push was NOT overwritten." | |
| echo " The bug is not present (or has been fixed)." | |
| echo "============================================" | |
| exit 0 | |
| else | |
| echo "" | |
| echo "============================================" | |
| echo " FAIL - Global pre-push WAS overwritten!" | |
| echo " syncHooks wrote a lefthook dispatcher into" | |
| echo " the core.hooksPath directory, clobbering" | |
| echo " the custom pre-push hook." | |
| echo "============================================" | |
| exit 1 | |
| fi | |
| ' | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| main() { | |
| build_image | |
| run_reproduction | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment