mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
38aba1b710
* deprecate prefix functionality * clean up branch auto-naming * update docs * preserve literal hyphens when slugifying branch names
7.4 KiB
7.4 KiB
gh-stack: Agent Instructions
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.
Build, test, and validate
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 detection
Always 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.
Install locally as a gh extension
go build -o gh-stack .
gh extension remove stack 2>/dev/null
gh extension install .
Project structure
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 (13 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)
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)
Command groups (registered in cmd/root.go)
| 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 |
Coding patterns
Command structure
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>.
Error handling
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) { ... }
Testing patterns
- 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.
Key interfaces
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): 13 methods for GitHub API (PRs, stacks). Stack operations use the public Stacks REST API (/repos/{owner}/{repo}/stacks):ListStacks,FindStackForPR,GetStack,CreateStack,AddToStack(delta append),Unstack. 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,RepoOverride).
Stack file
- Location:
.git/gh-stack(JSON format, schema version 1). - Schema:
internal/stack/schema.json. - Identity: each stack stores GitHub's global
id(string) and repo-scopednumber(int, shown in the GitHub UI and used as the primary way to reference a stack, e.g.gh stack checkout <number>).numbermay be0for stack files created before it was tracked; it is backfilled from the API on the next stack operation. - Locking: Exclusive file lock at
.git/gh-stack.lockwith 5-second timeout. Errors surface asLockError. - Staleness: Concurrent modifications detected via
StaleError.
CI workflows (.github/workflows/)
| 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 |
Non-obvious things
- 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).