Created
April 13, 2026 08:46
-
-
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.
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
| #!/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