Skip to content

Instantly share code, notes, and snippets.

@hexsprite
Created May 26, 2026 16:51
Show Gist options
  • Select an option

  • Save hexsprite/5b205cfa6025531a3305caaccd47f8c5 to your computer and use it in GitHub Desktop.

Select an option

Save hexsprite/5b205cfa6025531a3305caaccd47f8c5 to your computer and use it in GitHub Desktop.
Rebuild a working branch by cherry-picking your open GitHub PRs onto fresh upstream main. Uses git rerere to replay conflict resolutions across runs.
#!/usr/bin/env bash
# pr-restack — rebuild a working branch by cherry-picking my open PRs
# on top of an upstream base. Relies on git rerere to replay conflict
# resolutions across runs.
#
# Usage:
# pr-restack # from inside any git repo with gh access
# pr-restack -b my-branch # custom branch name
# pr-restack -r upstream/main # custom base ref
# pr-restack -o origin -u upstream # remote names
# pr-restack -a @me # gh PR author filter
# pr-restack -B main # gh --base filter
# pr-restack -n # dry run: list PRs only
#
# Defaults:
# - Runs in current git repo.
# - Picks base ref by trying: upstream/main, upstream/master,
# origin/main, origin/master (first that exists).
# - Filters PRs by --author @me --state open --base <main>.
set -euo pipefail
UPSTREAM_REMOTE="upstream"
ORIGIN_REMOTE="origin"
BASE_REF=""
AUTHOR="@me"
BASE_BRANCH=""
BRANCH=""
DRY_RUN=0
while getopts "b:r:o:u:a:B:nh" opt; do
case "$opt" in
b) BRANCH="$OPTARG" ;;
r) BASE_REF="$OPTARG" ;;
o) ORIGIN_REMOTE="$OPTARG" ;;
u) UPSTREAM_REMOTE="$OPTARG" ;;
a) AUTHOR="$OPTARG" ;;
B) BASE_BRANCH="$OPTARG" ;;
n) DRY_RUN=1 ;;
h) sed -n '2,20p' "$0"; exit 0 ;;
*) exit 2 ;;
esac
done
# Must be inside a git repo.
REPO_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$REPO_TOPLEVEL" ]]; then
echo "ERROR: not inside a git repo" >&2
exit 1
fi
cd "$REPO_TOPLEVEL"
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
echo "ERROR: tracked changes present (commit or stash first)" >&2
exit 1
fi
# Auto-detect remotes if defaults missing.
if ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
# No upstream remote — fall back to origin for both.
UPSTREAM_REMOTE="$ORIGIN_REMOTE"
fi
git config rerere.enabled true
git config rerere.autoupdate true
echo "==> Fetching $UPSTREAM_REMOTE${UPSTREAM_REMOTE:+ }${ORIGIN_REMOTE}"
git fetch --prune "$UPSTREAM_REMOTE"
if [[ "$ORIGIN_REMOTE" != "$UPSTREAM_REMOTE" ]]; then
git fetch --prune "$ORIGIN_REMOTE"
fi
# Resolve BASE_REF: explicit > first existing of common candidates.
if [[ -z "$BASE_REF" ]]; then
for cand in \
"$UPSTREAM_REMOTE/main" "$UPSTREAM_REMOTE/master" \
"$ORIGIN_REMOTE/main" "$ORIGIN_REMOTE/master"; do
if git rev-parse --verify --quiet "$cand" >/dev/null; then
BASE_REF="$cand"
break
fi
done
fi
if [[ -z "$BASE_REF" ]] || ! git rev-parse --verify --quiet "$BASE_REF" >/dev/null; then
echo "ERROR: could not resolve base ref (tried upstream/main, upstream/master, origin/main, origin/master)" >&2
exit 1
fi
# Derive --base filter from BASE_REF if not given.
if [[ -z "$BASE_BRANCH" ]]; then
BASE_BRANCH="${BASE_REF##*/}"
fi
# Default branch name.
if [[ -z "$BRANCH" ]]; then
BRANCH="restack-$(date +%Y%m%d-%H%M%S)"
fi
echo "==> Base: $BASE_REF Branch: $BRANCH Author: $AUTHOR"
echo "==> Listing open PRs (--base $BASE_BRANCH --author $AUTHOR)"
PRS_JSON="$(gh pr list \
--author "$AUTHOR" \
--state open \
--base "$BASE_BRANCH" \
--json number,title,headRefName,headRefOid,headRepositoryOwner \
--limit 100)"
COUNT=$(echo "$PRS_JSON" | jq 'length')
if [[ "$COUNT" -eq 0 ]]; then
echo "No open PRs found."
exit 0
fi
echo "Found $COUNT PR(s):"
echo "$PRS_JSON" | jq -r '.[] | " #\(.number) \(.headRefName) — \(.title)"'
if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi
echo "==> Creating $BRANCH from $BASE_REF"
git checkout -B "$BRANCH" "$BASE_REF"
# Cherry-pick each PR's commit range (BASE_REF..headRefOid).
# Multi-commit PRs preserve their commit boundaries.
echo "$PRS_JSON" | jq -c '.[]' | while read -r pr; do
num=$(echo "$pr" | jq -r '.number')
head=$(echo "$pr" | jq -r '.headRefName')
sha=$(echo "$pr" | jq -r '.headRefOid')
echo
echo "==> PR #$num ($head) — picking $BASE_REF..$sha"
if ! git cat-file -e "$sha^{commit}" 2>/dev/null; then
echo " fetching $head from $ORIGIN_REMOTE"
git fetch "$ORIGIN_REMOTE" "$head" || true
fi
if ! git cat-file -e "$sha^{commit}" 2>/dev/null; then
# gh's pr/<num>/head ref always exists on the upstream repo.
echo " fetching pr/$num from $UPSTREAM_REMOTE"
git fetch "$UPSTREAM_REMOTE" "pull/$num/head:refs/pr-restack/$num" 2>/dev/null || true
if git rev-parse --verify --quiet "refs/pr-restack/$num" >/dev/null; then
sha="$(git rev-parse "refs/pr-restack/$num")"
fi
fi
if ! git cat-file -e "$sha^{commit}" 2>/dev/null; then
echo " ERROR: commit $sha not reachable; skipping" >&2
continue
fi
if git cherry-pick -x "$BASE_REF..$sha"; then
echo " OK"
else
if [[ -z "$(git ls-files -u)" ]]; then
echo " rerere resolved — continuing"
GIT_EDITOR=true git cherry-pick --continue
else
echo " CONFLICT in PR #$num — resolve, then:"
echo " git cherry-pick --continue # finish this PR"
echo " $0 -b $BRANCH # resume remaining PRs"
exit 1
fi
fi
done
echo
echo "==> Done. Branch: $BRANCH"
git log --oneline "$BASE_REF..HEAD"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment