Files
Ethan be686a8d0d fix(scripts/githooks): clear all repo-local Git env vars in hooks (#24138)
## Problem

In linked worktrees, Git hooks inherit multiple repo-local environment
variables: `GIT_DIR`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, and others.
The
pre-commit and pre-push hooks only unset `GIT_DIR`, leaving the rest in
place.

When `make pre-commit` runs `go build`, Go tries to stamp VCS info by
shelling
out to `git`. With the leftover partial Git environment, `git` exits 128
and
the build fails:

```
error obtaining VCS status: exit status 128
    Use -buildvcs=false to disable VCS stamping.
```

This only happens inside hooks in a linked worktree — running `make
pre-commit`
directly from the terminal works fine because the repo-local vars are
not set.

## Fix

Replace the bare `unset GIT_DIR` in both hooks with a loop that clears
every
variable reported by `git rev-parse --local-env-vars`:

```sh
while IFS= read -r var; do
    unset "$var"
done < <(git rev-parse --local-env-vars)
```

This covers all 15 repo-local variables Git may inject (`GIT_DIR`,
`GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, etc.) and is
forward-compatible — if Git adds new local vars in the future, the loop
picks
them up automatically.
2026-04-09 01:06:12 +10:00

121 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Pre-push hook that runs tests and builds the site locally.
# Classifies changed files (vs remote branch or merge-base)
# and only runs relevant test targets. Falls back to the full
# `make pre-push` when the Makefile changed, the diff range
# can't be determined, or CODER_HOOK_RUN_ALL=1 is set.
#
# The pre-commit hook handles gen, fmt, lint, typos, and build.
#
# Opt in/out without modifying this file:
#
# git config coder.pre-push true # opt in
# git config coder.pre-push false # opt out (overrides allowlist)
# git config --unset coder.pre-push # default (allowlist decides)
#
# Installation (worktree-compatible):
#
# git config core.hooksPath scripts/githooks
#
# Bypass: git push --no-verify
set -euo pipefail
# Allowlist of developers who opt in to pre-push checks by default.
# Matched against CODER_WORKSPACE_OWNER_NAME.
ALLOWLIST=(
mafredri
johnstcn
)
cd "$(git rev-parse --show-toplevel)"
# Unset all repo-local Git env vars, not just GIT_DIR. In linked
# worktrees the hook inherits variables like GIT_COMMON_DIR and
# GIT_INDEX_FILE that confuse child processes (notably Go's VCS
# stamping, which shells out to git and gets exit status 128).
# Process substitution (not a pipe) so unset runs in the current shell.
while IFS= read -r var; do
unset "$var"
done < <(git rev-parse --local-env-vars)
# In linked worktrees, set worktree-scoped hooksPath to override shared config.
if [[ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]]; then
git config --worktree core.hooksPath scripts/githooks
fi
# Drain stdin before any early exits so git doesn't see a
# broken pipe. The push refs are used later for classification.
push_refs=()
while read -r local_ref local_oid remote_ref remote_oid; do
push_refs+=("$local_ref $local_oid $remote_ref $remote_oid")
done
# Explicit opt-in/opt-out via git config (overrides allowlist).
run=false
opt_in=$(git config --type=bool coder.pre-push 2>/dev/null || true)
if [[ $opt_in == true ]]; then
run=true
elif [[ $opt_in == false ]]; then
# Explicit opt-out, skip everything including hint.
exit 0
fi
# Check allowlist.
if ! $run; then
owner=${CODER_WORKSPACE_OWNER_NAME:-}
for allowed in "${ALLOWLIST[@]}"; do
if [[ $owner == "$allowed" ]]; then
run=true
break
fi
done
fi
rc=0
if $run; then
if [[ ${CODER_HOOK_RUN_ALL:-} == 1 ]]; then
make pre-push || rc=$?
else
# Determine changed files from push refs.
zero="0000000000000000000000000000000000000000"
changed=""
fallback=false
for entry in "${push_refs[@]}"; do
read -r _local_ref local_oid _remote_ref remote_oid <<< "$entry"
if [[ $local_oid == "$zero" ]]; then
continue
fi
if [[ $remote_oid == "$zero" ]]; then
base=$(git merge-base "$local_oid" origin/main 2>/dev/null || true)
if [[ -z $base ]]; then
fallback=true
break
fi
else
base="$remote_oid"
fi
files=$(git diff --name-only "$base" "$local_oid" 2>/dev/null || true)
if [[ -n $files ]]; then
changed+=$'\n'"$files"
fi
done
if $fallback || [[ -z $changed ]]; then
make pre-push || rc=$?
elif echo "$changed" | grep -qE '\.(go|ts|tsx|sql|proto)$|^go\.(mod|sum)$|^site/|^Makefile$'; then
make pre-push || rc=$?
else
echo "pre-push: no Go/TS changes, skipping tests"
fi
fi
fi
# Hint is printed unconditionally so that AI agents that are not
# opted in to pre-push checks are still reminded to monitor CI.
echo "Hint: If you're an AI agent creating a PR, monitor CI and iterate on failures until all checks pass."
exit "$rc"