diff --git a/.claude/hooks/pre-pr-check.sh b/.claude/hooks/pre-pr-check.sh index e7f073add2..c449a0d1c6 100755 --- a/.claude/hooks/pre-pr-check.sh +++ b/.claude/hooks/pre-pr-check.sh @@ -13,21 +13,52 @@ if [[ "$COMMAND" != *"gh pr create"* ]]; then exit 0 fi -# Determine base branch (default: main) -BASE="main" -if echo "$COMMAND" | grep -qoP '(?<=--base\s)\S+'; then - BASE=$(echo "$COMMAND" | grep -oP '(?<=--base\s)\S+') +# Parse the command as one line, with backslash continuations joined. Quoted strings (the --title and --body +# values, including a heredoc inside "$(...)") and a bare heredoc are dropped before reading flags, so text inside +# them cannot match. --base/--head values are unquoted first so they survive. +ONE_LINE=$(printf '%s\n' "$COMMAND" | sed -e ':a' -e '/\\$/N' -e 's/\\\n//' -e 'ta' | tr '\n' ' ') +FLAGS=$(printf '%s\n' "$ONE_LINE" | sed -E "s/((--base|-B|--head|-H)[[:space:]=]+)\"([^\"]*)\"/\1\3/g; s/((--base|-B|--head|-H)[[:space:]=]+)'([^']*)'/\1\3/g; s/\"[^\"]*\"//g; s/'[^']*'//g; s/<<.*//") + +# Only a command that runs gh pr create, not one that mentions it (a commit message, a grep) +RUNS_PR_CREATE='(^|[;&|(])[[:space:]]*gh[[:space:]]+pr[[:space:]]+create' +if ! [[ "$FLAGS" =~ $RUNS_PR_CREATE ]]; then + exit 0 +fi + +# Check the repo the command runs in: the target of the last `cd` before gh pr create, else the session's cwd. +# A relative target is resolved against the session's cwd. +CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) +BEFORE="; ${ONE_LINE%%gh pr create*}" +REPO_DIR=$(printf '%s\n' "$BEFORE" | sed -nE "s/.*[;&|][[:space:]]*cd[[:space:]]+(\"([^\"]+)\"|'([^']+)'|([^;&|[:space:]]+)).*/\2\3\4/p") +REPO_DIR="${REPO_DIR/#\~/$HOME}" +if [[ -n "$REPO_DIR" && "$REPO_DIR" != /* && -n "$CWD" ]]; then + REPO_DIR="$CWD/$REPO_DIR" +fi +REPO_DIR="${REPO_DIR:-${CWD:-.}}" + +# Determine base branch (default: main) and the PR's head branch (default: the checked-out one). gh uses the last +# value of a repeated flag, and --head may be owner:branch. grep -oE rather than grep -P, which macOS grep lacks. +BASE=$(printf '%s\n' "$FLAGS" | grep -oE -- '(^|[[:space:]])(--base|-B)[[:space:]=]+[^[:space:];&|)]+' | tail -n1 | sed -E 's/^[[:space:]]*(--base|-B)[[:space:]=]+//') +BASE="${BASE:-main}" +HEAD_BRANCH=$(printf '%s\n' "$FLAGS" | grep -oE -- '(^|[[:space:]])(--head|-H)[[:space:]=]+[^[:space:];&|)]+' | tail -n1 | sed -E 's/^[[:space:]]*(--head|-H)[[:space:]=]+//') +HEAD_BRANCH="${HEAD_BRANCH#*:}" +HEAD_BRANCH="${HEAD_BRANCH:-$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null)}" + +# A value that looks like an option would be passed to git as one, so skip the check instead +if [[ "$BASE" == -* || "$HEAD_BRANCH" == -* ]]; then + exit 0 fi # Fetch latest base and check freshness -git fetch origin "$BASE" --quiet 2>/dev/null || true -BEHIND=$(git rev-list --count HEAD.."origin/$BASE" 2>/dev/null || echo "?") +git -C "$REPO_DIR" fetch origin "$BASE" --quiet 2>/dev/null || true +BEHIND=$(git -C "$REPO_DIR" rev-list --count "$HEAD_BRANCH".."origin/$BASE" 2>/dev/null || echo "?") if [[ "$BEHIND" != "0" && "$BEHIND" != "?" ]]; then cat >&2 << EOF =================================================================== - PR BLOCKED: Branch is $BEHIND commit(s) behind $BASE + PR BLOCKED: Branch $HEAD_BRANCH is $BEHIND commit(s) behind $BASE + (checked in $REPO_DIR) =================================================================== Your PR will show "out of date with base branch" on GitHub. @@ -45,13 +76,12 @@ EOF exit 2 fi -# Check we're not on main/master -BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) -if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then +# Check we're not opening a PR from main/master +if [[ "$HEAD_BRANCH" == "main" || "$HEAD_BRANCH" == "master" ]]; then cat >&2 << EOF =================================================================== - PR BLOCKED: Cannot create PR from $BRANCH + PR BLOCKED: Cannot create PR from $HEAD_BRANCH =================================================================== Create a feature branch first: