Understanding the ClientOps Interface in gh-stack: How It Wraps GitHub API Calls
The ClientOps interface is a Go abstraction declared in internal/github/client_interface.go (lines 6–25) that encapsulates all GitHub API interactions—both GraphQL queries for pull requests and REST calls for stack management—enabling the gh-stack CLI to operate on stacked PRs through a unified, testable contract.
The gh-stack extension orchestrates stacked pull requests by interacting with GitHub's GraphQL and REST APIs through a thin abstraction layer. At the core of this architecture lies the ClientOps interface, which defines the complete set of operations required to query pull requests, manage stacks, and handle merge queues. By programming against this interface rather than concrete HTTP clients, gh-stack achieves clean separation between business logic and API transport concerns.
The ClientOps Interface Declaration
The interface is defined in internal/github/client_interface.go and comprises seventeen methods spanning three functional domains:
type ClientOps interface {
FindPRForBranch(branch string) (*PullRequest, error)
FindPRByNumber(number int) (*PullRequest, error)
FindPRDetailsForBranch(branch string) (*PRDetails, error)
CreatePR(base, head, title, body string, draft bool) (*PullRequest, error)
UpdatePRBase(number int, base string) error
MarkPRReadyForReview(prID string) error
DisableAutoMerge(prID string) error
ListStacks() ([]RemoteStack, error)
FindStackForPR(prNumber int) (*RemoteStack, error)
GetStack(stackNumber int) (*RemoteStack, error)
CreateStack(prNumbers []int) (*RemoteStack, error)
AddToStack(stackNumber int, prNumbers []int) (*RemoteStack, error)
Unstack(stackNumber int) (*RemoteStack, bool, error)
RepoMergeConfig() (*RepoMergeConfig, error)
MergeStackAsync(prNumber int, method, mergeAction string) (*AsyncMergeResult, error)
GetAsyncMergeResult(prNumber int, uuid string) (*AsyncMergeResult, error)
PRTitles(numbers []int) (map[int]string, error)
BaseBranchUsesMergeQueue(baseRef string) (bool, error)
}
GraphQL Query Methods
Methods like FindPRForBranch and FindPRByNumber execute GraphQL queries against GitHub's API. For example, FindPRForBranch constructs a query filtering by headRefName to locate the first open pull request for a given branch. Similarly, RepoMergeConfig and BaseBranchUsesMergeQueue query repository merge queue settings through GraphQL.
GraphQL Mutation Methods
Write operations including CreatePR, UpdatePRBase, MarkPRReadyForReview, and DisableAutoMerge use GraphQL mutations. CreatePR accepts parameters for base branch, head branch, title, body, and draft status to instantiate new pull requests.
REST API Methods
Stack management and async merge functionality utilize GitHub's REST endpoints:
- Stack operations:
ListStacksperforms a GET request to/repos/{owner}/{repo}/stacks, whileCreateStacksends a POST to the same endpoint.AddToStackuses PATCH on/stacks/:id, andUnstacksends DELETE. - Async merge:
MergeStackAsyncPOSTs to/pulls/:n/merge-async, andGetAsyncMergeResultpolls GET/pulls/:n/merge-async/:uuid. - Batch operations:
PRTitlesretrieves pull request titles in batch via REST.
Concrete Implementation with the Client Type
The production implementation resides in internal/github/github.go (lines 53–61) as the Client struct:
type Client struct {
gql *api.GraphQLClient // go-gh GraphQL client
rest *api.RESTClient // go-gh REST client
host string
owner string
repo string
slug string // "owner/repo"
}
Each ClientOps method is implemented on this struct. GraphQL operations invoke c.gql.Query or c.gql.Mutate, while REST operations call c.rest.Request. The underlying go-gh library (github.com/cli/go-gh/v2/pkg/api) automatically handles OAuth authentication, rate limiting, and response pagination.
Dependency Injection and Usage Patterns
Commands throughout gh-stack consume the interface through config.Config.GitHubClientOverride, enabling both production and test implementations.
Production Initialization
In commands like cmd/submit.go, the extension instantiates a real client:
cfg.GitHubClientOverride, err = github.NewClient(host, owner, repo)
This constructor configures the GraphQL and REST clients with the target repository's host, owner, and name (stored in the slug field as "owner/repo").
Testing with MockClient
The MockClient type in internal/github/mock_client.go satisfies the ClientOps interface, allowing unit tests to inject predetermined responses without network I/O.
Command Execution Flow
A typical workflow such as gh stack submit demonstrates the interface in action:
- Discovery:
client.FindPRForBranch(currentBranch)locates the existing PR for the current branch via GraphQL. - Stack creation:
client.CreateStack([]int{pr.Number})sends a REST POST to establish a new stack entity. - Modification:
client.AddToStack(stackID, []int{newPR.Number})PATCHes the stack to include additional pull requests. - Merge initiation:
client.MergeStackAsync(prNumber, method, mergeAction)triggers the async merge process via REST.
Code Examples
Querying a Pull Request by Branch
func getPRForBranch(cfg *config.Config, branch string) (*github.PullRequest, error) {
// cfg.GitHubClientOverride satisfies ClientOps
client := cfg.GitHubClientOverride
return client.FindPRForBranch(branch)
}
Creating and Populating a Stack
func createAndPopulateStack(cfg *config.Config, prNumber int, additionalPRs []int) (*github.RemoteStack, error) {
client := cfg.GitHubClientOverride
// Create initial stack
stack, err := client.CreateStack([]int{prNumber})
if err != nil {
return nil, err
}
// Add remaining PRs
return client.AddToStack(stack.Number, additionalPRs)
}
Checking Merge Queue Status
func checkMergeQueue(cfg *config.Config, baseRef string) (bool, error) {
client := cfg.GitHubClientOverride
return client.BaseBranchUsesMergeQueue(baseRef)
}
Initiating Async Merge
func asyncMerge(cfg *config.Config, prNumber int, method, action string) (*github.AsyncMergeResult, error) {
client := cfg.GitHubClientOverride
return client.MergeStackAsync(prNumber, method, action)
}
Summary
- The
ClientOpsinterface ininternal/github/client_interface.go(lines 6–25) defines the complete contract for GitHub API operations in gh-stack. - The
Clientstruct ininternal/github/github.goprovides the concrete implementation usinggo-ghGraphQL and REST clients. - GraphQL methods handle pull request queries and mutations, while REST methods manage the Stacks API and asynchronous merge endpoints.
- The interface enables dependency injection, allowing production commands to use real API clients and tests to use
MockClientfor isolation. - All high-level commands access GitHub through
config.GitHubClientOverride, ensuring consistent authentication and error handling across the codebase.
Frequently Asked Questions
What specific GitHub API endpoints does ClientOps wrap?
The interface wraps GraphQL queries for pull request data (filtering by headRefName and PR number) and REST endpoints for stack management (/repos/{owner}/{repo}/stacks) and async merges (/pulls/:n/merge-async). Specific methods like ListStacks use REST GET operations, while CreatePR executes GraphQL mutations.
How does the Client implementation handle authentication?
The concrete Client type relies on the go-gh library (github.com/cli/go-gh/v2/pkg/api), which automatically manages OAuth tokens, request headers, rate limiting, and pagination. The Client struct stores pre-configured *api.GraphQLClient and *api.RESTClient instances initialized with the user's GitHub CLI credentials.
Why does gh-stack use an interface for GitHub operations?
Using the ClientOps interface decouples command logic from HTTP implementation details. This abstraction allows commands in cmd/ packages to remain agnostic of whether they're calling real GitHub APIs or mock implementations, facilitating unit testing through github.MockClient and insulating business logic from changes in the underlying API transport layer.
Where can I find the implementation of specific methods like MergeStackAsync?
Method-specific implementations are distributed across the internal/github/ directory. The core Client struct and common methods reside in internal/github/github.go, while async merge specific logic appears in internal/github/merge_async.go. The interface definition in internal/github/client_interface.go links all implementations through the ClientOps contract.
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 →