Stack an existing or new git branch on top of another branch, restacking it in a git worktree when needed, then align the GitHub PR bases and stack metadata with `gh stack`. Use when invoked as `$git-stack` or when the user asks to stack or restack a branch or PR onto another branch.
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Stack <branch> onto <base-branch> (or restack it onto its current base) in a dedicated worktree, then align GitHub PR bases and the gh stack object with the resulting local chain.
Parse arguments (drop a bare literal onto token):
| Tokens | Meaning |
|---|---|
| 0 | subject = current branch; base resolved in step 2 (restack mode) |
| 1 | base = token; subject = current branch |
| 2 | subject = token 1; base = token 2 |
| 3+ | Stop: usage is stack [<branch>] [<base-branch>] |
Stop if subject equals base.
Resolve repo, remote, trunk, and (restack mode) the base:
repo_root=$(git rev-parse --path-format=absolute --git-common-dir); repo_root=${repo_root%/.git}
current=$(git branch --show-current)
remote=$(git config --get "branch.${subject}.remote" || true)
if [ -z "$remote" ]; then
remotes=$(git remote); remote_count=$(echo "$remotes" | wc -l)
[ "$remote_count" -eq 1 ] && remote=$(echo "$remotes" | head -1)
fi
[ -n "$remote" ] || { git remote | grep -qx origin && remote=origin; }
[ -n "$remote" ] || { echo "Multiple remotes found, none configured for branch. Candidates: $(git remote | tr '\n' ' ')"; exit 1; }
trunk=$(git symbolic-ref "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s|refs/remotes/${remote}/||")
[ -n "$trunk" ] || trunk=$(git config init.defaultBranch || echo main)
git fetch --prune "$remote"Never hard-code origin — the repo's remote may be named anything.
Restack mode (0 tokens) resolves base as the first hit of:
base=$(gh pr view "$subject" --json baseRefName -q .baseRefName 2>/dev/null)
[ -n "$base" ] || base=$( (cd "$repo_root" && gh stack view --json 2>/dev/null) | jq -r --arg s "$subject" '
.branches | to_entries | map(select(.value.name == $s))[0].key as $i
| if $i == null or $i == 0 then empty else .[$i-1].value.name end')Stop with the usage line if both are empty. Stop if base resolves to neither refs/heads/$base nor refs/remotes/$remote/$base.
Pick the working checkout — prefer the worktree that already holds subject so a branch checked out elsewhere is never rewritten from the wrong worktree:
work_dir=$(git worktree list --porcelain | awk -v b="refs/heads/$subject" '
/^worktree /{ wt=$2 } /^branch /{ if ($2 == b) print wt }')work_dir non-empty → use it; if git -C "$work_dir" status --porcelain=v1 is non-empty, stop and tell the user to commit or stash there first.
Otherwise create one under the using-git-worktrees convention:
path_key=${repo_root#"$HOME"/}; path_key=${path_key//\//-}
work_dir="$HOME/.worktrees/${path_key}/${subject}"
if git show-ref --quiet "refs/heads/${subject}"; then
git worktree add "$work_dir" "$subject"
elif git show-ref --quiet "refs/remotes/${remote}/${subject}"; then
git worktree add --track -b "$subject" "$work_dir" "${remote}/${subject}"
else # new-branch mode
base_ref="${remote}/${base}"; git show-ref --quiet "refs/remotes/${base_ref}" || base_ref="$base"
git worktree add "$work_dir" -b "$subject" --no-track "$base_ref"
git -C "$work_dir" push -u "$remote" "${subject}:${subject}"
git -C "$work_dir" branch -vv
fi--no-track is mandatory here: a branch cut from a remote base must not inherit the base's upstream. Report the branch -vv line as proof it tracks $remote/$subject.
New-branch mode has no commits to replay: report the ready worktree and stop, unless the branch already has commits, in which case continue at step 7 to open its PR.
Create a backup (existing-branch mode only):
git -C "$work_dir" branch "${subject}.bak" "$subject"If ${subject}.bak already exists, ask whether to overwrite (git branch -f) or abort. Keep the backup after success.
Restack locally — --onto replays exactly the commits unique to subject, correct whether it was cut from trunk or an older base tip; never plain git rebase <base> here:
base_ref="${remote}/${base}"; git show-ref --quiet "refs/remotes/${base_ref}" || base_ref="$base"
if git -C "$work_dir" merge-base --is-ancestor "$base_ref" "$subject"; then
echo "already stacked on ${base}; nothing to replay"
else
fork=$(git -C "$work_dir" merge-base "$subject" "$base_ref")
git -C "$work_dir" rebase --onto "$base_ref" "$fork" "$subject"
fi(equivalent to git rebase --onto <base_ref> <merge-base> <subject>, run with -C "$work_dir".)
On conflict, follow git-rebase's conflict handler: recorded rerere resolution as-is; clean-filtered machine state → --ours; version bumps → the higher version; deliberate config updates → --theirs; generated artifacts → regenerate. Stage each fix, verify no conflict markers remain, then GIT_EDITOR=true git -C "$work_dir" rebase --continue (or --skip if the commit went empty). Pause only for genuine semantic collisions in hand-written source. Rollback: git -C "$work_dir" reset --hard "${subject}.bak".
Push the subject — before any gh stack call, since gh stack link pushes without --force-with-lease:
git -C "$work_dir" push --force-with-lease -u "$remote" "${subject}:${subject}"
git -C "$work_dir" branch --set-upstream-to="${remote}/${subject}" "$subject"
git -C "$work_dir" branch -vv | grep '^\*'The tracking line must show [${remote}/${subject}]; fix it before continuing if it shows the base branch instead.
Build the stack chain bottom→top (trunk excluded), capped at 10 hops against a cycle:
chain=("$subject"); b="$base"; hops=0
while [ "$b" != "$trunk" ] && [ "$hops" -lt 10 ]; do
chain=("$b" "${chain[@]}")
parent=$(gh pr view "$b" --json baseRefName -q .baseRefName 2>/dev/null) || parent=""
[ -n "$parent" ] || break # no PR for $b: treat it as the bottom of the stack
b="$parent"; hops=$((hops + 1))
doneEnsure every chain member has an open PR based on its parent (previous chain entry, or $trunk for the first):
pr_json=$(gh pr view "$member" --json number,state,baseRefName 2>/dev/null || echo '{}')
state=$(echo "$pr_json" | jq -r '.state // empty')state is MERGED or CLOSED → stop and report: step 7's chain walk picked up a member whose PR already landed or was abandoned; restacking must resolve this manually (typically by re-running with a base below that member so the chain collapses to $trunk). Never create a spurious new PR for it.state is empty → invoke pr-create for that branch so the repo's title, body, label, draft, and @me assignment conventions (pr-style) apply, then retarget: gh pr edit "$number" --base "$parent". Never hand-roll gh pr create or let link auto-create the PR here — both bypass pr-style.state is OPEN and base differs from parent → gh pr edit "$number" --base "$parent".state is OPEN and base already matches parent → leave it.Align the GitHub stack:
if [ "${#chain[@]}" -ge 2 ]; then
(cd "$work_dir" && gh stack link --remote "$remote" --base "$trunk" "${chain[@]}")
fiA single-member chain (base is trunk) has no GitHub stack to create — skip link and say so; step 8 already put that PR's base on $trunk. Always pass --remote and --base explicitly. Never pass --open: new PRs stay drafts.
Add local stack tracking, best effort:
if ! (cd "$work_dir" && gh stack view --json >/dev/null 2>&1) && [ "${#chain[@]}" -ge 2 ]; then
git -C "$work_dir" config --local rerere.enabled true
(cd "$work_dir" && gh stack init --base "$trunk" "${chain[@]}") || echo "local stack tracking unavailable"
fiA non-zero exit is not a workflow failure — the GitHub metadata from step 9 is already correct; report "local tracking unavailable" and continue. If gh stack view --json already succeeds but lists a different branch set, skip init and report the mismatch instead of guessing.
Report children that now need restacking:
gh pr list --state open --json number,headRefName,baseRefName \
--jq ".[] | select(.baseRefName == \"$subject\") | .headRefName"For each child, print the follow-up stack command with $subject as its new base. Leaving children based on a rewritten branch silently is the main failure mode of manual stacking.
Report:
Stacked <subject> onto <base>.
Worktree: <work_dir>
Chain: <trunk> → <b1> → … → <subject>
PRs: #<n1> (base <trunk>) → … → #<nk> (base <base>)
GitHub stack: linked | single PR, no stack | local tracking unavailable
Tracking: <remote>/<subject>
Backup: <subject>.bak (kept — git branch -D <subject>.bak)
Children to restack: stack <child> <subject> (or "none")gh stack view — always use gh stack view --json; bare view opens an interactive TUI.modify subcommand, or bare switch/checkout without a branch argument — both are interactive-only.submit subcommand — it auto-generates PR titles/bodies, conflicting with this repo's pr-style conventions.unstack [<stack-number>] [--local] unless the user explicitly asks — it deletes the GitHub stack.phatblat/dotfiles
4d65cab
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.