# How the git.Ops Interface Works in gh-stack: Abstraction and Testing Patterns

> Discover how gh-stack's git.Ops interface abstracts Git commands for effortless testing. Learn to swap real Git ops with MockOps using SetOps() for robust unit tests.

- Repository: [GitHub/gh-stack](https://github.com/github/gh-stack)
- Tags: internals
- Published: 2026-08-02

---

**The `git.Ops` interface in gh-stack abstracts all Git command-line operations through a swappable layer, enabling tests to inject `MockOps` via `SetOps()` rather than executing real `git` commands.**

The `github/gh-stack` repository implements a robust abstraction layer for Git operations through the `git.Ops` interface defined in [`internal/git/gitops.go`](https://github.com/github/gh-stack/blob/main/internal/git/gitops.go). This interface exposes over 50 methods—including `CurrentBranch()`, `Rebase()`, and `Push()`—that production code uses instead of directly invoking the Git binary. By funneling all Git interactions through this single interface, the codebase achieves clean separation between business logic and version control operations while maintaining testability through a simple global swap mechanism.

## Understanding the git.Ops Interface Definition

Located in [`internal/git/gitops.go`](https://github.com/github/gh-stack/blob/main/internal/git/gitops.go), the `Ops` interface serves as the central contract for all Git interactions. It declares more than 50 methods covering repository queries and mutations:

- `CurrentBranch()` – retrieves the active branch name
- `Push()` – handles remote synchronization  
- `LogRange()` – queries commit history
- `Rebase()` – performs history rewriting

Rather than calling the `git` binary directly, the package provides helper functions such as `git.CurrentBranch()` and `git.Push()`. These helpers delegate to a package-private variable `ops` that stores the concrete `Ops` implementation. This indirection allows the entire package to switch behaviors without changing call sites throughout the application.

## Production Implementation with defaultOps

The production implementation, `defaultOps`, resides in [`internal/git/gitops.go`](https://github.com/github/gh-stack/blob/main/internal/git/gitops.go) and executes actual Git commands through internal helper functions like `run()` and `runSilent()`. At package initialization, the declaration `var ops Ops = &defaultOps{}` binds the global `ops` variable to a real Git client instance.

When production code invokes package-level helpers, they resolve to `defaultOps` methods, which spawn subprocesses to interact with the repository. This design keeps Git-specific execution details encapsulated while the rest of the application works against the abstract interface.

## Swapping Implementations via SetOps()

Testing requires bypassing the real Git binary to avoid filesystem dependencies and side effects. The `SetOps()` function enables this by atomically replacing the global `ops` variable:

```go
// SetOps replaces the git operations implementation. Returns a restore function.
func SetOps(o Ops) func() {
    old := ops
    ops = o
    return func() { ops = old }
}

```

This pattern returns a closure that restores the previous implementation when invoked. Tests defer this closure immediately after swapping, ensuring complete isolation between test cases and preventing mock state from leaking into subsequent tests.

## MockOps: The Test Double

The repository provides `MockOps` in [`internal/git/mock_ops.go`](https://github.com/github/gh-stack/blob/main/internal/git/mock_ops.go). This struct implements the full `Ops` interface using functional fields that tests can override:

```go
type MockOps struct {
    CurrentBranchFn func() (string, error)
    RebaseFn        func(string, RebaseOpts) error
    PushFn          func(string, []string) error
    // ... additional fields for each interface method
}

```

Each method checks if its corresponding function field is non-nil. If set, the mock executes that function; otherwise, it returns a sensible zero value. This design allows tests to stub only the specific Git behaviors relevant to the scenario under test without implementing the entire interface.

## Complete Testing Pattern Example

A typical test constructs a `MockOps`, swaps the global implementation using `SetOps()`, and defers restoration:

```go
package mycmd_test

import (
    "testing"

    "github.com/github/gh-stack/internal/git"
)

func TestRebaseWithMock(t *testing.T) {
    // 1️⃣ Build a mock that pretends the current branch is "feature"
    mock := &git.MockOps{
        CurrentBranchFn: func() (string, error) {
            return "feature", nil
        },
        RebaseFn: func(base string, opts git.RebaseOpts) error {
            if base != "main" {
                t.Fatalf("unexpected rebase base: %s", base)
            }
            return nil // simulate successful rebase
        },
    }

    // 2️⃣ Swap the implementation; defer restoration
    restore := git.SetOps(mock)
    defer restore()

    // 3️⃣ Call the code under test
    err := mycmd.RebaseOntoMain()
    if err != nil {
        t.Fatalf("RebaseOntoMain failed: %v", err)
    }
}

```

Production code accesses Git operations through the global interface, making the swap transparent:

```go
func RebaseOntoMain() error {
    // Resolve the default branch using the current Ops implementation.
    def, err := git.CurrentOps().DefaultBranch()
    if err != nil {
        return err
    }
    // Rebase onto that branch.
    return git.CurrentOps().Rebase(def, git.RebaseOpts{})
}

```

## Summary

- The `git.Ops` interface in [`internal/git/gitops.go`](https://github.com/github/gh-stack/blob/main/internal/git/gitops.go) defines over 50 methods abstracting Git CLI operations such as `Rebase()`, `Push()`, and `CurrentBranch()`.
- `defaultOps` provides the production implementation by executing real `git` commands via `run()` and `runSilent()`.
- `SetOps()` enables global swapping of implementations and returns a restoration closure, ensuring test isolation.
- `MockOps` in [`internal/git/mock_ops.go`](https://github.com/github/gh-stack/blob/main/internal/git/mock_ops.go) offers per-method stubbing via functional fields like `CurrentBranchFn`.
- This architecture allows `gh-stack` to maintain fast, deterministic unit tests without dependencies on actual Git repository state.

## Frequently Asked Questions

### What is the purpose of the git.Ops interface in gh-stack?

The interface centralizes all Git command-line interactions into a typed contract, preventing direct shell execution from spreading throughout the codebase. This abstraction enables the application to run against either real Git repositories via `defaultOps` or mocked in-memory implementations via `MockOps` depending on the execution context.

### How does SetOps() ensure test isolation?

`SetOps()` captures the current implementation in a closure before replacing the global `ops` variable with the mock, then returns that closure. When tests defer the returned function immediately after the swap, it restores the original implementation after the test completes, preventing state leakage between test cases.

### Where is the MockOps struct defined?

The `MockOps` testing double is defined in [`internal/git/mock_ops.go`](https://github.com/github/gh-stack/blob/main/internal/git/mock_ops.go). It implements the full `Ops` interface using exported function fields that tests can assign to override specific behaviors, while unconfigured methods return sensible defaults.

### Why does gh-stack use functional fields in MockOps instead of traditional methods?

Functional fields allow test authors to override only the specific Git operations relevant to their test scenario inline, without subclassing or complex setup hierarchies. This keeps test code concise and makes the test's specific dependencies explicit at the exact call site where the mock is constructed.