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: ListStacks performs a GET request to /repos/{owner}/{repo}/stacks, while CreateStack sends a POST to the same endpoint. AddToStack uses PATCH on /stacks/:id, and Unstack sends DELETE.
  • Async merge: MergeStackAsync POSTs to /pulls/:n/merge-async, and GetAsyncMergeResult polls GET /pulls/:n/merge-async/:uuid.
  • Batch operations: PRTitles retrieves 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:

  1. Discovery: client.FindPRForBranch(currentBranch) locates the existing PR for the current branch via GraphQL.
  2. Stack creation: client.CreateStack([]int{pr.Number}) sends a REST POST to establish a new stack entity.
  3. Modification: client.AddToStack(stackID, []int{newPR.Number}) PATCHes the stack to include additional pull requests.
  4. 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 ClientOps interface in internal/github/client_interface.go (lines 6–25) defines the complete contract for GitHub API operations in gh-stack.
  • The Client struct in internal/github/github.go provides the concrete implementation using go-gh GraphQL 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 MockClient for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →