|
| 1 | +# gh-stack — Agent Instructions |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Build, test, and validate |
| 6 | + |
| 7 | +```sh |
| 8 | +go mod download # install dependencies |
| 9 | +go build ./... # build (produces ./gh-stack binary) |
| 10 | +go vet ./... # static analysis — run before tests |
| 11 | +go test -race -count=1 ./... # all tests with race detection |
| 12 | +``` |
| 13 | + |
| 14 | +Always run `go vet` before `go test`. CI runs both on every push/PR across ubuntu, windows, and macOS (`test.yml`). |
| 15 | + |
| 16 | +There is no Makefile, linter config, or code generation step. The standard Go toolchain is all that's needed. |
| 17 | + |
| 18 | +### Install locally as a `gh` extension |
| 19 | + |
| 20 | +```sh |
| 21 | +go build -o gh-stack . |
| 22 | +gh extension remove stack 2>/dev/null |
| 23 | +gh extension install . |
| 24 | +``` |
| 25 | + |
| 26 | +## Project structure |
| 27 | + |
| 28 | +``` |
| 29 | +main.go # entrypoint — calls cmd.Execute() |
| 30 | +cmd/ # Cobra commands (one file per command + tests) |
| 31 | + root.go # registers all subcommands in four groups |
| 32 | + utils.go # shared helpers, ExitError types, exit codes |
| 33 | +internal/ |
| 34 | + git/ # git.Ops interface + defaultOps (exec-based) |
| 35 | + gitops.go # Ops interface (50+ methods) |
| 36 | + mock_ops.go # MockOps — each method has a corresponding *Fn field |
| 37 | + github/ # github.ClientOps interface + real Client |
| 38 | + client_interface.go # ClientOps interface (12 methods) |
| 39 | + mock_client.go # MockClient — function-pointer fields for testing |
| 40 | + stack/ # stack file (.git/gh-stack) management, JSON schema, locking |
| 41 | + schema.json # JSON Schema for the stack file format |
| 42 | + config/ # Config struct (I/O, colors, test overrides) |
| 43 | + testing.go # NewTestConfig() — returns *Config + stdout/stderr pipes |
| 44 | + branch/ # branch naming (Slugify, DateSlug, NextNumberedName) |
| 45 | + modify/ # interactive stack modification state machine |
| 46 | + pr/ # PR template discovery |
| 47 | + tui/ # bubbletea/bubbles/lipgloss terminal UI |
| 48 | + stackview/ # interactive stack visualization |
| 49 | + modifyview/ # interactive modify session UI |
| 50 | + shared/ # shared TUI types |
| 51 | +docs/ # Astro + Starlight documentation site |
| 52 | +skills/ # AI agent skill definition (SKILL.md) |
| 53 | +``` |
| 54 | + |
| 55 | +### Command groups (registered in `cmd/root.go`) |
| 56 | + |
| 57 | +| Group | Commands | |
| 58 | +|-------|----------| |
| 59 | +| Stack management | `init`, `add`, `view`, `checkout`, `modify`, `unstack` | |
| 60 | +| Remote operations | `submit`, `sync`, `rebase`, `push`, `link` | |
| 61 | +| Navigation | `switch`, `up`, `down`, `top`, `bottom`, `trunk` | |
| 62 | +| Utilities | `alias`, `feedback` | |
| 63 | + |
| 64 | +## Coding patterns |
| 65 | + |
| 66 | +### Command structure |
| 67 | + |
| 68 | +Each command lives in its own file (`cmd/<name>.go`) and follows this pattern: |
| 69 | + |
| 70 | +1. Define an `<name>Options` struct for flags/args. |
| 71 | +2. Export a `<Name>Cmd(cfg *config.Config) *cobra.Command` constructor. |
| 72 | +3. Implement logic in a private `run<Name>(cfg, opts, args)` function. |
| 73 | +4. The `RunE` field on the command calls `run<Name>`. |
| 74 | + |
| 75 | +### Error handling |
| 76 | + |
| 77 | +Use typed exit codes defined in `cmd/utils.go`: |
| 78 | + |
| 79 | +| Code | Sentinel | Meaning | |
| 80 | +|------|----------|---------| |
| 81 | +| 1 | `ErrSilent` | Error already printed | |
| 82 | +| 2 | `ErrNotInStack` | Branch/stack not found | |
| 83 | +| 3 | `ErrConflict` | Rebase conflict | |
| 84 | +| 4 | `ErrAPIFailure` | GitHub API error | |
| 85 | +| 5 | `ErrInvalidArgs` | Invalid arguments or flags | |
| 86 | +| 6 | `ErrDisambiguate` | Multiple stacks/remotes, can't auto-select | |
| 87 | +| 7 | `ErrRebaseActive` | Rebase already in progress | |
| 88 | +| 8 | `ErrLockFailed` | Stack file lock contention | |
| 89 | +| 9 | `ErrStacksUnavailable` | Stacked PRs not enabled for repository | |
| 90 | +| 10 | `ErrModifyRecovery` | Modify session interrupted | |
| 91 | + |
| 92 | +Return these from `RunE` — never call `os.Exit()` directly from commands. Check with `errors.As(err, &ExitError{})`. |
| 93 | + |
| 94 | +### Testing patterns |
| 95 | + |
| 96 | +- **Framework:** `stretchr/testify` (`assert`, `require`) for assertions. |
| 97 | +- **Table-driven tests** are the norm — see `cmd/utils_test.go` for examples. |
| 98 | +- **Config:** Use `config.NewTestConfig()` which returns `(*Config, stdoutReader, stderrReader)` with captured I/O and no-op color functions. |
| 99 | +- **Git mocking:** Call `git.SetOps(&git.MockOps{...})` — it returns a restore function: always `defer restore()` to prevent test pollution. |
| 100 | +- **GitHub mocking:** Set `cfg.GitHubClientOverride = &github.MockClient{...}`. |
| 101 | +- **Prompt mocking:** Set `cfg.SelectFn`, `cfg.ConfirmFn`, or `cfg.InputFn` on the config to simulate interactive input. |
| 102 | +- **Stack file setup:** Use `stack.Load(dir)` after writing a stack file to get correct checksums for `Save`. |
| 103 | + |
| 104 | +### Key interfaces |
| 105 | + |
| 106 | +- **`git.Ops`** (`internal/git/gitops.go`) — 50+ methods wrapping git CLI calls. Production implementation uses `exec.Command("git", ...)`. Package-level functions (e.g., `git.CurrentBranch()`) delegate to a swappable package-level `ops` variable. |
| 107 | +- **`github.ClientOps`** (`internal/github/client_interface.go`) — 12 methods for GitHub API (PRs, stacks). Injected via `cfg.GitHubClientOverride` in tests. |
| 108 | +- **`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`). |
| 109 | + |
| 110 | +### Stack file |
| 111 | + |
| 112 | +- **Location:** `.git/gh-stack` (JSON format, schema version 1). |
| 113 | +- **Schema:** `internal/stack/schema.json`. |
| 114 | +- **Locking:** Exclusive file lock at `.git/gh-stack.lock` with 5-second timeout. Errors surface as `LockError`. |
| 115 | +- **Staleness:** Concurrent modifications detected via `StaleError`. |
| 116 | + |
| 117 | +## CI workflows (`.github/workflows/`) |
| 118 | + |
| 119 | +| Workflow | Trigger | What it does | |
| 120 | +|----------|---------|-------------| |
| 121 | +| `test.yml` | push to main, PRs | `go vet` + `go test -race -count=1 ./...` on 3 OS matrix | |
| 122 | +| `release.yml` | `v*` tags | Cross-platform precompiled binaries via `cli/gh-extension-precompile` | |
| 123 | +| `docs.yml` | push to main (docs/**) | Builds Astro/Starlight docs, deploys to GitHub Pages | |
| 124 | + |
| 125 | +## Non-obvious things |
| 126 | + |
| 127 | +- The `Queued` field on `BranchRef` is transient (populated from GitHub API, never persisted to the stack JSON file). |
| 128 | +- `git.SetOps()` replaces the **package-level** ops variable — forgetting `defer restore()` in a test will break every subsequent test in the package. |
| 129 | +- Interrupt detection: Ctrl+C is caught as `terminal.InterruptErr`, wrapped into an `errInterrupt` sentinel, and printed with a friendly message before a silent exit. |
| 130 | +- 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. |
| 131 | +- The `.gitignore` ignores `/gh-stack` and `/gh-stack.exe` — the built binary. |
0 commit comments