A GitHub CLI (gh) extension for managing stacked branches and pull requests. Written in Go, it automates creating branches, keeping them rebased, setting PR base branches, and navigating between stack layers.
go mod download # install dependencies
go build -o gh-stack . # build (produces ./gh-stack binary)
go vet ./... # static analysis. Run before tests.
go test -race -count=1 ./... # all tests with race detectionAlways run go vet before go test. CI runs both on every push/PR across ubuntu, windows, and macOS (test.yml).
There is no Makefile, linter config, or code generation step. The standard Go toolchain is all that's needed.
go build -o gh-stack .
gh extension remove stack 2>/dev/null
gh extension install .main.go # entrypoint. Calls cmd.Execute().
cmd/ # Cobra commands (one file per command + tests)
root.go # registers all subcommands in four groups
utils.go # shared helpers, ExitError types, exit codes
internal/
git/ # git.Ops interface + defaultOps (exec-based)
gitops.go # Ops interface (52 methods)
mock_ops.go # MockOps. Each method has a corresponding *Fn field.
github/ # github.ClientOps interface + real Client
client_interface.go # ClientOps interface (11 methods)
mock_client.go # MockClient. Uses function-pointer fields for testing.
stack/ # stack file (.git/gh-stack) management, JSON schema, locking
schema.json # JSON Schema for the stack file format
config/ # Config struct (I/O, colors, test overrides)
testing.go # NewTestConfig(). Returns *Config + stdout/stderr pipes.
branch/ # branch naming (Slugify, DateSlug, NextNumberedName)
modify/ # interactive stack modification state machine
pr/ # PR template discovery
tui/ # bubbletea/bubbles/lipgloss terminal UI
stackview/ # interactive stack visualization
modifyview/ # interactive modify session UI
shared/ # shared TUI types
docs/ # Astro + Starlight documentation site
skills/ # AI agent skill definition (SKILL.md)
| Group | Commands |
|---|---|
| Stack management | init, add, view, checkout, modify, unstack |
| Remote operations | submit, sync, rebase, push, link |
| Navigation | switch, up, down, top, bottom, trunk |
| Utilities | alias, feedback |
Each command lives in its own file (cmd/<name>.go) and follows this pattern:
- Define an
<name>Optionsstruct for flags/args. - Export a
<Name>Cmd(cfg *config.Config) *cobra.Commandconstructor. - Implement logic in a private
run<Name>(cfg, opts, args)function. - The
RunEfield on the command callsrun<Name>.
Use typed exit codes defined in cmd/utils.go:
| Code | Sentinel | Meaning |
|---|---|---|
| 1 | ErrSilent |
Error already printed |
| 2 | ErrNotInStack |
Branch/stack not found |
| 3 | ErrConflict |
Rebase conflict |
| 4 | ErrAPIFailure |
GitHub API error |
| 5 | ErrInvalidArgs |
Invalid arguments or flags |
| 6 | ErrDisambiguate |
Multiple stacks/remotes, can't auto-select |
| 7 | ErrRebaseActive |
Rebase already in progress |
| 8 | ErrLockFailed |
Stack file lock contention |
| 9 | ErrStacksUnavailable |
Stacked PRs not enabled for repository |
| 10 | ErrModifyRecovery |
Modify session interrupted |
Return these from RunE. Never call os.Exit() directly from commands. Check with:
var exitErr *ExitError
if errors.As(err, &exitErr) { ... }- Framework:
stretchr/testify(assert,require) for assertions. - Table-driven tests are the norm. See
cmd/utils_test.gofor examples. - Config: Use
config.NewTestConfig()which returns(*Config, stdoutReader, stderrReader)with captured I/O and no-op color functions. - Git mocking: Call
git.SetOps(&git.MockOps{...}). It returns a restore function. Alwaysdefer restore()to prevent test pollution. - GitHub mocking: Set
cfg.GitHubClientOverride = &github.MockClient{...}. - Prompt mocking: Set
cfg.SelectFn,cfg.ConfirmFn, orcfg.InputFnon the config to simulate interactive input. - Stack file setup: Use
stack.Load(dir)after writing a stack file to get correct checksums forSave.
git.Ops(internal/git/gitops.go): 52 methods wrapping git CLI calls. The production implementation usescli/go-gh'sclient.Command()viarun()andrunSilent()helpers. Package-level functions (e.g.,git.CurrentBranch()) delegate to a swappable package-levelopsvariable.github.ClientOps(internal/github/client_interface.go): 11 methods for GitHub API (PRs, stacks). Injected viacfg.GitHubClientOverridein tests.config.Config(internal/config/config.go): Central configuration passed to all commands. Holds I/O streams, color functions, and test hook fields (SelectFn,ConfirmFn,InputFn,TokenForHostFn,RepoOverride).
- Location:
.git/gh-stack(JSON format, schema version 1). - Schema:
internal/stack/schema.json. - Locking: Exclusive file lock at
.git/gh-stack.lockwith 5-second timeout. Errors surface asLockError. - Staleness: Concurrent modifications detected via
StaleError.
| Workflow | Trigger | What it does |
|---|---|---|
test.yml |
push to main, PRs | go vet + go test -race -count=1 ./... on 3 OS matrix |
release.yml |
v* tags |
Cross-platform precompiled binaries via cli/gh-extension-precompile |
docs.yml |
push to main (docs/**) | Builds Astro/Starlight docs, deploys to GitHub Pages |
- The
Queuedfield onBranchRefis transient (populated from GitHub API, never persisted to the stack JSON file). git.SetOps()replaces the package-level ops variable. Forgettingdefer restore()in a test will break every subsequent test in the package.- Interrupt detection: Ctrl+C is caught as
terminal.InterruptErr, wrapped into anerrInterruptsentinel, and printed with a friendly message before a silent exit. - Rerere: on first rebase conflict, the user is prompted to enable
git rerere. If declined, a flag file prevents future prompts.tryAutoResolveRebase()loops up to 1000 times auto-continuing when rerere resolves conflicts. - The
.gitignoreignores/gh-stackand/gh-stack.exe(the built binary).