# How gh-stack Detects and Resolves Diverged Stacks During a Sync Operation

> Learn how gh-stack detects and resolves diverged stacks during a sync operation. Explore options to update local, delete remote, or cancel.

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

---

**During a `gh stack sync`, the tool compares ordered branch sequences between local and remote representations to detect divergence, then offers interactive resolution options including updating local to match remote, deleting the remote stack, or canceling the operation.**

The `gh-stack` extension for the GitHub CLI maintains synchronized state between your local Git branches and the stack objects stored on GitHub. When the ordered list of active branches in your local working directory no longer aligns with the remote stack's sequence, the tool must detect this divergence and provide safe resolution paths to prevent data loss or conflicting states.

## How Divergence Occurs in gh-stack

A **diverged stack** arises when the ordered list of active (non-merged) branches in your local stack is **not** a prefix of the remote stack's sequence, and vice versa. This typically happens when collaborators modify the stack structure on GitHub while you simultaneously add, remove, or reorder branches locally without pushing those changes.

Unlike simple desynchronization where one side merely has additional commits, divergence represents a fundamental structural mismatch that automatic fast-forwarding cannot reconcile.

## Fetching and Comparing Stack States

The reconciliation process begins inside `runSync` ([[`cmd/sync.go`](https://github.com/github/gh-stack/blob/main/cmd/sync.go)](https://github.com/github/gh-stack/blob/main/cmd/sync.go)), which delegates remote state verification to specialized utility functions.

### Retrieving Remote Stack Data

The `reconcileRemoteStack` function ([[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)) initiates the comparison by fetching the current remote state:

- Calls `client.ListStacks()` to obtain the list of PR numbers associated with the remote stack
- Invokes `fetchStackPRDetails` to retrieve full PR metadata for each remote branch
- Builds the `remoteActive` slice containing the ordered branch names from the fetched PRs

Simultaneously, the function constructs the `localActive` slice from the current `stack.Stack` object representing your local branches.

### Building the Active Branch Sequences

The `activeStackSequences` helper prepares both datasets for comparison by extracting ordered branch names while filtering out merged or deleted branches. This produces two comparable slices:

- `localActive`: The branch sequence from your local filesystem
- `remoteActive`: The branch sequence from GitHub's stored stack object

## Detection Logic in classifyRemoteStack

The core detection occurs in `classifyRemoteStack` ([[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)), which implements a prefix-based comparison algorithm:

```go
localActive, remoteActive := activeStackSequences(s, prs)
switch classifyRemoteStack(localActive, remoteActive) {
case remoteStackInSync:
    // Identical sequences - proceed with normal sync
case remoteStackCleanAhead:
    // Remote contains additional branches - pull remote additions
    return pullRemoteAdditions(...)
case remoteStackLocalAhead:
    // Local contains additional branches - safe to push
    // Continue with normal flow
default: // remoteStackDivergent
    // Structural mismatch detected
    return resolveStackDivergence(...)
}

```

The classification returns four distinct states:

- **`remoteStackInSync`**: Both sequences match exactly
- **`remoteStackCleanAhead`**: Remote is a strict superset (new PRs added remotely)
- **`remoteStackLocalAhead`**: Local is a strict superset (branches added locally)
- **`remoteStackDivergent`**: Neither sequence is a prefix of the other, indicating conflicting structural changes

Only the `remoteStackDivergent` case triggers the resolution workflow, as the other states can proceed through standard fast-forward or rebase operations.

## Resolution Strategies for Diverged Stacks

When divergence is detected, control passes to `resolveStackDivergence` ([[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)), which implements different behaviors based on terminal interactivity.

### Interactive Resolution Prompts

In an interactive terminal, `gh-stack` presents a warning displaying both the local and remote branch chains, followed by a selection prompt:

```go
options := []string{
    "Update local to match remote — replace your local stack with the remote version",
    "Delete the remote stack on GitHub — keep your local stack and recreate on remote later",
    "Cancel — make no changes",
}
selected, err := selectFn("How would you like to resolve?", "", options)

```

The user's selection determines which resolution function executes.

### Updating Local to Match Remote

Selecting **"Update local to match remote"** invokes `resolveDivergenceUseRemote` ([[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)), which performs the following operations:

1. Verifies a clean working tree via `git.HasUncommittedChanges` to prevent data loss
2. Removes the current local stack using `removeLocalStack`
3. Imports the remote stack via `importRemoteStack`, creating local branches that track the remote PRs
4. Updates the local stack file to reflect the adopted remote state
5. Optionally checks out the nearest surviving branch via `nearestBranchAfterReplace`

This operation effectively discards your local branch structure in favor of the GitHub-hosted version.

### Deleting the Remote Stack

Selecting **"Delete the remote stack on GitHub"** triggers `resolveDivergenceDeleteRemote` ([[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)):

```go
err := client.Unstack(stackID)  // Removes remote stack object
if err != nil { return err }
s.ID = ""       // Clear local stack ID
s.Number = 0    // Clear local stack number
saveStackFile(s) // Persist cleared state

```

This approach preserves all local branches and working tree state while removing the conflicting remote stack object. You can later recreate the remote stack when ready to push your local structure.

### Non-Interactive Behavior

In non-interactive environments (such as CI/CD pipelines), `gh-stack` cannot prompt for resolution. Instead, it outputs a message directing you to run `gh stack checkout <pr>` in an interactive terminal and aborts the sync with `stop: true`:

```go
reconcileRes, err := reconcileRemoteStack(cfg, sf, s, currentBranch, gitDir, remote)
if err != nil { 
    return err 
}
if reconcileRes.stack != nil {
    s = reconcileRes.stack  // Updated stack after divergence handling
}
if reconcileRes.stop {
    return nil  // Sync aborted (cancel or non-interactive)
}

```

The sync operation terminates early, leaving both local and remote states untouched until manual intervention occurs.

## Summary

- **Detection Method**: `gh-stack` detects diverged stacks by comparing ordered `localActive` and `remoteActive` branch sequences using the `classifyRemoteStack` function in [[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go)
- **Divergence Definition**: Divergence occurs when neither the local nor remote branch list is a prefix of the other, indicating conflicting structural modifications
- **Resolution Options**: Users may choose to **update local to match remote**, **delete the remote stack**, or **cancel** the operation entirely
- **Safety Checks**: The `resolveDivergenceUseRemote` function verifies clean working trees before replacing local state, while `resolveDivergenceDeleteRemote` preserves local branches when clearing remote objects
- **CI/CD Handling**: Non-interactive terminals automatically abort with instructions to resolve manually via `gh stack checkout`

## Frequently Asked Questions

### What constitutes a diverged stack in gh-stack?

A diverged stack occurs when the ordered list of active branches in your local working directory is not a prefix of the remote stack's sequence, and vice versa. According to the source code in [[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go), this happens when `classifyRemoteStack` returns `remoteStackDivergent`, indicating that both you and collaborators have made incompatible structural changes to the stack since the last sync.

### How does gh-stack handle divergence in CI/CD environments?

In non-interactive environments, `gh-stack` cannot present the interactive resolution prompt. The `resolveStackDivergence` function detects the non-interactive terminal and returns with `stop: true`, causing `runSync` in [[`cmd/sync.go`](https://github.com/github/gh-stack/blob/main/cmd/sync.go)](https://github.com/github/gh-stack/blob/main/cmd/sync.go) to abort early. The tool prints a message instructing you to run `gh stack checkout <pr>` locally to resolve the divergence manually before retrying the sync in the automated environment.

### What happens to local branches when resolving divergence?

The outcome depends on your resolution choice. If you select **"Update local to match remote"**, the `resolveDivergenceUseRemote` function removes your local stack and branches, then recreates them to match the remote state. If you select **"Delete the remote stack"**, the `resolveDivergenceDeleteRemote` function calls `client.Unstack` to remove only the remote stack object while leaving all local branches and working tree changes untouched.

### Can gh-stack automatically resolve diverged stacks without user input?

No, `gh-stack` requires explicit user input to resolve divergence. The tool intentionally avoids automatic resolution because both options—adopting the remote state or deleting the remote stack—involve destructive operations that could result in lost work. The interactive prompt in [[`cmd/utils.go`](https://github.com/github/gh-stack/blob/main/cmd/utils.go)](https://github.com/github/gh-stack/blob/main/cmd/utils.go) forces a conscious decision through the `selectFn` call, ensuring you understand which version of the stack will be preserved.