diff --git a/cmd/modify.go b/cmd/modify.go index ba2e5404..341ae28d 100644 --- a/cmd/modify.go +++ b/cmd/modify.go @@ -231,7 +231,7 @@ func runModifyAbort(cfg *config.Config) error { cfg.Printf("The stack may be in an inconsistent state.") cfg.Printf("Try `%s` to fix, or `%s` + `%s` to recreate.", cfg.ColorCyan("gh stack rebase"), cfg.ColorCyan("gh stack unstack --local"), - cfg.ColorCyan("gh stack init --adopt")) + cfg.ColorCyan("gh stack init")) return ErrSilent } cfg.Successf("Stack restored successfully") diff --git a/cmd/root.go b/cmd/root.go index bb1a1706..bbbb6765 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,7 @@ locally, then push to GitHub to create your stack of PRs.`, $ gh stack init # Or turn an existing set of branches into a stack - $ gh stack init --adopt branch1 branch2 branch3 + $ gh stack init branch1 branch2 branch3 # Make changes and commit, then add a branch to the stack $ gh stack add branch4 diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 01776261..26649c73 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -13,7 +13,7 @@ export default defineConfig({ integrations: [ starlight({ title: 'GitHub Stacked PRs', - description: 'Break large changes into small, reviewable pull requests that build on each other — with native GitHub support and the gh stack CLI.', + description: 'Break large changes into small, reviewable pull requests. Manage your stacks on GitHub, with the gh stack CLI, or via our APIs.', favicon: '/favicon.svg', logo: { src: './src/assets/github-invertocat.svg', @@ -72,6 +72,8 @@ export default defineConfig({ label: 'Reference', items: [ { label: 'CLI Commands', slug: 'reference/cli' }, + { label: 'REST API', slug: 'reference/rest-api' }, + { label: 'GraphQL API', slug: 'reference/graphql-api' }, { label: 'Webhooks', slug: 'reference/webhooks' }, ], }, diff --git a/docs/package-lock.json b/docs/package-lock.json index fe95bdbe..285d4137 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -72,9 +72,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -91,9 +88,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -110,9 +104,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -129,9 +120,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1191,9 +1179,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1210,9 +1195,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1229,9 +1211,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1248,9 +1227,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1267,9 +1243,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1286,9 +1259,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1305,9 +1275,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1324,9 +1291,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1343,9 +1307,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1368,9 +1329,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1393,9 +1351,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1418,9 +1373,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1443,9 +1395,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1468,9 +1417,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1493,9 +1439,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1518,9 +1461,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3810,9 +3750,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", diff --git a/docs/src/assets/screenshots/add-to-existing-stack.png b/docs/src/assets/screenshots/add-to-existing-stack.png index 212614b0..7e28a397 100644 Binary files a/docs/src/assets/screenshots/add-to-existing-stack.png and b/docs/src/assets/screenshots/add-to-existing-stack.png differ diff --git a/docs/src/assets/screenshots/modify-stack-tui.png b/docs/src/assets/screenshots/modify-stack-tui.png index b13b870f..fb3c2474 100644 Binary files a/docs/src/assets/screenshots/modify-stack-tui.png and b/docs/src/assets/screenshots/modify-stack-tui.png differ diff --git a/docs/src/assets/screenshots/newly-created-stack.png b/docs/src/assets/screenshots/newly-created-stack.png index cd43ec4c..efdc4927 100644 Binary files a/docs/src/assets/screenshots/newly-created-stack.png and b/docs/src/assets/screenshots/newly-created-stack.png differ diff --git a/docs/src/assets/screenshots/stack-merge-box.png b/docs/src/assets/screenshots/stack-merge-box.png index 2a4b3550..6e967bad 100644 Binary files a/docs/src/assets/screenshots/stack-merge-box.png and b/docs/src/assets/screenshots/stack-merge-box.png differ diff --git a/docs/src/assets/screenshots/stack-navigator.png b/docs/src/assets/screenshots/stack-navigator.png index 859111a9..7349c599 100644 Binary files a/docs/src/assets/screenshots/stack-navigator.png and b/docs/src/assets/screenshots/stack-navigator.png differ diff --git a/docs/src/assets/screenshots/stack-recommendation-banner.png b/docs/src/assets/screenshots/stack-recommendation-banner.png new file mode 100644 index 00000000..9849e109 Binary files /dev/null and b/docs/src/assets/screenshots/stack-recommendation-banner.png differ diff --git a/docs/src/assets/screenshots/stack-recommendation-dialog-add.png b/docs/src/assets/screenshots/stack-recommendation-dialog-add.png new file mode 100644 index 00000000..9442d692 Binary files /dev/null and b/docs/src/assets/screenshots/stack-recommendation-dialog-add.png differ diff --git a/docs/src/assets/screenshots/stack-recommendation-dialog-create.png b/docs/src/assets/screenshots/stack-recommendation-dialog-create.png new file mode 100644 index 00000000..fda745c6 Binary files /dev/null and b/docs/src/assets/screenshots/stack-recommendation-dialog-create.png differ diff --git a/docs/src/assets/screenshots/stacked-prs.png b/docs/src/assets/screenshots/stacked-prs.png index b185f702..4d9ea7ed 100644 Binary files a/docs/src/assets/screenshots/stacked-prs.png and b/docs/src/assets/screenshots/stacked-prs.png differ diff --git a/docs/src/assets/screenshots/unstack-entire-stack.png b/docs/src/assets/screenshots/unstack-entire-stack.png index b46da86a..7f6c3263 100644 Binary files a/docs/src/assets/screenshots/unstack-entire-stack.png and b/docs/src/assets/screenshots/unstack-entire-stack.png differ diff --git a/docs/src/content/docs/faq.md b/docs/src/content/docs/faq.md index 890c5208..75599f5f 100644 --- a/docs/src/content/docs/faq.md +++ b/docs/src/content/docs/faq.md @@ -7,7 +7,7 @@ description: Frequently asked questions about GitHub Stacked PRs. ### What is a Stacked PR? How is it different from a regular PR? -A Stacked PR is a pull request that is part of an ordered chain of PRs, where each PR targets the branch of the PR below it instead of targeting `main` directly. Each PR in the stack represents one focused layer of a larger change. Individually, each PR is still a regular pull request — it just has a different base branch, and GitHub understands the relationship between the PRs in the stack. +A Stacked PR is a pull request that is part of an ordered chain of PRs, where each PR targets the branch of the PR below it instead of targeting the merge target directly. Each PR in the stack represents one focused layer of a larger change. Individually, each PR is still a regular pull request — it just has a different base branch, and GitHub understands the relationship between the PRs in the stack. ### How do I create a Stacked PR? @@ -25,11 +25,13 @@ gh stack submit You can also create stacks entirely from the GitHub UI — create the first PR normally, then when creating subsequent PRs, select the option to add them to a stack. See [Creating a Stack from the UI](/gh-stack/guides/ui/#creating-a-stack-from-the-ui) for a walkthrough. +If you already have open PRs whose branches line up, GitHub will detect and suggest turning them into a stack. See [Turning Existing PRs into a Stack](/gh-stack/guides/ui/#turning-existing-prs-into-a-stack). + ### How do I add PRs to my stack? Use `gh stack add ` to add a new branch on top of the current stack. When you run `gh stack submit`, a PR is created for each branch, and they are linked together as a Stack on GitHub. -You can also add PRs to an existing stack from the GitHub UI. See [Adding to an Existing Stack](/gh-stack/guides/ui/#adding-to-an-existing-stack) for details. +You can also add PRs to an existing stack from the GitHub UI — either a brand-new PR or an already-open PR (via the recommendation banner), added to the top of the stack. See [Adding to an Existing Stack](/gh-stack/guides/ui/#adding-to-an-existing-stack) for details. ### How can I modify my stack? @@ -52,12 +54,23 @@ gh stack init db-migrations api-routes frontend **From the CLI** — Run `gh stack unstack` (or `gh stack delete`) to delete the stack on GitHub and remove local tracking. You can also unstack any stack by its number from anywhere in the repository — `gh stack unstack 7` — whether or not it's checked out locally. Use `--local` to only remove local tracking. -**From the UI** — You can unstack PRs from the GitHub UI — see [Unstacking](/gh-stack/guides/ui/#unstacking) for a walkthrough. This dissolves the association between PRs, turning them back into standard independent PRs. +**From the UI** — You can unstack PRs from the GitHub UI — see [Unstacking](/gh-stack/guides/ui/#unstacking) for a walkthrough. This dissolves the association between the PRs, turning them back into standard independent PRs. + +Unstacking only removes **open, draft, and closed** PRs from the stack. **Merged and queued PRs remain part of the stack** — once a PR has merged (or is queued for merge) as part of a stack, it can't be unstacked. A stack is fully dissolved only when none of its PRs have merged or are queued for merge; otherwise it persists with those PRs still in it. ### Can stacks be created across forks? No, Stacked PRs currently require all branches to be in the same repository. Cross-fork stacks are not supported. +### Can a stack target a branch other than my default branch? + +Yes. A stack's **trunk** (the base branch of the bottom PR) can be any branch in the repository, such as a release branch or a long-lived feature branch. It defaults to your repository's default branch (e.g., `main`), but you can pick a different one: + +- **CLI** — pass `--base ` to `gh stack init` or `gh stack link` (for example, `gh stack init --base release`). +- **Web** — create the bottom PR against whatever branch you want as the trunk; the rest of the stack chains on top of it. + +The same behavior applies to whatever trunk your stack targets — branch protection rules, required checks, and CI are all evaluated against your stack's base branch. + ## Checks, Rules & Requirements ### How are branch protection rules evaluated for Stacked PRs? @@ -75,7 +88,7 @@ GitHub Actions workflows trigger as if each PR in the stack is targeting the bas ### How do I access stack metadata in my GitHub Actions workflow? -For advanced use cases, you can access the stack's base ref and base SHA in workflow expressions via `github.event.pull_request.stack`. This property is only present when the PR belongs to a stack. +For advanced use cases, you can access the stack's metadata in workflow expressions via `github.event.pull_request.stack`. This property is only present when the PR belongs to a stack. ```yaml jobs: @@ -89,6 +102,7 @@ jobs: run: | echo "Stack base ref: ${{ github.event.pull_request.stack.base.ref }}" echo "Stack base SHA: ${{ github.event.pull_request.stack.base.sha }}" + echo "PR ${{ github.event.pull_request.stack.position }} of ${{ github.event.pull_request.stack.size }} in the stack" - name: Run a step only when the stack targets a release branch if: github.event.pull_request.stack != null && startsWith(github.event.pull_request.stack.base.ref, 'release/') @@ -97,10 +111,40 @@ jobs: | Expression | Description | |------------|-------------| +| `github.event.pull_request.stack.number` | The stack's number, scoped to the repository. | +| `github.event.pull_request.stack.size` | Total number of pull requests in the stack. | +| `github.event.pull_request.stack.position` | 1-based position of this PR within the stack (`1` is the bottom). | | `github.event.pull_request.stack.base.ref` | The branch the entire stack ultimately targets (e.g., `main`). | -| `github.event.pull_request.stack.base.sha` | The HEAD SHA of that target branch at the time of the event. | +| `github.event.pull_request.stack.base.sha` | The HEAD SHA of the stack's base branch. | + +See the [Webhooks reference](/gh-stack/reference/webhooks/) for the full details on the `stack` object in webhook payloads, or the [REST API reference](/gh-stack/reference/rest-api/) to read the same object on demand from a pull request. + +### How can I optimize CI usage for a stack? + +Because a workflow runs for every PR in a stack, a large stack can multiply your CI usage. You can use the `stack` fields to selectively run jobs based on the position of the current PR in the stack. + +Two conditions are especially useful for deciding where a job should run: + +- **Lowest unmerged PR** — the PR currently at the bottom of the remaining stack. Because it targets the stack base directly, `github.event.pull_request.stack.base.ref` equals `github.event.pull_request.base.ref`. +- **Top PR** — the last PR in the stack, containing the full set of changes. It's the PR where `github.event.pull_request.stack.position` equals `github.event.pull_request.stack.size`. + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run for the lowest unmerged PR in the stack + if: github.event.pull_request.stack != null && github.event.pull_request.stack.base.ref == github.event.pull_request.base.ref + run: echo "Lowest unmerged PR in the stack" -See the [Webhooks reference](/gh-stack/reference/webhooks/) for the full details on the `stack` object in webhook payloads. + - name: Run for the top PR in the stack + if: github.event.pull_request.stack != null && github.event.pull_request.stack.position == github.event.pull_request.stack.size + run: echo "Top PR in the stack" +``` + +As PRs merge from the bottom up, the lowest unmerged PR changes: once the bottom PR lands, the next PR is rebased to target the stack base directly, so it becomes the new lowest unmerged PR on the following workflow run. You can also gate on the original bottom PR with `github.event.pull_request.stack.position == 1`, or on any specific layer using `position`. ### Do all previous PRs need to be passing checks before I can merge? @@ -121,13 +165,21 @@ If the stack is not linear (e.g., after changes were pushed to a lower branch), Every PR in a stack must meet the same merge requirements as a PR targeting the stack base (e.g., `main`): required reviews, passing CI checks, CODEOWNER approvals, and a linear history. All PRs below it must also meet these requirements. See the [Checks, Rules & Requirements](#checks-rules--requirements) section above for details. +### Can I bypass the rules to merge a Stacked PR? + +Not yet — bypassing rules is coming soon, but currently unavailable for stacked PRs. You can't bypass a stack's branch protection rules or rulesets to merge it before its requirements are met, so every PR in the stack must satisfy its rules and required checks before the stack can land. + +### Can I enable auto-merge on a Stacked PR? + +Not yet — auto-merge is coming soon, but currently unavailable for stacked PRs, for both direct merges and the merge queue. You can't set a PR in a stack to land automatically once its requirements are met. Until then, merge the stack (or the part you want to land) yourself once its PRs are ready. + ### How does merging a stack of PRs differ from merging a regular PR? -Stacks merge from the bottom up as a single atomic operation. When you click merge on a PR in a stack, that PR and all unmerged PRs below it land on the base branch together. PRs above remain open, and the remaining stack is automatically rebased so the next PR targets `main` directly. +Stacks merge from the bottom up. When you click merge on a PR in a stack, that PR and all unmerged PRs below it land on the base branch together; PRs above remain open, and the remaining stack is automatically rebased so the next PR targets your base branch directly. With a direct merge the group lands as a single atomic operation; through a merge queue the PRs enter the queue together and are evaluated individually, from the bottom up. ### What happens when you merge a PR in the middle of the stack? -When you click merge on a PR in the middle of the stack, that PR and all unmerged PRs below it land on the base branch together as a single atomic operation, ordered from the bottom up in the resulting history. PRs above the selected one remain open. After the merge, the lowest unmerged PR is updated to target the stack base directly, and a cascading rebase runs across the remaining branches. +When you click merge on a PR in the middle of the stack, that PR and all unmerged PRs below it land on the base branch together, ordered from the bottom up in the resulting history. PRs above the selected one remain open. After the merge, the lowest unmerged PR is updated to target the stack base directly, and a cascading rebase runs across the remaining branches. It is not possible to merge a middle PR in isolation: the PRs below it always merge with it. @@ -175,13 +227,14 @@ When you merge a stack using the merge commit strategy, it creates **one merge c ### How does rebase merge work? -With rebase merge, the commits from each PR in the stack are replayed onto the base branch, creating a linear history without merge commits. The full set of commits lands as a single atomic operation. +With rebase merge, the commits from each PR in the stack are replayed onto the base branch, creating a linear history without merge commits. The full set of commits lands on the base branch. ### Do all PRs get merged at once or one at a time? -All PRs in the stack land in a single atomic operation. When you click merge on a PR, that PR and all unmerged PRs below it are merged together onto the base branch at the same time, ordered from the bottom up in the resulting history. PRs above the selected one remain open. +It depends on the merge method: -This applies whether or not a merge queue is enabled. With a merge queue, the same atomic landing happens once the stack's merge group reaches the front of the queue. +- **Direct merge** — All the included PRs land in a single atomic operation. The selected PR and every unmerged PR below it are merged together onto the base branch at once, ordered from the bottom up. Either the whole group lands, or if any part fails, none of it does. +- **Merge queue** — The PRs enter the queue together and are evaluated individually, from the bottom up. If a PR fails while in the queue, it and all its descendants are ejected, while the PRs below it are unaffected. ### Can I merge only part of a stack? What happens to the remaining unmerged PRs? @@ -197,15 +250,18 @@ Closing a PR in the middle of the stack will block all PRs above it from being m ### What happens when there is an error merging a PR in the middle of a stack? -Pre-merge checks run before any merge attempt, but a merge can still fail (e.g., due to an unexpected merge conflict or intermittent failure). If a failure occurs partway through, merging stops at that PR. PRs below it that successfully merged remain landed on the base branch; the failed PR and PRs above it stay open. Resolve the issue on the failed PR and retry to land the rest of the stack. +Pre-merge checks run before any merge attempt, but a merge can still fail (e.g., due to a merge conflict or intermittent failure). The behavior depends on the merge method: + +- **Direct merge** — The merge is atomic. If any part fails, the entire operation is rolled back and nothing is merged. +- **Merge queue** — Because each PR is evaluated individually, a failure ejects that PR and all its descendants (the PRs stacked above it) from the queue, while the PRs below it are unaffected. You can fix the issue on the failed PR and requeue the ejected PRs. ### Do Stacked PRs support merge queue? -Yes, Stacked PRs fully support merging via merge queue. When you merge a stack through the merge queue: +Yes, Stacked PRs fully support merging via GitHub merge queue. When you merge a stack through the merge queue: -- **All PRs in the stack are added to the queue** in the correct order, ensuring a linear sequence. -- **If a PR is removed or ejected from the merge queue**, all PRs above it in the stack are also ejected and removed from the queue. -- **Stacks are kept in the same merge group on a best-effort basis.** To keep a stack together, the merge queue allows the merge group to exceed its configured max size by up to 50%. If the stack is too large to fit within that buffer, it splits across consecutive merge groups: as much of the stack as fits goes into the current group, and the remaining PRs continue in subsequent groups until the full stack has landed. +- **All PRs in the stack enter the queue together** in the correct order and are evaluated individually, from the bottom up. +- **If a PR is ejected from the merge queue** (for example, because it fails), that PR and all its descendants are ejected too, while the PRs below it are unaffected. +- **The queue makes a best-effort attempt to keep the stack together** in a single merge group. If the stack is too large to fit, it lands across consecutive merge groups: as much of the stack as fits goes into the current group, and the remaining PRs continue in subsequent groups until the full stack has landed. The stack order is preserved, so downstack PRs are merged before upstack PRs. ## Local Development diff --git a/docs/src/content/docs/guides/stacked-prs.md b/docs/src/content/docs/guides/stacked-prs.md index 4336ce44..04356f4f 100644 --- a/docs/src/content/docs/guides/stacked-prs.md +++ b/docs/src/content/docs/guides/stacked-prs.md @@ -5,7 +5,7 @@ description: Practical guide for reviewing, merging, and managing stacked pull r This guide covers the practical day-to-day experience of working with Stacked PRs — how to review them, how merging works step by step, and how to keep things in sync from the CLI. -For an introduction to what stacks are and how GitHub supports them natively, see the [Overview](/gh-stack/introduction/overview/). For a visual walkthrough of the UI, see [Stacked PRs in the GitHub UI](/gh-stack/guides/ui/). +For an introduction to what stacks are and how it works in GitHub, see the [Overview](/gh-stack/introduction/overview/). For a visual walkthrough of the UI, see [Stacked PRs in the GitHub UI](/gh-stack/guides/ui/). ## Reviewing Stacked PRs @@ -21,14 +21,17 @@ Each PR in a stack shows only the diff for its layer — the changes between its - **Review individual PRs** when you're focusing on a specific concern (e.g., reviewing only the API layer). - **Use the stack map** to navigate between PRs without going back to the PR list. -## Merging from the Bottom Up +## Merging a Stack -Stacks are merged **from the bottom up** — you can merge any number of PRs at once, as long as they form a contiguous group starting from the lowest unmerged PR. For example, in a stack of four PRs, you can merge just the bottom one, or the bottom three together, but you cannot merge only the second and third PRs while leaving the first unmerged. Mid-stack merges are not allowed. +Merging is driven by a single action: **click Merge on the highest PR you want to land, and that PR plus every unmerged PR below it are merged together, from the bottom up.** You do not need to merge PRs one at a time, unless you choose to. -1. When the lowest unmerged PR (and any PRs above it that you want to include) meet all merge requirements, merge them. -2. After the merge, the remaining stack is **automatically rebased** — the next unmerged PR's base is updated to target `main` directly. -3. The next unmerged PR is now at the bottom and can be reviewed, approved, and merged. -4. Repeat until the entire stack is landed. +- **To land the whole stack**, merge the **top** PR — every PR below it lands with it in a single step. +- **To land part of the stack**, merge a lower PR — the PRs below it come along, and the PRs above stay open. +- **To land a single PR**, merge the **bottom** PR - only that PR will be merged, and the rest of the PRs stay open. + +You can merge any contiguous group, as long as it starts from the lowest unmerged PR. In a stack of four PRs you can land just the bottom one, or the bottom three together, but you can't merge only the second and third while leaving the first unmerged — a PR always brings the unmerged PRs below it along. Merging a stacked PR always merges all the unmerged PRs below it as well. + +When you land only part of a stack, the remaining PRs are **automatically rebased** and retargeted so the next unmerged PR targets your base branch directly and is immediately ready to review and merge. Once the entire stack has landed, it is complete and can't be extended. If you add new branches on top and run `gh stack submit`, the CLI automatically starts a **new** stack rooted at the trunk for those branches (a new PR on a fully merged stack would target the trunk directly rather than chaining onto the merged PRs). diff --git a/docs/src/content/docs/guides/ui.md b/docs/src/content/docs/guides/ui.md index 0f4f9e48..53fd912c 100644 --- a/docs/src/content/docs/guides/ui.md +++ b/docs/src/content/docs/guides/ui.md @@ -7,9 +7,9 @@ This guide walks through the key UI components and workflows for working with St ## Navigating Stacked PRs -When a pull request is part of a stack, a **stack navigator** appears in the PR header. This component gives you an at-a-glance view of the entire stack and lets you jump between PRs. +When a pull request is part of a stack, a **stack map** appears in the PR header. This component gives you an at-a-glance view of the entire stack and lets you jump between PRs. -The stack navigator shows: +The stack map shows: - All PRs in the stack, listed in order from top to bottom - Which PR you're currently viewing (highlighted) @@ -17,7 +17,7 @@ The stack navigator shows: - Link to Add to Stack, where you can create a new PR that targets the head of the topmost PR - Unstack option to dissolve the association between PRs, turning them back into standard PRs -![The stack navigator in a PR header](../../../assets/screenshots/stack-navigator.png) +![The stack map in a PR header](../../../assets/screenshots/stack-navigator.png) ## Creating a Stack from the UI @@ -37,15 +37,37 @@ When you create the next PR, set its base branch to the first PR's branch. You'l ### Step 3: Confirm the stack -After creating the PR, you'll see the stack navigator appear in the header, showing both PRs linked together. +After creating the PR, you'll see the stack map appear in the header, showing both PRs linked together. -![The stack navigator showing the newly created stack](../../../assets/screenshots/newly-created-stack.png) +![The stack map showing the newly created stack](../../../assets/screenshots/newly-created-stack.png) Repeat this process for each additional PR in the stack — each one targets the branch of the PR before it. +## Turning Existing PRs into a Stack + +If you already have open PRs whose branches line up (each PR's base branch is the head branch of the PR below it), GitHub recognizes the chain and shows a **recommendation banner** offering to turn them into a stack. + +![Banner recommending that eligible PRs be turned into a stack](../../../assets/screenshots/stack-recommendation-banner.png) + +Click the banner to open a dialog that previews the stack, listing each PR in order from top to bottom. Review it and confirm to link the PRs together into a stack. + +![Dialog previewing the stack before it's created](../../../assets/screenshots/stack-recommendation-dialog-create.png) + +Once you confirm, the PRs are stacked and the stack map appears in each PR's header. + ## Adding to an Existing Stack -If a stack already exists and you want to add a new PR to it: +You can add a PR to the top of an existing stack either when you create the PR or after it already exists. + +### Add an existing PR + +If you already have an open PR whose base branch is the head branch of the stack's topmost PR, GitHub shows a **recommendation banner** on that PR, giving you an option to add it to the stack. Click it to preview and confirm, and the PR is added to the top of the existing stack. + +![Recommendation dialog for adding an existing PR to a stack](../../../assets/screenshots/stack-recommendation-dialog-add.png) + +### Create a new PR on the stack + +To create a brand-new PR directly on top of the stack: 1. Open a PR in the stack, click the stack icon in the header, and click **Add**. @@ -77,9 +99,13 @@ Before a PR in the stack can be merged, the following conditions must be met: ![Merge box for a stacked pull request](../../../assets/screenshots/stack-merge-box.png) +:::note[Rule bypass & auto-merge currently unsupported] +**Rule bypass** and **auto-merge** are coming soon, but currently unavailable for stacked PRs. You can't enable auto-merge on a PR in a stack, and you can't bypass a stack's rules to merge before its requirements are met. +::: + ### Rebasing from the UI -When the stack is not linear (e.g., after changes were pushed to a lower branch, or after `main` has moved ahead), a **Rebase Stack** button appears in the merge box. Clicking it triggers a server-side cascading rebase that: +When the stack is not linear (e.g., after changes were pushed to a lower branch, or after the trunk has moved ahead), a **Rebase Stack** button appears in the merge box. Clicking it triggers a server-side cascading rebase that: 1. Rebases the entire stack on top of the latest trunk (e.g., `main`) HEAD. 2. Rebases every unmerged branch on top of the latest changes from its base branch, working from the bottom of the stack upward. @@ -95,10 +121,12 @@ Commits created by a server-side rebase are **not signed**. If your repository r If you want to reorder or reorganize the PRs in a stack from the UI, you must first dissolve the stack and then re-create it. For CLI users, `gh stack modify` provides an interactive way to [restructure a stack](/gh-stack/guides/modify/) — including reordering, inserting, dropping, and renaming branches — without needing to dissolve it. -### Dissolving the Entire Stack +### Dissolving the Stack -To dissolve the stack entirely (turning all Stacked PRs back into independent PRs), use the unstack option on the stack itself. +To dissolve the stack, use the Unstack option on the stack. ![Dissolving an entire stack](../../../assets/screenshots/unstack-entire-stack.png) -After unstacking, each PR retains its current base branch but is no longer linked to the other PRs. The stack navigator and stack-related merge requirements disappear from all affected PRs. +Unstacking removes the **open, draft, and closed** PRs from the stack. Each of those PRs keeps its current base branch but is no longer linked to the others, and the stack map and stack-related merge requirements disappear from them. + +**Merged and queued PRs stay in the stack.** Once a PR has merged — or is queued for merge — as part of a stack, it remains part of that stack and can't be unstacked. So if every PR in the stack is open, draft, or closed, unstacking removes them all and the stack is dissolved entirely; if any PR has already merged or is queued for merge, the stack persists with those PRs still in it. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 285b784e..b16bcd19 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -1,10 +1,10 @@ --- title: GitHub Stacked PRs -description: Break large changes into small, reviewable, stacked pull requests with first-class GitHub support. +description: Break large changes into small, reviewable, stacked pull requests on GitHub. template: splash hero: title: GitHub Stacked PRs - tagline: Break large changes into small, reviewable pull requests that build on each other — with native GitHub support and the gh stack CLI. + tagline: Break large changes into small, reviewable pull requests. Manage your stacks on GitHub, with the gh stack CLI, or via our APIs. actions: - text: Quick Start link: /gh-stack/getting-started/quick-start/ @@ -21,7 +21,7 @@ import StackDiagram from '../../components/StackDiagram.astro'; import stackNavigator from '../../assets/screenshots/stack-navigator.png'; - + Arrange pull requests in an ordered stack and merge them all in one click. Each PR represents one focused layer of your change, reviewed independently and landed together. @@ -41,21 +41,21 @@ Large pull requests are hard to review, slow to merge, and prone to conflicts. R ## Arranging PRs in a Stack -A **stack** is a series of pull requests in the same repository where each PR targets the branch of the PR below it, forming an ordered chain that ultimately lands on your main branch. +A **stack** is a series of pull requests in the same repository where each PR targets the branch of the PR below it, forming an ordered chain that ultimately lands on your trunk branch (e.g., `main`). GitHub understands stacks end-to-end: the pull request UI shows a **stack map** so reviewers can navigate between layers, branch protection rules are enforced against the **final target branch** (not just the direct base), and CI runs for every PR in the stack as if they were targeting the final branch.
- The stack navigator in a pull request header + The stack map in a pull request header
## Working with Stacks -**While the `gh stack` CLI makes the local workflow seamless, it is entirely optional.** You can create and manage Stacked PRs directly via the GitHub UI, the API, or your standard Git workflow. If you choose to use the CLI, it handles creating branches, managing rebases, pushing to GitHub, and creating PRs with the correct base branches. On GitHub, the PR UI gives reviewers the context they need — a stack map for navigation, focused diffs for each layer, and proper rules enforcement. +**While the `gh stack` CLI makes the local workflow seamless, it is entirely optional.** You can create and manage Stacked PRs directly via the GitHub UI, the API, or your standard Git workflow. If you choose to use the CLI, it handles creating branches, managing rebases, pushing to GitHub, and creating PRs with the correct base branches. On GitHub, the PR UI gives reviewers the context they need — a stack map, focused diffs for each layer, and proper rules enforcement. -When you're ready to merge, you can merge all or a part of the stack. Each PR can be merged directly or through the merge queue. **If you want to merge multiple PRs at once (e.g., the bottom two PRs in a stack), simply wait for CI to pass on those specific layers, and you can merge them in a single step.** After a merge, the remaining PRs in the stack are automatically rebased so the lowest unmerged PR targets the updated base branch. +When you're ready to merge, click **Merge** on the highest PR you want to land — that PR **and every unmerged PR below it are merged together, from the bottom up**. Merge the top PR to land the whole stack in one click, or merge a lower PR to land just part of it (the PRs above stay open). After a partial merge, the remaining PRs are automatically rebased so the lowest unmerged PR targets the updated base branch. ## Get Started diff --git a/docs/src/content/docs/introduction/overview.md b/docs/src/content/docs/introduction/overview.md index eeec98ab..a85a4ff3 100644 --- a/docs/src/content/docs/introduction/overview.md +++ b/docs/src/content/docs/introduction/overview.md @@ -1,6 +1,6 @@ --- title: Overview -description: What stacked pull requests are, why they matter, and how GitHub supports them natively. +description: What stacked pull requests are and how they work in GitHub. --- ## Why Stacks? @@ -15,7 +15,7 @@ For developers who want to break large changes into smaller, dependent parts, th A **pull request stack** consists of two or more pull requests in the same repository where: -- The **first (bottom) pull request** targets the main branch (e.g., `main`). +- The **first (bottom) pull request** targets the stack's **trunk** — this can be any branch, and defaults to your repository's default branch (e.g., `main`). - Each subsequent pull request targets the branch of the PR below it. ``` @@ -34,20 +34,20 @@ Each pull request in a stack: ## GitHub Stacked PRs -GitHub supports Stacked PRs natively, combining a rich pull request UI with the `gh stack` CLI to give both authors and reviewers a seamless experience. +Stacked pull requests build on the existing pull request experience in GitHub, allowing authors to group a chain of individual PRs together as a stack. Together with the `gh stack` CLI, authors and reviewers can easily create, modify, navigate, and merge stacks. ### Stack Map in the PR UI When a pull request is part of a stack, a **stack map** appears at the top of the PR page. It shows every PR in the stack, their status, and lets you navigate to any layer with one click. This gives reviewers immediate context about where a PR fits in the bigger picture. -![The stack navigator in a pull request header](../../../assets/screenshots/stack-navigator.png) +![The stack map in a pull request header](../../../assets/screenshots/stack-navigator.png) ### Rules and CI Enforcement The merge requirements for any PR in the stack are determined by the **bottom PR's base** — typically `main`. This means: -- **Branch protection rules** like CODEOWNER approvals are enforced on every PR in the stack, even mid-stack PRs that don't directly target `main`. -- **CI checks** triggered by pull requests on `main` run for all PRs in the stack, not just the bottom one. +- **Branch protection rules** like CODEOWNER approvals are enforced on every PR in the stack, even mid-stack PRs that don't directly target the trunk. +- **CI checks** triggered by pull requests targeting the trunk (e.g., `main`) run for all PRs in the stack, not just the bottom one. This ensures that every layer of the stack meets the same quality bar before it can be merged. @@ -55,12 +55,23 @@ This ensures that every layer of the stack meets the same quality bar before it ### Merging Stacks -The entire stack does not need to be merged at once, but PRs must be merged **from the bottom up**. GitHub supports two merge methods: +You can merge your entire stack, a single PR, or a portion of the stack spanning multiple PRs. When you click **Merge** on any PR, that PR and every unmerged PR below it are merged together, from the bottom up. So you can: -- **Direct merge** — Merges a PR (and all non-merged PRs below it) in a single operation, as long as all conditions are met. -- **Merge queue** — Works as usual but is stack-aware. For example, if the bottom PR is removed from the queue, all other PRs in the stack are also removed. +- **Land the entire stack in one click** by merging the top PR — every PR below it comes with it. +- **Land part of the stack** by merging a mid-stack PR — the PRs below it come along, and the PRs above stay open. -The resulting commit history is the same as merging each PR individually, starting from the bottom. +You can't merge a PR while leaving an unmerged PR below it behind. Merging a stacked PR always merges all the unmerged PRs below it as well. + +GitHub supports two merge methods: + +- **Direct merge** — The selected PR and all unmerged PRs below it land in a single **atomic** operation. Either the whole group merges, or if any part fails, nothing is merged and the operation is rolled back. +- **Merge queue** — The PRs enter the queue together and each PR is evaluated **individually**, from the bottom up. If a PR fails while in the queue, that PR and all its descendants are ejected from the queue, while prior PRs are unaffected. The queue makes a best-effort attempt to keep the whole stack in a single merge group; if the stack is too large to fit, it lands across consecutive groups. If PRs are split across merge groups, the stack order is preserved so downstack PRs are merged before upstack PRs. + +In both methods, the resulting commit history is the same as if each PR had been merged individually, starting from the bottom. + +:::note[Rule bypass & auto-merge currently unsupported] +Rule bypass and auto-merge functionality are coming soon, but currently unavailable for stacked PR merges. A stack merge can only be triggered once all selected PRs meet their requirements. See the [FAQ](/gh-stack/faq/#can-i-bypass-the-rules-to-merge-a-stacked-pr) for details. +::: ### Merge Methods @@ -76,7 +87,7 @@ Rebasing is the trickiest part of working with Stacked PRs, and GitHub handles i - **In the PR UI** — A **Rebase Stack** button lets you trigger a server-side cascading rebase. It rebases the entire stack on top of the latest trunk, updates every unmerged branch, and force-pushes the results. See [Rebasing from the UI](/gh-stack/guides/ui/#rebasing-from-the-ui) for details. - **From the CLI** — `gh stack rebase` performs the same cascading rebase locally. -- **After partial merges** — When you merge a PR at the bottom of the stack, the remaining branches are automatically rebased so the next PR targets `main` and is ready for review and merge. +- **After partial merges** — When you merge a PR at the bottom of the stack, the remaining branches are automatically rebased so the next PR targets the trunk and is ready for review and merge. - **Safe squash-merge handling** — Squash merges are fully supported. The rebase engine safely replays your unique commits on top of the squashed base, avoiding artificial merge conflicts. See the [FAQ](/gh-stack/faq/#how-does-squash-merge-work) for a detailed description of how this works. ## The CLI: `gh stack` diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index e14313b9..8143627a 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -27,16 +27,16 @@ Initialize a new stack in the current repository. gh stack init [flags] [branches...] ``` +| Flag | Description | +|------|-------------| +| `-b, --base ` | Trunk branch for the stack (defaults to the repository's default branch) | + Initializes a new stack locally. In interactive mode (no arguments), prompts for a branch name and offers to use the current branch as the first layer. When explicit branch names are given, existing branches are adopted automatically and any missing branches are created. The trunk defaults to the repository's default branch unless overridden with `--base`. Enables `git rerere` automatically so that conflict resolutions are remembered across rebases. -| Flag | Description | -|------|-------------| -| `-b, --base ` | Trunk branch for the stack (defaults to the repository's default branch) | - **Examples:** ```sh @@ -61,10 +61,6 @@ Add a new branch on top of the current stack. gh stack add [flags] [branch] ``` -Creates a new branch at the current HEAD, adds it to the top of the stack, and checks it out. Must be run while on the topmost branch of a stack. If no branch name is given, prompts for one. - -You can optionally stage changes and create a commit as part of the `add` flow. When `-m` is provided without an explicit branch name, the branch name is auto-generated in date+slug format (e.g., `03-24-add_login`). - | Flag | Description | |------|-------------| | `-A, --all` | Stage all changes (including untracked files); requires `-m` | @@ -73,6 +69,10 @@ You can optionally stage changes and create a commit as part of the `add` flow. > **Note:** `-A` and `-u` are mutually exclusive. +Creates a new branch at the current HEAD, adds it to the top of the stack, and checks it out. Must be run while on the topmost branch of a stack. If no branch name is given, prompts for one. + +You can optionally stage changes and create a commit as part of the `add` flow. When `-m` is provided without an explicit branch name, the branch name is auto-generated in date+slug format (e.g., `03-24-add_login`). + **Examples:** ```sh @@ -106,13 +106,13 @@ View the current stack. gh stack view [flags] ``` -Shows all branches in the stack, their ordering, PR links, and the most recent commit with a relative timestamp. Output is piped through a pager (respects `GIT_PAGER`, `PAGER`, or defaults to `less -R`). - | Flag | Description | |------|-------------| | `-s, --short` | Compact output (branch names only) | | `--json` | Output stack data as JSON | +Shows all branches in the stack, their ordering, PR links, and the most recent commit with a relative timestamp. Output is piped through a pager (respects `GIT_PAGER`, `PAGER`, or defaults to `less -R`). + **Examples:** ```sh @@ -164,13 +164,13 @@ Interactively restructure the current stack. gh stack modify [flags] ``` -Opens an interactive terminal UI for restructuring a stack. All changes are staged in the TUI and applied together when you press `Ctrl+S`. Branches from merged PRs cannot be modified. - | Flag | Description | |------|-------------| | `--continue` | Continue after resolving conflicts | | `--abort` | Abort the modify session and restore the stack to its pre-modify state | +Opens an interactive terminal UI for restructuring a stack. All changes are staged in the TUI and applied together when you press `Ctrl+S`. Branches from merged PRs cannot be modified. + **Preconditions:** The command checks these conditions before opening the TUI: @@ -228,6 +228,10 @@ Remove a stack from local tracking and unstack it on GitHub. Also available as ` gh stack unstack [] [flags] ``` +| Flag | Description | +|------|-------------| +| `--local` | Only remove the stack locally (keep it on GitHub) | + With no argument, the command targets the active stack — the one that contains the currently checked out branch — unstacking it on GitHub and removing local tracking. Provide a stack number (the identifier shown in the github.com stack UI) to unstack a specific stack on GitHub. This works from anywhere in the repository, whether or not the stack is checked out locally — the stack is unstacked directly through the GitHub API. When the stack is also available locally, its local tracking is removed as well. @@ -236,10 +240,6 @@ PRs that are merged, merging, or queued for merge cannot be removed from a stack This is useful when you need to restructure a stack — remove a branch, insert a branch, reorder branches, rename branches, or make other large changes. After unstacking, use `gh stack init` to re-create the stack with the desired structure — existing branches are adopted automatically. -| Flag | Description | -|------|-------------| -| `--local` | Only remove the stack locally (keep it on GitHub) | - **Examples:** ```sh @@ -265,6 +265,12 @@ Push all branches and create/update PRs and the stack on GitHub. gh stack submit [flags] ``` +| Flag | Description | +|------|-------------| +| `--auto` | Skip the editor and use auto-generated PR titles | +| `--open` | Create new PRs as ready for review instead of drafts, and mark existing PRs as ready for review | +| `--remote ` | Remote to push to (defaults to auto-detected remote) | + Creates a Stacked PR for every branch in the stack, pushing branches to the remote. After creating PRs, `submit` automatically creates a **Stack** on GitHub to link the PRs together. If the stack already exists on GitHub (e.g., from a previous submit), new PRs are added to the existing stack. If every PR in the stack has already been merged, that stack is complete and can't be extended. In that case `submit` automatically starts a **new** stack rooted at the trunk for your unmerged branches and creates it on GitHub, leaving the merged stack untouched. @@ -280,12 +286,6 @@ If the branches already have open PRs but no stack exists on GitHub, you will ha In the editor, new PRs default to **ready for review**; flip any PR to **draft** with the ready ↔ draft toggle. With `--auto`, new PRs are created as **drafts** unless you pass `--open`. -| Flag | Description | -|------|-------------| -| `--auto` | Skip the editor and use auto-generated PR titles | -| `--open` | Create new PRs as ready for review instead of drafts, and mark existing PRs as ready for review | -| `--remote ` | Remote to push to (defaults to auto-detected remote) | - **Examples:** ```sh @@ -302,10 +302,15 @@ Fetch, rebase, push, and sync PR state in a single command. gh stack sync [flags] ``` +| Flag | Description | +|------|-------------| +| `--remote ` | Remote to fetch from and push to (defaults to auto-detected remote) | +| `--prune` | Delete local branches for merged PRs | + Performs a synchronization of the entire stack: 1. **Fetch** — fetches the latest changes from `origin`. -2. **Reconcile the remote stack** — mirrors the GitHub stack locally. When PRs have been added to the stack on GitHub (the remote is ahead of your local stack), their branches are pulled down and appended to your local stack automatically. When the local and remote stacks have genuinely diverged (for example, you added a branch locally while different PRs were added to the stack on GitHub), you are prompted to resolve (see [Diverged stacks](#diverged-stacks) below). In a non-interactive terminal a divergence aborts the sync (nothing is pushed or updated). +2. **Reconcile the remote stack** — mirrors the GitHub stack locally. When PRs have been added to the stack on GitHub (the remote is ahead of your local stack), their branches are pulled down and appended to your local stack automatically. When the local and remote stacks have genuinely diverged (for example, you added a branch locally while different PRs were added to the stack on GitHub), you are prompted to resolve (see **Diverged stacks** below). In a non-interactive terminal a divergence aborts the sync (nothing is pushed or updated). 3. **Fast-forward trunk** — fast-forwards the trunk branch to match the remote (skips if diverged). 4. **Cascade rebase** — rebases all stack branches onto their updated parents (only if trunk moved). If a conflict is detected, all branches are restored to their original state, and you are advised to run `gh stack rebase` to resolve conflicts interactively. 5. **Push** — pushes all branches (uses `--force-with-lease` if a rebase occurred). @@ -325,11 +330,6 @@ When neither stack is a clean prefix of the other — for example, you added a b In a non-interactive terminal, a divergence aborts the sync (exit success) without pushing branches or updating PRs; resolve it by unstacking and recreating the stack. -| Flag | Description | -|------|-------------| -| `--remote ` | Remote to fetch from and push to (defaults to auto-detected remote) | -| `--prune` | Delete local branches for merged PRs | - **Examples:** ```sh @@ -347,12 +347,6 @@ Pull from remote and do a cascading rebase across the stack. gh stack rebase [flags] [branch] ``` -Fetches the latest changes from `origin`, then ensures each branch in the stack has the tip of the previous layer in its commit history. Rebases branches in order from trunk upward. - -If a branch's PR has been merged, the rebase automatically switches to `--onto` mode to correctly replay commits on top of the merge target. - -If a rebase conflict occurs, the operation pauses and prints the conflicted files with line numbers. Resolve the conflicts, stage with `git add`, and continue with `--continue`. To undo the entire rebase, use `--abort` to restore all branches to their pre-rebase state. - | Flag | Description | |------|-------------| | `--downstack` | Only rebase branches from trunk to the current branch | @@ -367,6 +361,12 @@ If a rebase conflict occurs, the operation pauses and prints the conflicted file |----------|-------------| | `[branch]` | Target branch (defaults to the current branch) | +Fetches the latest changes from `origin`, then ensures each branch in the stack has the tip of the previous layer in its commit history. Rebases branches in order from trunk upward. + +If a branch's PR has been merged, the rebase automatically switches to `--onto` mode to correctly replay commits on top of the merge target. + +If a rebase conflict occurs, the operation pauses and prints the conflicted files with line numbers. Resolve the conflicts, stage with `git add`, and continue with `--continue`. To undo the entire rebase, use `--abort` to restore all branches to their pre-rebase state. + **Examples:** ```sh @@ -400,12 +400,12 @@ Push all branches in the current stack to the remote. gh stack push [flags] ``` -Pushes every branch to the remote using `--force-with-lease --atomic`. This is a lightweight wrapper around `git push` that knows about all branches in the stack. It does not create or update pull requests — use `gh stack submit` for that. - | Flag | Description | |------|-------------| | `--remote ` | Remote to push to (defaults to auto-detected remote) | +Pushes every branch to the remote using `--force-with-lease --atomic`. This is a lightweight wrapper around `git push` that knows about all branches in the stack. It does not create or update pull requests — use `gh stack submit` for that. + **Examples:** ```sh @@ -421,6 +421,12 @@ Link PRs into a stack on GitHub without local tracking. gh stack link [flags] [...] ``` +| Flag | Description | +|------|-------------| +| `--base ` | Base branch for the bottom of the stack (defaults to the repository's default branch); ignored when adding to an existing stack | +| `--open` | Mark new and existing PRs as ready for review | +| `--remote ` | Remote to push to (defaults to auto-detected remote) | + Creates or updates a stack on GitHub from branch names or PR numbers/URLs. This command does not create or modify any `gh-stack` local tracking state. It is designed for users who manage branches with other tools locally (e.g., jj, Sapling, git-town) and want to simply open a stack of PRs. Arguments are provided in stack order (bottom to top). Branch arguments are automatically pushed to the remote before creating or looking up PRs. For branches that already have open PRs, those PRs are used. For branches without PRs, new PRs are created automatically with the correct base branch chaining. Existing PRs whose base branch doesn't match the expected chain are corrected automatically. @@ -429,12 +435,6 @@ If the PRs are not yet in a stack, a new stack is created. If some of the PRs ar To grow an existing stack without re-listing its PRs, pass a stack number (the number shown in the GitHub stack UI) as the first argument. The remaining arguments are appended to the top of that stack. Arguments already in the stack are skipped, and arguments that belong to a different stack are rejected. Because stack and PR numbers never overlap, a numeric first argument is treated as a stack only when it matches an existing stack — otherwise it is treated as a PR or branch. -| Flag | Description | -|------|-------------| -| `--base ` | Base branch for the bottom of the stack (defaults to the repository's default branch); ignored when adding to an existing stack | -| `--open` | Mark new and existing PRs as ready for review | -| `--remote ` | Remote to push to (defaults to auto-detected remote) | - **Examples:** ```sh @@ -566,14 +566,14 @@ Create a short command alias so you can type less. gh stack alias [flags] [name] ``` -Installs a small wrapper script into `~/.local/bin/` that forwards all arguments to `gh stack`. The default alias name is `gs`, but you can choose any name by passing it as an argument. After setup, you can run `gs push` instead of `gh stack push`. - -On Windows, automatic alias creation is not supported — the command prints manual instructions for creating a batch file or PowerShell function. - | Flag | Description | |------|-------------| | `--remove` | Remove a previously created alias | +Installs a small wrapper script into `~/.local/bin/` that forwards all arguments to `gh stack`. The default alias name is `gs`, but you can choose any name by passing it as an argument. After setup, you can run `gs push` instead of `gh stack push`. + +On Windows, automatic alias creation is not supported — the command prints manual instructions for creating a batch file or PowerShell function. + **Examples:** ```sh @@ -635,3 +635,4 @@ GH_STACK_THEME=light gh stack view | 7 | Rebase already in progress | | 8 | Stack is locked by another process | | 9 | Stacked PRs not enabled for this repository | +| 10 | Modify session interrupted (recovery required) | diff --git a/docs/src/content/docs/reference/graphql-api.md b/docs/src/content/docs/reference/graphql-api.md new file mode 100644 index 00000000..828e8ebd --- /dev/null +++ b/docs/src/content/docs/reference/graphql-api.md @@ -0,0 +1,96 @@ +--- +title: GraphQL API +description: Reference for the read-only stack fields and objects on the GraphQL PullRequest type. +--- + +The GraphQL API exposes a pull request's stack membership through two read-only fields on the `PullRequest` type, backed by a small set of stack objects. These fields are **read-only** — there are no stack mutations via GraphQL. To create or modify stacks use the [REST API](/gh-stack/reference/rest-api/). + +## Fields on `PullRequest` + +| Field | Type | Description | +|-------|------|-------------| +| `stack` | `PullRequestStack` | The stack this pull request belongs to, or `null` if it is not part of a stack. | +| `stackEntry` | `PullRequestStackEntry` | This pull request's entry within its stack (including its position), or `null` if it is not part of a stack. | + +## Objects + +### `PullRequestStack` + +A stack of pull requests. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `ID!` | The Node ID of the `PullRequestStack` object. | +| `number` | `Int!` | A number uniquely identifying the stack within its repository. | +| `size` | `Int!` | The total number of pull requests in the stack. | +| `baseRefName` | `String!` | The branch that the stack's pull requests target. | +| `entries` | `PullRequestStackEntryConnection!` | The entries in the stack. | + +### `PullRequestStackEntry` + +A member of a `PullRequestStack`. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `ID!` | The Node ID of the `PullRequestStackEntry` object. | +| `position` | `Int!` | This entry's position in the stack, where `1` is the closest to the base branch, `2` is stacked on top of `1`, and so on. | +| `pullRequest` | `PullRequest` | The pull request that occupies this position in the stack. | +| `stack` | `PullRequestStack` | The stack that this entry is a part of. | + +### `PullRequestStackEntryConnection` + +A paginated connection of stack entries. Follows the standard [GraphQL connection pattern](https://docs.github.com/en/graphql/guides/introduction-to-graphql#connection). + +| Field | Type | Description | +|-------|------|-------------| +| `edges` | `[PullRequestStackEntryEdge]` | A list of edges. | +| `nodes` | `[PullRequestStackEntry]` | A list of the entries. | +| `pageInfo` | `PageInfo!` | Information to aid in pagination. | +| `totalCount` | `Int!` | The total number of entries in the connection. | + +### `PullRequestStackEntryEdge` + +An edge in a `PullRequestStackEntryConnection`. + +| Field | Type | Description | +|-------|------|-------------| +| `cursor` | `String!` | A cursor for use in pagination. | +| `node` | `PullRequestStackEntry` | The item at the end of the edge. | + +## Example + +Read a pull request's stack, its position, and the first 5 pull requests in the stack: + +```graphql +{ + repository(owner: "OWNER", name: "REPO") { + pullRequest(number: 42) { + number + baseRefName + stackEntry { + position + } + stack { + number + size + baseRefName + entries(first: 5) { + totalCount + nodes { + position + pullRequest { + number + title + state + } + } + } + } + } + } +} +``` + +The pull request's own `baseRefName` is the branch it directly targets (the PR below it in the stack), while `stack.baseRefName` is the branch the entire stack ultimately targets. These differ for every PR in the stack except the bottom one. + +`entries` is a paginated connection (ex: `first: 5` returns up to the first 5 entries). Check the `totalCount` for the full size and if a stack has more entries, page through the rest with the connection's `pageInfo`. diff --git a/docs/src/content/docs/reference/rest-api.md b/docs/src/content/docs/reference/rest-api.md new file mode 100644 index 00000000..1f227242 --- /dev/null +++ b/docs/src/content/docs/reference/rest-api.md @@ -0,0 +1,203 @@ +--- +title: REST API +description: Reference for the Stacks REST API and the stack object on pull request resources. +--- + +GitHub exposes stacks through the REST API in two ways: + +1. **A `stack` object on pull request resources** — every pull request returned by the REST API carries a `stack` object describing its stack membership when it belongs to one. +2. **A dedicated Stacks API** — endpoints to list, read, create, extend, and dissolve stacks directly. + +:::caution[Private Preview] +Stacked PRs is currently in private preview. These endpoints are only available for repositories where the feature is enabled. [Sign up for the waitlist →](https://gh.io/stacksbeta) +::: + +## The `stack` object on Pull Requests + +When a pull request belongs to a stack, GitHub includes a `stack` object on the pull request resource. This lets you read a PR's stack membership — the stack it belongs to, its size, and this PR's position within it — directly from the pull request, without a separate lookup. + +The `stack` object is present on every REST endpoint that returns a pull request, including: + +| Endpoint | Description | +|----------|-------------| +| `GET /repos/{owner}/{repo}/pulls` | List pull requests | +| `GET /repos/{owner}/{repo}/pulls/{pull_number}` | Get a pull request | + +```sh +gh api /repos/OWNER/REPO/pulls/42 --jq '.stack' +``` + +```json +{ + "id": 123456, + "number": 50, + "size": 5, + "position": 2, + "base": { + "ref": "main", + "sha": "def456..." + } +} +``` + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `stack.id` | `integer` | Global identifier for the stack. | +| `stack.number` | `integer` | The stack's number, scoped to the repository (shown in the GitHub UI). | +| `stack.size` | `integer` | Total number of pull requests in the stack. | +| `stack.position` | `integer` | 1-based position of this PR within the stack, where `1` is the bottom (the PR closest to the stack's base). | +| `stack.base.ref` | `string` | The branch the entire stack ultimately targets (e.g., `main`). | +| `stack.base.sha` | `string` | The HEAD SHA of the stack's base branch. | + +The pull request's own `base.ref` is the branch it directly targets (the PR below it in the stack), while `stack.base.ref` is the ultimate target of the entire stack. These differ for every PR in the stack except the bottom one. + +The `stack` object is **only present** when the pull request belongs to a stack. For standalone PRs, the field is `null`. + +The same object is delivered on `pull_request` webhook events. See the [Webhooks reference](/gh-stack/reference/webhooks/) for details. + +## The Stacks API + +The Stacks API provides endpoints to read and manage stacks directly. A stack is addressed by its **stack number** — the repository-scoped number shown in the GitHub UI (the same value as `stack.number` on a pull request). + +If stacked PRs are not enabled for the repository, these endpoints return `404 Not Found`. + +### List stacks + +``` +GET /repos/{owner}/{repo}/stacks +``` + +Lists the stacks in a repository, ordered by stack number (newest first). + +| Parameter | In | Type | Description | +|-----------|-----|------|-------------| +| `pull_request` | query | `integer` | Filter to the stack containing this pull request number. | +| `per_page` | query | `integer` | Results per page (max 100). | +| `page` | query | `integer` | Page number of the results. | + +```sh +# All stacks in the repository +gh api repos/OWNER/REPO/stacks + +# The stack containing PR #102 +gh api "repos/OWNER/REPO/stacks?pull_request=102" +``` + +```json +[ + { + "id": 9876543, + "number": 42, + "node_id": "S_kwDOABCDEF4AAAAA", + "url": "https://api.github.com/repos/octocat/hello-world/stacks/42", + "base": { "ref": "main" }, + "open": true, + "created_at": "2026-04-15T10:00:00Z", + "pull_requests": [ + { + "number": 101, + "state": "open", + "draft": false, + "merged_at": null, + "head": { "ref": "user-model", "sha": "aaa1111..." } + }, + { + "number": 102, + "state": "open", + "draft": false, + "merged_at": null, + "head": { "ref": "user-api", "sha": "bbb2222..." } + } + ] + } +] +``` + +### Get a stack + +``` +GET /repos/{owner}/{repo}/stacks/{stack_number} +``` + +Returns a single stack by its stack number. + +```sh +gh api repos/OWNER/REPO/stacks/42 +``` + +### Create a stack + +``` +POST /repos/{owner}/{repo}/stacks +``` + +Creates a stack from an ordered list of pull request numbers, from the **bottom of the stack to the top**. Each pull request's base ref must match the previous pull request's head ref, forming a valid chain. Returns `201 Created` with the new stack. + +| Body field | Type | Description | +|------------|------|-------------| +| `pull_requests` | `array[integer]` | Ordered pull request numbers, bottom to top. Minimum 2, maximum 100. | + +```sh +echo '{"pull_requests": [101, 102, 103]}' | \ + gh api --method POST repos/OWNER/REPO/stacks --input - +``` + +### Add pull requests to a stack + +``` +POST /repos/{owner}/{repo}/stacks/{stack_number}/add +``` + +Appends pull requests onto the **top** of an existing stack. Provide only the pull requests you want to add (the delta), from the current top of the stack upward. The first new pull request's base ref must match the current top pull request's head ref. Returns `200 OK` with the updated stack. + +| Body field | Type | Description | +|------------|------|-------------| +| `pull_requests` | `array[integer]` | Ordered pull request numbers to append, from the current top upward. Minimum 1, maximum 100. | + +```sh +echo '{"pull_requests": [104]}' | \ + gh api --method POST repos/OWNER/REPO/stacks/42/add --input - +``` + +### Unstack + +``` +POST /repos/{owner}/{repo}/stacks/{stack_number}/unstack +``` + +Removes the unmerged pull requests from a stack. This endpoint takes no request body. Pull requests that cannot be unstacked (those merged, merging, or queued for merge) are left in place. + +- When pull requests remain in the stack, the updated stack is returned with `200 OK`. +- When no pull requests remain, the stack is dissolved and `204 No Content` is returned. + +```sh +gh api --method POST repos/OWNER/REPO/stacks/42/unstack +``` + +### The stack resource + +Each stack is represented by the following resource. The get, create, and add endpoints return a single stack; the list endpoint returns an array of them; and unstack returns the remaining merged stack, or `204 No Content` when the stack is dissolved. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `integer` | Global identifier for the stack. | +| `number` | `integer` | The stack's number, scoped to the repository. Used to address the stack in these endpoints. | +| `node_id` | `string` | Global node ID for the stack. | +| `url` | `string` | The API URL of the stack. | +| `base.ref` | `string` | The branch the stack targets (e.g., `main`). | +| `open` | `boolean` | Whether the stack has any open pull request. `false` when all pull requests are merged or closed. | +| `created_at` | `string` | Timestamp when the stack was created (ISO 8601). | +| `pull_requests` | `array` | The pull requests in the stack, ordered from bottom to top. | + +Each entry in `pull_requests` is a minimal pull request representation: + +| Field | Type | Description | +|-------|------|-------------| +| `number` | `integer` | The pull request number. | +| `state` | `string` | `open` or `closed`. | +| `draft` | `boolean` | Whether the pull request is a draft. | +| `merged_at` | `string \| null` | Timestamp when the pull request was merged, or `null` if not merged. | +| `head.ref` | `string` | The head branch of the pull request. | +| `head.sha` | `string` | The HEAD SHA of that branch. | diff --git a/docs/src/content/docs/reference/webhooks.md b/docs/src/content/docs/reference/webhooks.md index 349fdd49..d08dffa8 100644 --- a/docs/src/content/docs/reference/webhooks.md +++ b/docs/src/content/docs/reference/webhooks.md @@ -1,17 +1,17 @@ --- title: Webhooks -description: Reference for the stack object in pull_request webhook event payloads. +description: Reference for the stacked action and stack object in pull_request webhook event payloads. --- When a pull request belongs to a stack, GitHub adds a `stack` property to the `pull_request` object in webhook event payloads. This lets apps and integrations inspect the stack's ultimate target branch — not just the direct parent branch of the PR. -The `stack` object is included in the `pull_request` webhook payload for all [pull request lifecycle events](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request). +The `stack` object is included in the `pull_request` webhook payload for all [pull request lifecycle events](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request) whenever the pull request is part of a stack. ## The `stack` Object -The `stack` object is nested inside the `pull_request` object and contains information about the stack's base branch: +The `stack` object is nested inside the `pull_request` object. It identifies the stack, describes this PR's place within it, and reports the stack's base branch and ultimate merge target: ```json { @@ -24,6 +24,10 @@ The `stack` object is nested inside the `pull_request` object and contains infor "sha": "abc123..." }, "stack": { + "id": 123456, + "number": 50, + "size": 5, + "position": 2, "base": { "ref": "main", "sha": "def456..." @@ -37,15 +41,90 @@ The `stack` object is nested inside the `pull_request` object and contains infor | Field | Type | Description | |-------|------|-------------| +| `pull_request.stack.id` | `integer` | Global identifier for the stack. | +| `pull_request.stack.number` | `integer` | The stack's number, scoped to the repository. | +| `pull_request.stack.size` | `integer` | Total number of pull requests in the stack. | +| `pull_request.stack.position` | `integer` | 1-based position of this PR within the stack, where `1` is the bottom (the PR closest to the stack's base). | | `pull_request.stack.base.ref` | `string` | The branch the entire stack ultimately targets (e.g., `main`). | -| `pull_request.stack.base.sha` | `string` | The HEAD SHA of that target branch at the time of the event. | +| `pull_request.stack.base.sha` | `string` | The HEAD SHA of the stack's base branch. | `pull_request.base.ref` is the direct parent branch of an individual PR (the branch below it in the stack), while `pull_request.stack.base.ref` is the ultimate target of the entire stack. These differ for all PRs in the stack except the bottom one. The `stack` object is **only present** when the pull request belongs to a stack. For standalone PRs, the field is null. +## The `stacked` Event + +GitHub delivers the `pull_request` event with the `stacked` action when a pull request is **added to a stack**. Because a PR is created before it joins a stack, this is the event to listen for when you need to know exactly when a PR becomes part of a stack. + +| | | +|---|---| +| **Event** (`X-GitHub-Event` header) | `pull_request` | +| **Action** | `stacked` | +| **Fires when** | A pull request is added to a stack | + +The `stacked` payload surfaces the joined stack as a **top-level `stack` object**, in addition to the `stack` nested under `pull_request`. The two objects use the same [fields](#fields) and always match, so you can read either one. + +```json +{ + "action": "stacked", + "number": 42, + "stack": { + "id": 123456, + "number": 50, + "size": 5, + "position": 2, + "base": { + "ref": "main", + "sha": "def456..." + } + }, + "pull_request": { + "number": 42, + "title": "Add API routes", + "base": { + "ref": "feat/auth-layer", + "sha": "abc123..." + }, + "stack": { + "id": 123456, + "number": 50, + "size": 5, + "position": 2, + "base": { + "ref": "main", + "sha": "def456..." + } + } + } +} +``` + +The top-level `stack` object is unique to the `stacked` event; other `pull_request` actions (such as `opened` or `synchronize`) only carry the `stack` nested inside `pull_request`. + ## GitHub Actions -GitHub Actions automatically evaluates workflow triggers using the stack's base branch. If a PR is part of a stack targeting `main`, any workflow configured to run on pull requests targeting `main` will run for every PR in the stack — no workflow changes are required. +GitHub Actions automatically evaluates workflow triggers using the stack's base branch. For example, if a PR is part of a stack targeting `main`, any workflow configured to run on pull requests targeting `main` will run for every PR in the stack — no workflow changes are required. The `stack` object is also available in GitHub Actions workflow expressions via `github.event.pull_request.stack`. See [How do I access stack metadata in my GitHub Actions workflow?](/gh-stack/faq/#how-do-i-access-stack-metadata-in-my-github-actions-workflow) in the FAQ for examples. + +### Optimizing CI usage + +Because a workflow runs for every PR in a stack, you can use the `stack` fields to selectively run jobs. For example, if you only plan on merging one PR at a time, you can choose to only run CI for the lowest unmerged PR. Compare the stack's base ref to the PR's own base ref to detect the **lowest unmerged PR**, and compare `position` to `size` to detect the **top PR**. Note that on a standalone PR the `stack` object is `null`, so you can check that to ensure this logic only applies to stacks. + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run for the lowest unmerged PR in the stack + if: github.event.pull_request.stack != null && github.event.pull_request.stack.base.ref == github.event.pull_request.base.ref + run: echo "Lowest unmerged PR in the stack" + + - name: Run for the top PR in the stack + if: github.event.pull_request.stack != null && github.event.pull_request.stack.position == github.event.pull_request.stack.size + run: echo "Top PR in the stack" +``` + +See [How can I optimize CI usage for a stack?](/gh-stack/faq/#how-can-i-optimize-ci-usage-for-a-stack) for more detail. diff --git a/skills/gh-stack/SKILL.md b/skills/gh-stack/SKILL.md index d5900216..a09c25dd 100644 --- a/skills/gh-stack/SKILL.md +++ b/skills/gh-stack/SKILL.md @@ -55,7 +55,7 @@ git config remote.pushDefault origin # if multiple remotes exist (skips remo 1. **Always supply branch names as positional arguments** to `init`, `add`, and `checkout`. Running these commands without arguments triggers interactive prompts. Branch names are used exactly as given — a name is never prefixed or transformed, so `gh stack add refactor/foo` creates a branch named `refactor/foo`. 2. **Always use `--auto` with `gh stack submit`** to auto-generate PR titles. Without `--auto`, `submit` prompts for a title for each new PR. 3. **Always use `--json` with `gh stack view`.** Without `--json`, the command launches an interactive TUI that cannot be operated by agents. There is no other appropriate flag — always pass `--json`. -4. **Use `--remote ` when multiple remotes are configured**, or pre-configure `git config remote.pushDefault origin`. Without this, `push`, `submit`, `sync`, `link`, and `checkout` trigger an interactive remote picker. +4. **Handle multiple remotes.** If more than one remote is configured, pre-configure `git config remote.pushDefault origin`, or pass `--remote ` to the commands that accept it: `push`, `submit`, `sync`, `rebase`, and `link`. `checkout`, `modify`, and `trunk` resolve a remote but have **no `--remote` flag** — they rely on `remote.pushDefault`. With multiple remotes and no configured default, these commands exit with an error in non-interactive mode. 5. **Avoid branches shared across multiple stacks.** If a branch belongs to multiple stacks, commands exit with code 6. Check out a non-shared branch first. 6. **Plan your stack layers by dependency order before writing code.** Foundational changes (models, APIs, shared utilities) go in lower branches; dependent changes (UI, consumers) go in higher branches. Think through the dependency chain before running `gh stack init`. 7. **Use standard `git add` and `git commit` for staging and committing.** This gives you full control over which changes go into each branch. The `-Am` shortcut is available but should not be the default approach—stacked PRs are most effective when each branch contains a deliberate, logical set of changes. @@ -68,7 +68,7 @@ git config remote.pushDefault origin # if multiple remotes exist (skips remo - ❌ `gh stack init` without branch arguments — always provide branch names - ❌ `gh stack add` without a branch name — always provide a branch name - ❌ `gh stack checkout` without an argument — always provide a PR number or branch name -- ❌ `gh stack checkout ` when a different local stack already exists on those branches — this triggers an unbypassable conflict resolution prompt; use `gh stack unstack` first to remove the local stack, then retry the checkout +- ❌ `gh stack checkout ` when a different local stack already exists on those branches — this triggers an unbypassable conflict resolution prompt; use `gh stack unstack --local` first to remove the local tracking state (this keeps the stack on GitHub intact), then retry the checkout ## Thinking about stack structure @@ -390,7 +390,7 @@ echo "$output" | jq '[.branches[] | .isMerged] | all' Use `unstack` to tear down the stack, make structural changes, then re-init: ```bash -# 1. Remove the stack (locally and on GitHub) +# 1. Remove the local tracking and the GitHub stack grouping (PRs are NOT deleted) gh stack unstack # 2. Make structural changes — e.g. delete a branch, reorder, rename @@ -733,6 +733,7 @@ gh stack view --json "base": "def5678...", "isCurrent": false, "isMerged": true, + "isQueued": false, "needsRebase": false, "pr": { "number": 42, @@ -746,6 +747,7 @@ gh stack view --json "base": "abc1234...", "isCurrent": true, "isMerged": false, + "isQueued": false, "needsRebase": false, "pr": { "number": 43, @@ -763,8 +765,9 @@ Fields per branch: - `base` — parent branch's HEAD SHA at last sync - `isCurrent` — whether this is the checked-out branch - `isMerged` — whether the PR has been merged +- `isQueued` — whether the PR is queued for merge (in a merge queue) - `needsRebase` — whether the base branch is not an ancestor (non-linear history) -- `pr` — PR metadata (omitted if no PR exists). `state` is `"OPEN"` or `"MERGED"`. +- `pr` — PR metadata (omitted if no PR exists). `state` is `"OPEN"`, `"MERGED"`, or `"QUEUED"`. --- @@ -779,6 +782,7 @@ gh stack down # Move down one branch (closer to trunk) gh stack down 2 # Move down two branches gh stack top # Jump to the top of the stack (furthest from trunk) gh stack bottom # Jump to the bottom (first non-merged branch above trunk) +gh stack trunk # Jump to the trunk branch (e.g. main) ``` Navigation clamps to stack bounds. Merged branches are skipped when navigating from active branches. @@ -787,23 +791,29 @@ Navigation clamps to stack bounds. Merged branches are skipped when navigating f ### Check out a stack — `gh stack checkout` -Check out a stack from a pull request number or branch name. **Always provide an argument** — running `gh stack checkout` without arguments triggers an interactive selection menu. +Check out a stack by stack number, pull request number, PR URL, or branch name. **Always provide an argument** — running `gh stack checkout` without arguments triggers an interactive selection menu. ``` -gh stack checkout +gh stack checkout ``` ```bash +# By stack number (the identifier shown in the GitHub stack UI) +gh stack checkout 7 + # By PR number (pulls from GitHub) gh stack checkout 42 +# By PR URL +gh stack checkout https://github.com/owner/repo/pull/42 + # By branch name (local only) gh stack checkout feature-auth ``` -When a PR number is provided (e.g. `123`), the command fetches the stack on GitHub, pulls the branches, and sets up the stack locally. If the stack already exists locally and matches, it switches to the branch. +A bare number is resolved as a **stack number first** (the identifier shown in the GitHub stack UI); if no stack has that number it is tried as a PR number, then a branch name. When a stack or PR number (or PR URL) is provided, the command fetches the stack on GitHub, pulls the branches, and sets up the stack locally. If the stack already exists locally and matches, it switches to the branch. -> **⚠️ Agent warning:** If the local and remote stacks have different branch compositions, this command triggers an interactive conflict-resolution prompt that cannot be bypassed with a flag. To avoid this: run `gh stack unstack` first to remove the conflicting local stack, then retry `gh stack checkout `. +> **⚠️ Agent warning:** If the local and remote stacks have different branch compositions, this command triggers an interactive conflict-resolution prompt that cannot be bypassed with a flag. To avoid this: run `gh stack unstack --local` first to remove the conflicting local tracking state (this keeps the stack on GitHub intact), then retry `gh stack checkout `. When a branch name is provided, the command resolves it against locally tracked stacks only. This is always safe for non-interactive use. @@ -813,6 +823,8 @@ When a branch name is provided, the command resolves it against locally tracked Tear down a stack so you can restructure it — remove a branch, reorder branches, rename branches, or make other large changes. After unstacking, use `gh stack init` to re-create the stack with the desired structure. +Unstacking only removes the stack grouping (on GitHub and/or locally); it never deletes the underlying pull requests or branches. + With no argument, the command targets the active stack — the one containing the currently checked out branch — unstacking it on GitHub and removing local tracking. Provide a stack number to unstack a specific stack on GitHub. This works from anywhere in the repository, whether or not the stack is checked out locally — the number is unstacked directly through the GitHub API (like `gh stack link`, no local tracking required). If the stack is also tracked locally, its local tracking is removed as well. @@ -822,7 +834,7 @@ gh stack unstack [] [flags] ``` ```bash -# Tear down the current stack (locally and on GitHub), then rebuild +# Tear down the current stack — removes local tracking and the GitHub grouping (PRs are NOT deleted), then rebuild gh stack unstack gh stack init --base main branch-2 branch-1 branch-3 # reordered @@ -860,13 +872,14 @@ gh stack unstack --local | 6 | Disambiguation required | A branch belongs to multiple stacks. Run `gh stack checkout ` to switch to a non-shared branch first | | 7 | Rebase already in progress | Run `gh stack rebase --continue` (after resolving conflicts) or `gh stack rebase --abort` to start over | | 8 | Stack is locked | Another `gh stack` process is writing the stack file. Wait and retry — the lock times out after 5 seconds | -| 9 | Stacked PRs unavailable | The repository does not have stacked PRs enabled. `submit` will offer to create regular (unstacked) PRs in interactive mode | +| 9 | Stacked PRs unavailable | The repository does not have stacked PRs enabled. Tell the user that stacks must be enabled on the repository first | +| 10 | Modify recovery required | A `gh stack modify` session was interrupted. This skill does not use `modify`, so agents should not produce this; if the repo is left in this state, run `gh stack modify --abort` to restore the pre-modify state | ## Known limitations 1. **Stacks are strictly linear.** Branching stacks (multiple children on a single parent) are not supported. Each branch has exactly one parent and at most one child. If you need parallel workstreams, use separate stacks. 2. **Stack disambiguation cannot be bypassed.** If the current branch is the trunk of multiple stacks, commands error with code 6. Check out a non-shared branch first. -3. **Multiple remotes require `--remote` or config.** If more than one remote is configured, pass `--remote ` or set `remote.pushDefault` in git config before running `push`, `sync`, or `rebase`. +3. **Multiple remotes require `--remote` or config.** If more than one remote is configured, set `remote.pushDefault` in git config, or pass `--remote ` to the commands that accept it (`push`, `submit`, `sync`, `rebase`, `link`). `checkout`, `modify`, and `trunk` have no `--remote` flag and rely on `remote.pushDefault`. 4. **Merging PRs:** Merging Stacked PRs from the CLI is not supported yet. Direct users to open the PR URL in a browser to merge PRs. -5. **Remote stack checkout requires a PR number.** `checkout` with a branch name only works with locally tracked stacks. Use a PR number (e.g. `gh stack checkout 123`) to pull stacks from GitHub. +5. **Remote stack checkout requires a stack or PR number.** `checkout` with a branch name only works with locally tracked stacks. Use a stack number or PR number (e.g. `gh stack checkout 7` or `gh stack checkout 123`) to pull a stack from GitHub. 6. **PR title and body are auto-generated.** There is no flag to set a custom PR title or body during `submit`. The title and body are generated from commit messages plus a footer. Use `gh pr edit` to modify PR title and body after creation.