Skip to content

Instantly share code, notes, and snippets.

@TimoPtr
Created April 13, 2026 08:46
Show Gist options
  • Select an option

  • Save TimoPtr/42e5ce3f524679f4ad1efe4ff20f1ce9 to your computer and use it in GitHub Desktop.

Select an option

Save TimoPtr/42e5ce3f524679f4ad1efe4ff20f1ce9 to your computer and use it in GitHub Desktop.
Git hooks to setup upstream automatically for a PR. Doc included within the script.
#!/bin/sh
# Automatically wire up push config for branches named `pull/<N>` so that a
# plain `git push` on a PR branch pushes back to the contributor's fork.
#
# Requires: gh (GitHub CLI, authenticated) and jq.
# The contributor must have "Allow edits by maintainers" enabled on the PR.
#
# Args passed by git: $1=prev_head $2=new_head $3=branch_checkout_flag (1 = branch)
# Can also be invoked manually (no args) from inside a repo while on a
# `pull/<N>` branch — useful to (re)run the setup without re-checkout.
#
# ---------------------------------------------------------------------------
# Setup on a new machine
# ---------------------------------------------------------------------------
# 1. Install prerequisites:
# macOS: brew install gh jq
# Linux: apt install gh jq (or your distro equivalent)
# 2. Authenticate gh:
# gh auth login
# 3. Drop this file at ~/.githooks/post-checkout and make it executable:
# mkdir -p ~/.githooks
# cp post-checkout ~/.githooks/post-checkout
# chmod +x ~/.githooks/post-checkout
# 4. Point every repo at this shared hooks directory:
# git config --global core.hooksPath ~/.githooks
# 5. Allow `git push` to use the upstream branch name even when it differs
# from the local branch name (required because `pull/6701` tracks e.g.
# `contributor/fix/theme-toggle-reload`):
# git config --global push.default upstream
#
# ---------------------------------------------------------------------------
# Per-repo configuration so PRs appear as `pull/<N>`
# ---------------------------------------------------------------------------
# By default `git fetch` does not retrieve GitHub's pull-request refs. Add
# this refspec to the origin remote so every PR is fetched as
# `refs/remotes/origin/pull/<N>`:
#
# git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pull/*'
# git fetch origin
#
# After that you can `git checkout pull/<N>` to land on any PR branch, and
# this hook will prompt to wire up push config for it.
#
# Resulting `.git/config` fragment:
# [remote "origin"]
# url = git@github.com:<org>/<repo>.git
# fetch = +refs/heads/*:refs/remotes/origin/*
# fetch = +refs/pull/*/head:refs/remotes/origin/pull/*
set -eu
log() { printf 'post-checkout: %s\n' "$*" >&2; }
# When invoked as a git hook, git passes 3 args and $3=1 for branch checkouts.
# When invoked manually (no args) from inside a repo, skip that check.
if [ $# -ge 3 ]; then
[ "$3" = "1" ] || exit 0
fi
if ! branch=$(git symbolic-ref --short -q HEAD); then
log "HEAD is not on a branch (detached?); nothing to configure"
exit 0
fi
case "$branch" in
pull/[0-9]*) ;;
*)
log "branch '$branch' does not match 'pull/<number>'; nothing to configure"
exit 0
;;
esac
num=${branch#pull/}
# Skip if we've already configured this branch.
if git config --get "branch.$branch.pushRemote" >/dev/null; then
exit 0
fi
if ! command -v gh >/dev/null 2>&1; then
log "'gh' not found in PATH; skipping PR push config for '$branch'"
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
log "'jq' not found in PATH; skipping PR push config for '$branch'"
exit 0
fi
# Ask the user before touching remotes / config. Read from the controlling tty
# so this works even when the hook's stdin isn't interactive (e.g. under gh/git
# wrappers). If there is no tty, skip silently.
if [ ! -r /dev/tty ] || [ ! -w /dev/tty ]; then
log "no controlling tty; skipping interactive setup for '$branch'"
exit 0
fi
printf '\npost-checkout: set up push to the contributor fork for PR #%s? [y/N] ' "$num" >/dev/tty
read -r answer </dev/tty || exit 0
case "$answer" in
y|Y|yes|YES) ;;
*) log "skipped setup for '$branch'"; exit 0 ;;
esac
data=$(gh pr view "$num" --json headRefName,headRepository,headRepositoryOwner 2>/dev/null) || {
log "failed to fetch PR #$num metadata via gh; is this a GitHub repo and are you authenticated?"
exit 0
}
owner=$(printf '%s' "$data" | jq -r '.headRepositoryOwner.login // empty')
repo=$(printf '%s' "$data" | jq -r '.headRepository.name // empty')
head=$(printf '%s' "$data" | jq -r '.headRefName // empty')
if [ -z "$owner" ] || [ -z "$repo" ] || [ -z "$head" ]; then
log "incomplete PR metadata for #$num (owner='$owner' repo='$repo' head='$head'); aborting"
exit 0
fi
if ! git remote get-url "$owner" >/dev/null 2>&1; then
git remote add "$owner" "git@github.com:$owner/$repo.git"
log "added remote '$owner' -> git@github.com:$owner/$repo.git"
fi
if ! git fetch --quiet "$owner" "$head"; then
log "failed to fetch $owner/$head; leaving branch config untouched"
exit 0
fi
git config "branch.$branch.remote" "$owner"
git config "branch.$branch.pushRemote" "$owner"
git config "branch.$branch.merge" "refs/heads/$head"
log "configured '$branch' to push to $owner/$repo:$head"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment