Last active
April 13, 2026 18:16
-
-
Save maximetinu/b6abf4d0a166c3d55ffec220640f9f67 to your computer and use it in GitHub Desktop.
git merge-stack — Cascade-merge a PR stack from bottom to top
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
| #!/usr/bin/env bash | |
| # | |
| # git-merge-stack — Cascade-merge a PR stack from bottom to top. | |
| # | |
| # Setup (run once): | |
| # mkdir -p ~/.local/bin | |
| # cp this-file ~/.local/bin/git-merge-stack | |
| # chmod +x ~/.local/bin/git-merge-stack | |
| # Ensure ~/.local/bin is on your PATH (add to ~/.zshrc if needed): | |
| # export PATH="$HOME/.local/bin:$PATH" | |
| # | |
| # Usage: git merge-stack | |
| # Run from any branch in the stack. Discovers the full chain via `gh`, | |
| # merges each base into its child, and pushes. Aborts on any conflict. | |
| # | |
| set -euo pipefail | |
| ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) | |
| trap 'git checkout "$ORIGINAL_BRANCH" 2>/dev/null' EXIT | |
| PR_JSON=$(gh pr list --author @me --state open --json number,headRefName,baseRefName) | |
| if [ "$(echo "$PR_JSON" | jq length)" -eq 0 ]; then | |
| echo "No open PRs found for you." | |
| exit 1 | |
| fi | |
| # --- Walk down to the root of the stack containing the current branch --- | |
| current="$ORIGINAL_BRANCH" | |
| while true; do | |
| base=$(echo "$PR_JSON" | jq -r --arg h "$current" \ | |
| '[.[] | select(.headRefName == $h)][0].baseRefName // empty') | |
| if [ -z "$base" ]; then | |
| echo "Error: '$current' is not the head of any of your open PRs." | |
| exit 1 | |
| fi | |
| parent_head=$(echo "$PR_JSON" | jq -r --arg h "$base" \ | |
| '[.[] | select(.headRefName == $h)][0].headRefName // empty') | |
| if [ -z "$parent_head" ]; then | |
| stack_base="$base" | |
| break | |
| fi | |
| current="$base" | |
| done | |
| # --- Build the ordered chain (bottom → top) --- | |
| chain=() | |
| cursor="$current" | |
| while [ -n "$cursor" ]; do | |
| chain+=("$cursor") | |
| # Ensure the stack is linear (no forks) | |
| children=$(echo "$PR_JSON" | jq --arg b "$cursor" \ | |
| '[.[] | select(.baseRefName == $b)] | length') | |
| if [ "$children" -gt 1 ]; then | |
| echo "Error: '$cursor' has $children PRs targeting it — not a linear stack." | |
| exit 1 | |
| fi | |
| cursor=$(echo "$PR_JSON" | jq -r --arg b "$cursor" \ | |
| '[.[] | select(.baseRefName == $b)][0].headRefName // empty') | |
| done | |
| # --- Print the stack --- | |
| echo "Stack:" | |
| echo " $stack_base" | |
| for b in "${chain[@]}"; do | |
| pr=$(echo "$PR_JSON" | jq -r --arg h "$b" \ | |
| '[.[] | select(.headRefName == $h)][0].number') | |
| echo " └─ #$pr $b" | |
| done | |
| echo "" | |
| # --- Back up each branch before any changes --- | |
| TIMESTAMP=$(date +%Y%m%d-%H%M%S) | |
| echo "Creating backups (timestamp: $TIMESTAMP) ..." | |
| for branch in "${chain[@]}"; do | |
| safe_name=$(echo "$branch" | tr '/' '-') | |
| backup="backup/${safe_name}_${TIMESTAMP}" | |
| git branch "$backup" "$branch" 2>/dev/null || git branch "$backup" "origin/$branch" | |
| echo " $branch → $backup" | |
| done | |
| echo "" | |
| # --- Fetch once, then merge + push each level --- | |
| git fetch origin | |
| prev_ref="origin/$stack_base" | |
| for branch in "${chain[@]}"; do | |
| pr=$(echo "$PR_JSON" | jq -r --arg h "$branch" \ | |
| '[.[] | select(.headRefName == $h)][0].number') | |
| git checkout "$branch" 2>/dev/null | |
| # Fast-forward local to match remote (fail if diverged) | |
| if git rev-parse --verify "origin/$branch" >/dev/null 2>&1; then | |
| git merge "origin/$branch" --ff-only 2>/dev/null || { | |
| echo "Error: local '$branch' has diverged from origin — resolve manually." | |
| exit 1 | |
| } | |
| fi | |
| echo "Merging into #$pr $branch ..." | |
| git merge "$prev_ref" --no-edit || { | |
| echo "CONFLICT merging into $branch — aborting." | |
| git merge --abort | |
| exit 1 | |
| } | |
| git push origin "$branch" | |
| # Use the local ref going forward (it's now ahead of origin/) | |
| prev_ref="$branch" | |
| done | |
| echo "" | |
| echo "Done — all branches in the stack are up to date." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment