Skip to content

Stacks CLI prototype with core operations#1

Merged
skarim merged 78 commits intomainfrom
skarim/gh-stack-prototype
Mar 25, 2026
Merged

Stacks CLI prototype with core operations#1
skarim merged 78 commits intomainfrom
skarim/gh-stack-prototype

Conversation

@skarim
Copy link
Copy Markdown
Collaborator

@skarim skarim commented Feb 18, 2026

gh stack CLI extension

A GitHub CLI extension for managing stacked branches and pull requests. Breaks large changes into small, reviewable layers with automated branch management, cascading rebases, and PR creation.


Local stack management

  • init [branches...] — Initialize a new stack. Supports interactive (prompts for branch names), explicit (creates branches from args), and adopt (--adopt) modes. Auto-detects trunk from origin/HEAD. Enables git rerere for conflict reuse. Prevents initializing inside an existing stack. Persists state to .git/gh-stack. Supports --prefix to set a branch name prefix and --numbered (with --prefix) for auto-incrementing branch names (prefix/01, prefix/02, …). In interactive mode, prompts for an optional prefix.
  • add [branch] — Add a new branch on top of the current stack. Must be run from the topmost branch. Supports staging and committing as part of the add flow: -A stages all changes (including untracked), -u stages tracked files only, and -m creates a commit with the given message. When -m is provided without an explicit branch name, the branch name is auto-generated (numbered format if the stack uses --numbered, otherwise date+slug format). On a branch with no commits yet, add -Am commits directly on it rather than creating a new one.
  • unstack [branch] — Remove a stack from local tracking and optionally from GitHub. With --local, only removes local metadata.

Viewing and navigation

  • view — Display the stack structure with branch status, PR links, and last commit info. Status indicators: merged, needs rebase, open PR. Pipes through pager (GIT_PAGER / PAGER / less -R). Supports --short (compact branch list) and --json (machine-readable output).
  • checkout [<pr-or-branch>] — Check out a locally tracked stack from a PR number or branch name. Interactive mode shows a menu of all locally available stacks. Currently limited to stacks created locally via gh stack init; server-side stack discovery is not yet implemented.
  • up [n], down [n], top, bottom — Navigate between branches in the stack. Clamps to bounds — moving past the top or bottom is a no-op with a message.

Rebasing

  • rebase [branch] — Cascading rebase of all branches in the stack. Fetches from origin first. Detects squash-merged PRs and uses git rebase --onto to avoid duplicate-patch conflicts. Scoping: --downstack (trunk to current), --upstack (current to top). Conflict handling: pauses and prints conflicted files with line numbers; resume with --continue, undo with --abort (restores all branches to pre-rebase SHAs). Leverages git rerere to auto-resolve previously seen conflicts. Supports --remote to specify which remote to fetch from.

Remote operations & PR workflow

  • push — Push all branches and create/update pull requests. Uses --force-with-lease by default for safety after rebases. Creates PRs with branch-chaining (each PR targets the branch below it). Prompts for PR titles on new PRs (press Enter for branch name default, or use --auto to skip prompting). Supports --draft, --skip-prs (push branches without creating/updating PRs), and --remote.
  • sync — All-in-one fetch → fast-forward trunk → cascade rebase → push → sync PR state. Performs each step safely: fast-forward is skipped if trunk has diverged, rebase only runs if trunk moved, push uses --force-with-lease after rebase. On conflict, aborts and restores all branches, directing the user to gh stack rebase for manual resolution. Supports --remote.

Planned (not yet implemented)

  • merge <pr> — Merge a stack of PRs. Currently prints a notice that it is not yet implemented.

Other

  • feedback [title] — Opens the gh-stack GitHub Discussions page to submit feedback.

Abbreviated workflow

With --prefix + --numbered on init and -Am on add, the typical workflow collapses to minimal keystrokes — no need to name branches, run git add, or run git commit separately:

gh stack init -p feat --numbered   # creates feat/01
# ... write code ...
gh stack add -Am "Auth middleware"  # commits on feat/01 (no commits yet)
# ... write code ...
gh stack add -Am "API routes"      # creates feat/02, commits there
gh stack push                      # pushes all, creates PRs

Internals

  • Persistence — Stack state stored as JSON in .git/gh-stack (schema version 1). Tracks repository context (host:owner/repo), trunk, branches with merge-base SHAs, and PR metadata (number, GraphQL ID, URL, merged status). Rebase state stored in .git/gh-stack-rebase-state.
  • Git operations — Thin wrapper over cli/cli/v2/git.Client. Custom support for --force-with-lease push, three-argument rebase --onto, rerere auto-continue, conflict marker detection, and fast-forward merge.
  • GitHub API — GraphQL for PR queries/mutations (FindPRForBranch, CreatePR, UpdatePRBase). REST fallback. Auth via GitHub CLI defaults.
  • Config — Colored terminal output (auto-disabled without TTY). Structured output methods (Successf, Errorf, Warningf, Infof).
  • Exit codes — Differentiated codes: 0 success, 1 generic, 2 not-in-stack, 3 conflict, 4 API failure, 5 invalid args, 6 disambiguation, 7 rebase in progress.
  • Squash-merge handling — Detects merged PRs via synced PR state. Triggers --onto mode for subsequent rebases to replay only unique commits.

Copilot AI review requested due to automatic review settings February 18, 2026 10:01
@skarim skarim changed the title Stack CLI prototype with local stack management and API placeholders Stacks CLI prototype with core operations Feb 18, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an initial gh stack CLI prototype, including local stack state persistence in .git/gh-stack, basic stack operations (init/add/view/update/navigation), and GitHub PR create/update plumbing with placeholders for stack-aware APIs.

Changes:

  • Adds local stack state model + persistence and a git helper layer used by commands.
  • Implements core CLI commands (init, add, view, update, push, navigation) plus placeholder commands (checkout, merge, hidden stubs).
  • Wires the extension entrypoint to the Cobra command tree and adds dependencies for CLI UX and GitHub API access.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
main.go Switches entrypoint to execute the Cobra CLI.
go.mod Updates module path and adds required CLI/GitHub dependencies.
go.sum Adds checksums for new dependencies.
cmd/root.go Defines the stack root command and registers subcommands.
cmd/init.go Initializes/adopts a stack and persists state in .git/gh-stack.
cmd/add.go Adds a new branch on top of the current stack.
cmd/navigate.go Adds stack navigation commands (up/down/top/bottom).
cmd/view.go Displays stack state (short/full) and optionally opens PRs in browser.
cmd/update.go Implements fetch + cascading rebase with conflict state persistence.
cmd/push.go Pushes branches and creates/updates PRs (stack-linking is a stub).
cmd/checkout.go Placeholder for future stack checkout workflow.
cmd/merge.go Placeholder for future stack merge workflow.
cmd/placeholder.go Hidden placeholder commands for planned features.
internal/stack/stack.go Stack data structures + JSON load/save in .git/gh-stack.
internal/git/git.go Git helper wrapper around cli/v2/git plus custom helpers.
internal/config/config.go Shared config for terminal IO and colorized output.
internal/github/github.go GitHub API client wrapper (GraphQL PR find/create/update).
Comments suppressed due to low confidence (2)

cmd/update.go:327

  • abortUpdate ignores errors from checkout/reset for each branch, which can leave the repo partially restored while still reporting success. Consider handling errors (at least collecting/reporting them and returning a non-zero error) so users know when a restore did not fully succeed.
	for branch, sha := range state.OriginalRefs {
		_ = git.CheckoutBranch(branch)
		_ = git.ResetHard(sha)
	}

cmd/update.go:339

  • saveRebaseState discards JSON marshal/write errors. If the state file can’t be written, --continue/--abort won’t work but the user won’t know why. Return an error from saveRebaseState and surface it where it’s called (especially on the first conflict).
