Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# gh-stack: Copilot Instructions

A Go CLI extension (`gh stack`) for managing stacked branches and pull requests. Uses Cobra for commands, bubbletea/lipgloss for TUI, and `stretchr/testify` for tests.

## Build and validate

```sh
go mod download # install deps
go build -o gh-stack . # build
go vet ./... # static analysis. Always run before tests.
go test -race -count=1 ./... # tests with race detection
```

No Makefile, no code generation, no external linter config. Standard Go toolchain only.

## Project layout

- `cmd/`: One Cobra command per file. Each exports `<Name>Cmd(cfg *config.Config)` with logic in `run<Name>()`.
- `internal/git/`: `Ops` interface (52 methods) wrapping git CLI. `MockOps` for tests. Package-level functions delegate to swappable `ops` variable.
- `internal/github/`: `ClientOps` interface (11 methods) for GitHub API. `MockClient` for tests.
- `internal/config/`: `Config` struct passed to all commands. Holds I/O, colors, and test hooks (`SelectFn`, `ConfirmFn`, `InputFn`, `GitHubClientOverride`).
- `internal/stack/`: Stack file (`.git/gh-stack`, JSON) management with file locking.
- `internal/tui/`: bubbletea views (`stackview`, `modifyview`).

## Coding conventions

- Return typed `ExitError` sentinels (codes 1-10 in `cmd/utils.go`) from `RunE`. Never call `os.Exit()` directly.
- Check errors with `var exitErr *ExitError; errors.As(err, &exitErr)`.
- Table-driven tests with `t.Run()` subtests.
- Use `config.NewTestConfig()` for test configs with captured I/O.
- Mock git: `restore := git.SetOps(&git.MockOps{...}); defer restore()`. Always defer restore.
- Mock GitHub: `cfg.GitHubClientOverride = &github.MockClient{...}`.
- Mock prompts: set `cfg.SelectFn`, `cfg.ConfirmFn`, or `cfg.InputFn`.
- Load stack files with `stack.Load(dir)` after writing to get correct checksums.

For full architecture details, see [AGENTS.md](../AGENTS.md) in the repository root.
136 changes: 136 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 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

```sh
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

```sh
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 (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)
```

### 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:

1. Define an `<name>Options` struct for flags/args.
2. Export a `<Name>Cmd(cfg *config.Config) *cobra.Command` constructor.
3. Implement logic in a private `run<Name>(cfg, opts, args)` function.
4. The `RunE` field on the command calls `run<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:

```go
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.go` for 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. Always `defer restore()` to prevent test pollution.
- **GitHub mocking:** Set `cfg.GitHubClientOverride = &github.MockClient{...}`.
- **Prompt mocking:** Set `cfg.SelectFn`, `cfg.ConfirmFn`, or `cfg.InputFn` on the config to simulate interactive input.
- **Stack file setup:** Use `stack.Load(dir)` after writing a stack file to get correct checksums for `Save`.

### Key interfaces

- **`git.Ops`** (`internal/git/gitops.go`): 52 methods wrapping git CLI calls. The production implementation uses `cli/go-gh`'s `client.Command()` via `run()` and `runSilent()` helpers. Package-level functions (e.g., `git.CurrentBranch()`) delegate to a swappable package-level `ops` variable.
- **`github.ClientOps`** (`internal/github/client_interface.go`): 11 methods for GitHub API (PRs, stacks). Injected via `cfg.GitHubClientOverride` in 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`).

### Stack file

- **Location:** `.git/gh-stack` (JSON format, schema version 1).
- **Schema:** `internal/stack/schema.json`.
- **Locking:** Exclusive file lock at `.git/gh-stack.lock` with 5-second timeout. Errors surface as `LockError`.
- **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 `Queued` field on `BranchRef` is transient (populated from GitHub API, never persisted to the stack JSON file).
- `git.SetOps()` replaces the **package-level** ops variable. Forgetting `defer restore()` in a test will break every subsequent test in the package.
- Interrupt detection: Ctrl+C is caught as `terminal.InterruptErr`, wrapped into an `errInterrupt` sentinel, 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 `.gitignore` ignores `/gh-stack` and `/gh-stack.exe` (the built binary).