mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
68ce60c760
* merge cmd
* Refine the merge TUI and simplify the async-merge client
Follow-up polish for `gh stack merge` (the command itself landed in the
previous commit). These changes refine the interactive wizard, enrich the
PR picker, and replace the merge client's bespoke HTTP handling with the
standard go-gh REST client.
Wizard and stepper:
- Redesign the top stepper as a segmented bar: completed steps are green,
the active step is the brightest, and upcoming steps are dimmed. Steps
are separated by a Powerline arrow that blends into the shading, with a
graceful fallback to abutting segments on terminals that lack the glyph
(e.g. Apple Terminal). Set GH_STACK_POWERLINE=1/0 to override detection.
- Show the stack number in the header ("Merge stack #123").
- Hide the header and stepper once the merge is submitted so the live
progress view stands on its own.
PR picker:
- Render each pull request on two lines: the title (white/black, a touch
bolder when selected) above its "#number • branch" (gray, fainter when
deselected). Titles are fetched in one batched GraphQL query (PRTitles)
and fall back to the branch name.
- Scroll long stacks in a fixed 10-item window with persistent "N more"
indicators, so the list no longer jumps as those hints appear and
disappear. Add shift+up / shift+down to jump to the top or bottom.
Progress and outcome:
- Always render a status line ("Submitting merge request...") so it does
not pop in later and shift the view, and normalize messages to end in an
ellipsis.
- Print the final result from the command layer rather than the TUI: a
success line that includes the merge commit SHA
("Merged #1, #2 into main (abc1234)"), an atomic-rollback note on
failure, a distinct message when the user stops watching an in-flight
merge, and "Cancelled operation, nothing merged" on cancel.
- Clamp every rendered line to the terminal width so resizing no longer
leaves duplicated header lines behind, and make truncation ANSI-aware.
Async-merge client:
- Use the go-gh REST client (c.rest.Put / c.rest.Get) for both the submit
and poll endpoints, removing the bespoke http.Client, base-URL helper,
and manual response decoding. The REST client discards non-2xx bodies,
but that only costs the rare 400 message and 409 UUID: real merge
failures still surface through the 200 poll body, and the in-range PRs
are validated open, non-draft, and non-merged before submitting.
- Add classifyAsyncMergeError to map status codes to clear errors (404
unavailable, 409 already exists, 400 no longer mergeable) and drop the
now-unused AsyncMergeResult.StatusCode field. Rework the client tests to
drive the REST client through a stub http.RoundTripper.
* warn merge queue unsupported
* update for new status field from api
* merge cmd docs
* more helpful error msgs
* update to support merge queue
* addressing review comments
* hide merge method step for merge queue
* set merge action explicitly
* address review comments to clarify docs on merge/api behavior
7.7 KiB
7.7 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 (18 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, merge |
| 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): 18 methods for GitHub API (PRs, stacks, merges). Stack operations use the public Stacks REST API (/repos/{owner}/{repo}/stacks):ListStacks,FindStackForPR,GetStack,CreateStack,AddToStack(delta append),Unstack. Async stack merges useRepoMergeConfig(GraphQL: allowed merge methods + viewer's default),BaseBranchUsesMergeQueue(GraphQL: detects a base-branch merge queue to select the explicitmerge_action),MergeStackAsync, andGetAsyncMergeResult(/repos/{owner}/{repo}/pulls/{n}/merge-async). 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).