func saveRebaseState(gitDir string, state *rebaseState) {
	data, _ := json.MarshalIndent(state, "", "  ")
	_ = os.WriteFile(filepath.Join(gitDir, rebaseStateFile), data, 0644)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/git/git.go Outdated
Comment thread cmd/navigate.go Outdated
Comment thread cmd/push.go Outdated
Comment thread cmd/init.go Outdated
Comment thread cmd/rebase.go Outdated
Comment thread cmd/view.go Outdated
Comment thread internal/github/github.go
Comment thread cmd/view.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR turns gh-stack into a functional GitHub CLI extension prototype by introducing the core command set, local stack persistence, and supporting git/GitHub API wrappers.

Changes:

  • Replace the previous main.go prototype with a Cobra-based gh stack command tree (init/add/view/rebase/push/sync/unstack + placeholders).
  • Add local stack persistence (.git/gh-stack) with a JSON schema and associated stack model/helpers.
  • Introduce internal wrappers for git operations and GitHub API calls to support PR discovery/creation and rebasing workflows.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
main.go Switches entrypoint to execute the Cobra CLI.
go.mod Updates module path and adds dependencies for CLI + TUI + GitHub operations.
go.sum Adds/upgrades dependency checksums consistent with new imports.
README.md Documents installation, concepts, and the new command set/workflows.
internal/stack/stack.go Implements stack persistence model + load/save helpers for .git/gh-stack.
internal/stack/schema.json Defines JSON schema for the stack state file.
internal/github/github.go Adds GitHub client wrapper (GraphQL/REST) for PR operations.
internal/git/git.go Adds git wrapper utilities for branching, pushing, rebasing, and conflict detection.
internal/config/config.go Adds shared config for IO, terminal detection, and styled output.
cmd/root.go Adds root Cobra command and wires up subcommands.
cmd/init.go Implements local stack initialization (interactive/explicit/adopt).
cmd/add.go Implements adding a new branch at the top of the current stack.
cmd/view.go Implements stack visualization (short/full) + pager + open-in-browser.
cmd/rebase.go Implements cascading rebase with conflict state persistence and resume/abort.
cmd/push.go Implements pushing stack branches and creating/updating chained PRs.
cmd/sync.go Implements fetch → trunk FF → cascade rebase → push → PR sync workflow.
cmd/unstack.go Implements removing a stack from local tracking and (placeholder) GitHub deletion.
cmd/navigate.go Implements stack navigation commands (up/down/top/bottom).
cmd/utils.go Adds stack disambiguation + PR metadata syncing helpers.
cmd/placeholder.go Adds hidden placeholder commands for planned features.
cmd/checkout.go Adds placeholder for stack checkout workflow.
cmd/merge.go Adds placeholder for stack merge workflow.
cmd/feedback.go Adds command to open a Discussions feedback form.
Comments suppressed due to low confidence (2)

cmd/utils.go:33

  • This message includes a trailing newline, but Warningf already prints one. This will produce an extra blank line in the output; remove the \n from the format string.
	cfg.Warningf("Branch %q is the trunk of multiple stacks\n", branch)

cmd/view.go:306

  • runView already synced PR state before invoking viewWeb, but viewWeb still performs a GitHub API lookup for every branch (FindPRForBranch). This is redundant and can significantly slow down --web for large stacks. Prefer opening br.PullRequest.URL when present, and only querying when it’s missing.
	opened := 0
	for _, br := range s.Branches {
		pr, err := client.FindPRForBranch(br.Branch)
		if err != nil || pr == nil {
			continue
		}
		url := fmt.Sprintf("https://github.com/%s/%s/pull/%d", repo.Owner, repo.Name, pr.Number)
		if err := b.Browse(url); err != nil {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread cmd/utils.go Outdated
Comment thread cmd/add.go Outdated
Comment thread cmd/feedback.go Outdated
Comment thread cmd/rebase.go Outdated
Comment thread cmd/view.go
Comment thread cmd/sync.go Outdated
Comment thread cmd/root.go
Comment thread cmd/utils.go
Comment thread cmd/merge.go Outdated
Comment thread internal/stack/stack.go
Copy link
Copy Markdown

Copilot AI commented Mar 12, 2026

Warning

This is an internal experiment to assess Copilot's ability to auto-approve PRs. Please 👍 this comment if the assessment below is correct and 👎 if not. Feedback in #f-ccr-auto-approve is appreciated!

Copilot thinks this PR is not ready to merge — see review comments for details.

skarim and others added 8 commits March 22, 2026 09:18
When Ctrl+C is pressed during a prompter interaction, the CLI now prints
a friendly 'Received interrupt, aborting operation' message instead of
ugly wrapped errors like 'failed to read prefix: could not prompt:
interrupt'.

Changes:
- Add isInterruptError(), printInterrupt(), and errInterrupt sentinel
  to cmd/utils.go for centralized interrupt detection
- Update all 8 prompt sites (init, push, checkout, utils) to detect
  survey's terminal.InterruptErr and exit cleanly
- Update callers of resolveStack, pickRemote, and ensureRerere to
  propagate interrupt without double-printing errors
- Change ensureRerere signature to return error so callers can abort
  on interrupt
- Add tests for interrupt detection helpers

Closes td-746bdc

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@skarim skarim force-pushed the skarim/gh-stack-prototype branch from fdce698 to 426ad11 Compare March 23, 2026 02:28
@skarim skarim requested a review from georgebrock March 23, 2026 08:50
@skarim skarim merged commit de20682 into main Mar 25, 2026
7 checks passed
@skarim skarim deleted the skarim/gh-stack-prototype branch March 25, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants