Understanding gh-stack Internal Architecture: A Deep Dive into cmd/, internal/, and tui/
The gh-stack internal architecture organizes its Go codebase into three distinct layers—cmd/ for CLI entry points, internal/ for core business logic, and internal/tui/ for interactive terminal interfaces—enabling clean separation between command parsing, Git/GitHub operations, and user-facing views.
The gh-stack tool is a Go-based GitHub CLI extension that manages stacked pull requests. Its internal architecture follows standard Go project conventions by separating concerns into discrete packages that handle command-line interfacing, core domain logic, and terminal user interfaces. This modular design allows developers to test individual components in isolation while maintaining clear boundaries between user input, business operations, and presentation layers.
Command Layer: The cmd/ Package
The cmd/ directory serves as the entry point for all user interactions, implementing the Cobra command framework to parse arguments and delegate work to internal packages.
In cmd/root.go, the top-level command (gh stack) is created and sub-commands are grouped into logical categories including stack management, remote operations, and navigation utilities. Each sub-command follows a consistent pattern: an Options struct holds flag values, a NewCmd(cfg) constructor returns a *cobra.Command, and the RunE field invokes a private runX function containing the actual logic.
For example, cmd/init.go parses the initial branch list, initializes a new stack using stack.Load and stack.Save, and prints status messages via the injected configuration. All commands receive a *config.Config instance that supplies I/O streams (Out, Err, In), color helpers (ColorSuccess, ColorError), and test hooks (SelectFn, ConfirmFn, InputFn) for mocking interactive prompts in unit tests.
Core Engine: The internal/ Package
The internal/ directory contains the domain logic, organized into focused sub-packages that handle Git operations, GitHub API integration, stack persistence, and shared utilities.
Git Operations (internal/git)
The Ops interface defined in internal/git/gitops.go abstracts every Git operation required by gh-stack, including branch creation, fetching, pushing, and rebasing. The default implementation (defaultOps) forwards calls to the real git binary via helper functions (run, runSilent), while tests can substitute git.MockOps via git.SetOps for deterministic validation without a real repository.
Key methods include CurrentBranch and BranchExists for repository queries, FetchBranch and FetchBranches for explicit refspec handling with graceful missing-remote support, and Push which builds per-branch --force-with-lease arguments. The rebasing methods (Rebase, RebaseOnto, RebaseContinue) encapsulate conflict resolution logic and automatically invoke auto-resolve helpers when conflicts arise.
GitHub API Abstraction (internal/github)
The ClientOps interface in internal/github/client_interface.go defines the subset of GitHub API calls required by gh-stack, including PR creation, stack creation, async merge operations, and merge-queue detection. The concrete implementation is instantiated via github.NewClient using the repository's host, owner, and name, while MockClient in mock_client.go provides a test double that can be injected via Config.GitHubClientOverride.
Stack File Management (internal/stack)
The stack state persists as .git/gh-stack JSON. The stack.Stack struct models a complete stack (trunk plus ordered branches), while stack.BranchRef holds each branch's HEAD SHA, base SHA, and optional PR metadata.
Core responsibilities include loading and saving via stack.Load (which reads the file and records a SHA-256 checksum) and stack.Save (which acquires an exclusive lock, checks for stale data via checkStale, and writes updated JSON). Concurrency control is implemented through platform-specific file locking in lock_unix.go and lock_windows.go. Helper methods like ActiveBranches, MergedBranches, and NearestSurvivingBranch provide the logic required by commands such as submit, sync, and modify.
Shared Utilities (internal/config)
The config.Config struct centralizes terminal handling, color theme management, and test instrumentation. Additional utilities include branch/name.go for branch slugification (e.g., DateSlug), modify/state.go for the interactive modify command's state machine, and pr/template.go for discovering repository PR templates.
Terminal UI Layer: The internal/tui/ Package
The internal/tui/ directory houses the Bubble Tea-based interactive views used by commands like gh stack view and gh stack submit. Each view resides in its own sub-folder following the Model-Update-View pattern.
checkoutview
Located in internal/tui/checkoutview/, this view displays a list of branches for users to select a checkout target. It implements tea.Model with fields for selection state and uses lipgloss for styling via styles.go.
modifyview
The internal/tui/modifyview/ package drives the interactive modify session, showing the current stack, pending actions, and allowing users to reorder or drop branches. Key files include model.go for state management, status.go for operation feedback, and help.go for contextual keybindings.
submitview
Found in internal/tui/submitview/, this view presents a preview of PRs that will be submitted, enables editing of titles and descriptions through an integrated editor, and confirms operations before execution. It coordinates with the GitHub client and Git operations layers through the shared Config instance.
Cross-Cutting Interaction Patterns
The layers communicate through well-defined interfaces and shared configuration objects. When a user executes gh stack submit, the flow proceeds as follows:
cmd/submit.goparses flags and obtains a*config.Configinstance.- The command loads the stack file via
stack.Load(git.RootDir()). - If
--interactiveis set, the command instantiatessubmitview.NewModel(cfg, sf.Stacks[0])and starts a Bubble Tea program. - Upon confirmation, the command invokes
cfg.GitHubClient()for API operations andgit.Opsmethods (Push,Rebase, etc.) for repository modifications. - Finally,
stack.Savepersists changes using file locking to prevent race conditions.
Example: Programmatic Branch Addition
cfg := config.New()
gitRoot, _ := git.RootDir()
sf, _ := stack.Load(gitRoot)
// Create a new branch on top of the current stack head.
branch := "feature-xyz"
base := sf.Stacks[0].ActiveBaseBranch(branch)
git.CreateBranch(branch, base)
// Update the stack file.
newRef := stack.BranchRef{Branch: branch, Base: base}
sf.Stacks[0].Branches = append(sf.Stacks[0].Branches, newRef)
stack.Save(gitRoot, sf)
Relevant sources: config.New in internal/config/config.go, git.CreateBranch in internal/git/gitops.go, and stack.Load/stack.Save in internal/stack/stack.go.
Example: Running the Interactive Modify UI
cfg := config.New()
gitRoot, _ := git.RootDir()
sf, _ := stack.Load(gitRoot)
// Build the model and start the TUI.
m := modifyview.NewModel(cfg, sf.Stacks[0])
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
cfg.Errorf("modify UI failed: %v", err)
}
Relevant sources: modifyview.NewModel in internal/tui/modifyview/model.go, Bubble Tea's tea.NewProgram, and Config error helpers.
Summary
- The
cmd/layer handles CLI parsing, flag validation, and command routing using Cobra, remaining thin by delegating to internal packages. - The
internal/layer contains the core engine, including Git abstraction via theOpsinterface, GitHub API clients, stack file persistence with concurrency control, and shared configuration utilities. - The
internal/tui/layer provides interactive Bubble Tea views for checkout, modify, and submit operations, communicating with core logic through theConfigobject. - Cross-cutting concerns like I/O streams, color themes, and test hooks are centralized in
config.Config, enabling consistent behavior and testability across all layers.
Frequently Asked Questions
What is the purpose of the internal/ directory in gh-stack?
The internal/ directory encapsulates the core business logic that should not be imported by external packages, following Go's visibility conventions. It contains implementations for Git operations (internal/git), GitHub API interactions (internal/github), stack file management (internal/stack), and shared utilities (internal/config), ensuring that command handlers remain focused on argument parsing and delegation.
How does gh-stack handle Git operations without tight coupling to the binary?
According to the source code in internal/git/gitops.go, gh-stack defines an Ops interface that abstracts all Git operations. The production code uses defaultOps which shells out to the real git binary, but the architecture allows swapping in git.MockOps via git.SetOps during unit tests. This dependency injection pattern enables deterministic testing without requiring actual Git repositories or network operations.
What framework powers the interactive UI components in gh-stack?
The terminal UI is built on Bubble Tea, a Go framework based on The Elm Architecture. Views in internal/tui/checkoutview/, internal/tui/modifyview/, and internal/tui/submitview/ implement the tea.Model interface with Init, Update, and View methods, using lipgloss for styling and bubbles for reusable components like lists and text inputs.
How does gh-stack prevent concurrent modifications to the stack file?
The internal/stack package implements file-based concurrency control using platform-specific locking mechanisms in lock_unix.go and lock_windows.go. When stack.Save writes to .git/gh-stack, it acquires an exclusive lock, verifies that the file hasn't been modified since loading (via SHA-256 checksum comparison in checkStale), and only then commits the changes, preventing race conditions when multiple gh-stack processes run simultaneously.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →