Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Stack metadata is stored in `.git/gh-stack` (a JSON file, not committed to the r
Initialize a new stack in the current repository.

```
gh stack init [branches...] [flags]
gh stack init [flags] [branches...]
```

Creates an entry in `.git/gh-stack` to track stack state. In interactive mode (no arguments), prompts you to name branches and offers to use the current branch as the first layer. In interactive mode, you'll also be prompted to set an optional branch prefix (unless adopting existing branches). When a prefix is set, branch names you enter are automatically prefixed. When explicit branch names are given, creates any that don't already exist (branching from the trunk). The trunk defaults to the repository's default branch unless overridden with `--base`.
Expand Down Expand Up @@ -115,7 +115,7 @@ gh stack init -p feat --numbered
Add a new branch on top of the current stack.

```
gh stack add [branch] [flags]
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.
Expand Down Expand Up @@ -190,7 +190,7 @@ gh stack checkout
Pull from remote and do a cascading rebase across the stack.

```
gh stack rebase [branch] [flags]
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 squash-merged, the rebase automatically switches to `--onto` mode to correctly replay commits on top of the merge target.
Expand Down Expand Up @@ -331,7 +331,7 @@ gh stack view --json
Remove a stack from local tracking and delete it on GitHub. Also available as `gh stack delete`.

```
gh stack unstack [branch] [flags]
gh stack unstack [flags] [branch]
```

If no branch is specified, uses the current branch to find the stack. Deletes the stack on GitHub first, then removes local tracking. Use `--local` to only remove the local tracking entry.
Expand Down Expand Up @@ -414,7 +414,7 @@ gh stack feedback "Support for reordering branches"
Create a short command alias so you can type less.

```
gh stack alias [name] [flags]
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`.
Expand Down
11 changes: 7 additions & 4 deletions cmd/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (
"github.com/spf13/cobra"
)

const feedbackBaseURL = "https://github.com/github/gh-stack/discussions/new?category=feedback"
const (
feedbackURL = "https://gh.io/stacks-feedback"
feedbackFormURL = "https://gh.io/stacks-feedback-form"
)

func FeedbackCmd(cfg *config.Config) *cobra.Command {
cmd := &cobra.Command{
Expand All @@ -25,15 +28,15 @@ func FeedbackCmd(cfg *config.Config) *cobra.Command {
}

func runFeedback(cfg *config.Config, args []string) error {
feedbackURL := feedbackBaseURL
targetURL := feedbackURL

if len(args) > 0 {
title := strings.Join(args, " ")
feedbackURL += "&title=" + url.QueryEscape(title)
targetURL = feedbackFormURL + "?title=" + url.QueryEscape(title)
}

b := browser.New("", cfg.Out, cfg.Err)
if err := b.Browse(feedbackURL); err != nil {
if err := b.Browse(targetURL); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func generatePRBody(commitBody string) string {

footer := fmt.Sprintf(
"<sub>Stack created with <a href=\"https://github.com/github/gh-stack\">GitHub Stacks CLI</a> • <a href=\"%s\">Give Feedback 💬</a></sub>",
feedbackBaseURL,
feedbackURL,
)
parts = append(parts, footer)

Expand Down
2 changes: 1 addition & 1 deletion cmd/submit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestGeneratePRBody(t *testing.T) {
commitBody: "",
wantContains: []string{
"GitHub Stacks CLI",
feedbackBaseURL,
feedbackURL,
"<sub>",
},
},
Expand Down
3 changes: 3 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default defineConfig({
head: [
{ tag: 'meta', attrs: { name: 'robots', content: 'noindex, nofollow' } },
],
components: {
SocialIcons: './src/components/CustomHeader.astro',
},
customCss: [
'./src/styles/custom.css',
],
Expand Down
Binary file not shown.
Binary file modified docs/src/assets/screenshots/stack-merge-box.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
167 changes: 167 additions & 0 deletions docs/src/components/CustomHeader.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
import type { Props } from '@astrojs/starlight/props';
import Default from '@astrojs/starlight/components/SocialIcons.astro';
const base = import.meta.env.BASE_URL;
Comment thread
skarim marked this conversation as resolved.
Outdated
---

<nav class="custom-header-links" aria-label="Primary navigation">
<a href={`${base}introduction/overview/`} class="header-link">Overview</a>
<a href={`${base}getting-started/quick-start/`} class="header-link">Quick Start</a>
<a href={`${base}reference/cli/`} class="header-link">CLI</a>
<a href={`${base}guides/ui/`} class="header-link">UI</a>
<a href={`${base}faq/`} class="header-link">FAQ</a>
</nav>

<!-- Hamburger menu for narrow viewports where full nav would overflow -->
<div class="tablet-nav-wrapper">
<button class="hamburger-btn" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="tablet-nav-dropdown">
<span class="hamburger-icon" aria-hidden="true"></span>
</button>
<nav class="tablet-dropdown" id="tablet-nav-dropdown" aria-label="Primary navigation" hidden>
<a href={`${base}introduction/overview/`} class="dropdown-link">Overview</a>
<a href={`${base}getting-started/quick-start/`} class="dropdown-link">Quick Start</a>
<a href={`${base}reference/cli/`} class="dropdown-link">CLI</a>
<a href={`${base}guides/ui/`} class="dropdown-link">UI</a>
<a href={`${base}faq/`} class="dropdown-link">FAQ</a>
</nav>
</div>

<Default {...Astro.props} />

<style>
.custom-header-links {
display: flex;
align-items: center;
gap: 1rem;
margin-right: 1rem;
}

.header-link {
color: var(--sl-color-text);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
padding: 0.25rem 0.75rem;
border-radius: 4px;
transition: color 0.15s ease, background-color 0.15s ease;
white-space: nowrap;
}

.header-link:hover {
color: var(--sl-color-text-accent);
background-color: rgba(110, 118, 129, 0.1);
}

/* Tablet navigation — hidden by default; shown via global CSS at narrow widths */
.tablet-nav-wrapper {
display: none;
position: relative;
margin-right: 0.5rem;
}

.hamburger-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
color: var(--sl-color-text);
border-radius: 4px;
min-height: 44px;
min-width: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}

.hamburger-btn:hover {
background-color: rgba(110, 118, 129, 0.1);
}

.hamburger-icon {
display: block;
width: 20px;
height: 2px;
background: currentColor;
position: relative;
}

.hamburger-icon::before,
.hamburger-icon::after {
content: '';
display: block;
width: 20px;
height: 2px;
background: currentColor;
position: absolute;
left: 0;
}

.hamburger-icon::before { top: -6px; }
.hamburger-icon::after { top: 6px; }

.tablet-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background: var(--sl-color-bg-nav, #161b22);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 8px;
padding: 0.5rem 0;
min-width: 180px;
z-index: 100;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

.dropdown-link {
display: block;
padding: 0.625rem 1rem;
color: var(--sl-color-text);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: background-color 0.15s ease;
}

.dropdown-link:hover {
background-color: rgba(110, 118, 129, 0.1);
color: var(--sl-color-text-accent);
}
</style>

<script>
function initHamburgerMenu() {
const hamburgerBtn = document.querySelector<HTMLButtonElement>('.hamburger-btn');
const tabletDropdown = document.querySelector<HTMLElement>('.tablet-dropdown');

Comment thread
skarim marked this conversation as resolved.
if (!hamburgerBtn || !tabletDropdown) return;

hamburgerBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = hamburgerBtn.getAttribute('aria-expanded') === 'true';
hamburgerBtn.setAttribute('aria-expanded', String(!isOpen));
tabletDropdown.hidden = isOpen;
if (!isOpen) {
const firstLink = tabletDropdown.querySelector<HTMLAnchorElement>('.dropdown-link');
firstLink?.focus();
}
});

document.addEventListener('click', (e) => {
if (!hamburgerBtn.contains(e.target as Node) && !tabletDropdown.contains(e.target as Node)) {
hamburgerBtn.setAttribute('aria-expanded', 'false');
tabletDropdown.hidden = true;
}
});

document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && hamburgerBtn.getAttribute('aria-expanded') === 'true') {
hamburgerBtn.setAttribute('aria-expanded', 'false');
tabletDropdown.hidden = true;
hamburgerBtn.focus();
}
});
}

initHamburgerMenu();
document.addEventListener('astro:page-load', initHamburgerMenu);
</script>
Comment thread
skarim marked this conversation as resolved.
12 changes: 10 additions & 2 deletions docs/src/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { defineCollection } from 'astro:content';
import { defineCollection, z } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({
loader: docsLoader(),
schema: docsSchema(),
schema: docsSchema({
extend: z.object({
banner: z.object({
content: z.string(),
}).default({
content: 'Stacked PRs is currently in private preview. <a href="https://gh.io/stacksbeta">Sign up for the waitlist →</a>',
}),
}),
}),
}),
};
2 changes: 1 addition & 1 deletion docs/src/content/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Yes, you can continue to use your tool of choice (e.g. jj, Sapling, ghstack, git

Stacked PRs on GitHub are based on the standard pull request model — any tool that creates PRs with the correct base branches can work with them. The `gh stack` CLI is purpose-built for the GitHub experience, but other tools that manage branch chains should be compatible.

You can also use the GitHub CLI in conjunction with other tools to simply create a stack of PRs:
You can also use the GitHub CLI in conjunction with other tools to open your PRs as a stack:

```bash
# Create a stack of branches locally using jj
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Before a PR in the stack can be merged, the following conditions must be met:
- **The stack must be fully rebased** with a linear history
- **The current PR itself** must meet all branch protection requirements for the stack base

![Merge box showing failing stack requirements](../../../assets/screenshots/merge-box-failing-stack-requirements.png)
![Merge box for a stacked pull request](../../../assets/screenshots/stack-merge-box.png)

## Unstacking

Expand Down
17 changes: 10 additions & 7 deletions docs/src/content/docs/guides/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ gh stack rebase
# 7. Push the updated branches
gh stack push

# 8. When the first PR is merged, sync the stack
# 8. Sync upstream changes as PRs get merged
gh stack sync
```

Expand All @@ -42,34 +42,37 @@ gh stack sync
For speed, use a branch prefix with `--numbered` and the `-Am` flags to fold staging, committing, and branch creation into a single command. Branch names are auto-generated as `prefix/01`, `prefix/02`, etc.

```sh
# Alias `gh stack` as `gs` for easier use
gh stack alias

# 1. Start a stack with numbered branches
gh stack init -p feat --numbered
gs init -p feat --numbered
# → creates feat/01 and checks it out

# 2. Write code for the first layer
# ... write code ...

# 3. Stage and commit on the current branch
gh stack add -Am "Auth middleware"
gs add -Am "Auth middleware"
# → feat/01 has no commits yet, so the commit lands here

# 4. Write code for the next layer
# ... write code ...

# 5. Create the next branch and commit
gh stack add -Am "API routes"
gs add -Am "API routes"
# → feat/01 already has commits, so feat/02 is created

# 6. Keep going
# ... write code ...
gh stack add -Am "Frontend components"
gs add -Am "Frontend components"
# → creates feat/03

# 7. Push everything and create PRs
gh stack submit
gs submit
```

Each `gh stack add -Am "..."` stages all files, commits, and (if the current branch already has commits) creates a new branch — no separate `git add` or `git commit` needed.
Each `gs add -Am "..."` stages all files, commits, and (if the current branch already has commits) creates a new branch — no separate `git add` or `git commit` needed.

## Making Mid-Stack Changes

Expand Down
Loading
Loading