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
fetchStackPRDetailsto retrieve full PR metadata for each remote branch - Builds the
remoteActiveslice 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 filesystemremoteActive: 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 exactlyremoteStackCleanAhead: 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:
- Verifies a clean working tree via
git.HasUncommittedChangesto prevent data loss - Removes the current local stack using
removeLocalStack - Imports the remote stack via
importRemoteStack, creating local branches that track the remote PRs - Updates the local stack file to reflect the adopted remote state
- 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-stackdetects diverged stacks by comparing orderedlocalActiveandremoteActivebranch sequences using theclassifyRemoteStackfunction 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
resolveDivergenceUseRemotefunction verifies clean working trees before replacing local state, whileresolveDivergenceDeleteRemotepreserves 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →