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

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)), 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)) 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)), which implements a prefix-based comparison algorithm:

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)), 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:

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)), 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)):

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:

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)
  • 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), 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) 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) forces a conscious decision through the selectFn call, ensuring you understand which version of the stack will be preserved.

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 →