# How the `gh stack modify` Command Restructures Git Stacks: A Deep Dive into the Interactive Workflow

> Learn how gh stack modify restructures Git stacks using an interactive ten-stage pipeline for efficient Git operations and cascading rebasing with recovery.

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

---

**The `gh stack modify` command restructures Git stacks through a ten-stage pipeline that converts TUI interactions into concrete Git operations, including renames, inserts, folds, drops, and reorders, followed by a cascading rebase with full snapshot-based recovery capabilities.**

The `gh stack modify` command in the [github/gh-stack](https://github.com/github/gh-stack) repository provides an interactive terminal interface for reshaping stacked pull requests. It allows developers to drop, fold, insert, reorder, and rename branches within a stack while maintaining data integrity through deterministic execution and rollback capabilities.

## Pre-Flight Checks: Validating the Repository State

Before launching the interactive interface, `checkModifyPreconditions` in [[`cmd/modify.go`](https://github.com/github/gh-stack/blob/main/cmd/modify.go)](https://github.com/github/gh-stack/blob/main/cmd/modify.go#L74-L98) validates the environment. This function ensures the terminal is interactive, no rebase is in progress, the working tree is clean, and no existing modify session is active. It also verifies the presence of a trunk branch, confirms PRs are synced, and checks stack linearity to prevent operations on corrupted metadata.

## Loading and Preparing the Interactive UI

Once preconditions pass, the command loads the current stack state using `loadStack` and retrieves PR details. The raw stack data transforms into visual components through `stackview.LoadBranchNodes`, which creates `stackview.BranchNode` objects. The command reverses this list so the top of the visual stack corresponds to index 0, then wraps each node into `modifyview.ModifyBranchNode` structures that carry UI-specific state including `OriginalPosition`, `Removed`, and `PendingAction` fields ([[`cmd/modify.go`](https://github.com/github/gh-stack/blob/main/cmd/modify.go)](https://github.com/github/gh-stack/blob/main/cmd/modify.go#L85-L100)).

```go
func runModify(cfg *config.Config) error {
    result, _ := checkModifyPreconditions(cfg)
    viewNodes := stackview.LoadBranchNodes(cfg, result.Stack, result.CurrentBranch, result.PRDetails)
    // reverse for visual order
    reversed := make([]stackview.BranchNode, len(viewNodes))
    for i, n := range viewNodes { reversed[len(viewNodes)-1-i] = n }
    // wrap for modify UI
    modifyNodes := make([]modifyview.ModifyBranchNode, len(reversed))
    for i, n := range reversed {
        modifyNodes[i] = modifyview.ModifyBranchNode{BranchNode: n, OriginalPosition: i}
    }
    // start Bubble Tea UI
    m := modifyview.New(modifyNodes, result.Stack.Trunk, Version)
    p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseAllMotion())
    final, _ := p.Run()
    // ...
}

```

## Capturing User Intent in the Bubble Tea TUI

The command instantiates a `modifyview.Model` with the prepared nodes and executes it via `tea.NewProgram` ([[`cmd/modify.go`](https://github.com/github/gh-stack/blob/main/cmd/modify.go)](https://github.com/github/gh-stack/blob/main/cmd/modify.go#L104-L112)). Users manipulate branches within the terminal UI; when they press **Ctrl+S**, the model returns an `ApplyResult` containing the staged actions. This result captures every rename, insertion, deletion, and reorder operation requested by the user.

## Building the Execution Plan

`modify.BuildPlan` in [[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L50-L99) processes the final `ModifyBranchNode` slice and generates a concrete slice of `modify.Action` structs. Each struct describes a specific operation—rename, insert, move, fold, or drop—with precise source and target parameters. This plan serves as the authoritative blueprint for all subsequent Git mutations.

## Creating Safety Snapshots and State Files

Before mutating the repository, `modify.BuildSnapshot` captures the complete pre-modify state including all branch names, tip SHAs, and the full stack JSON. This **Snapshot** enables full unwinding if operations fail. The command then persists a `StateFile` to `.git/gh-stack.modify` with phase **PhaseApplying**, storing the snapshot, execution plan, stack index, and timestamps ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L36-L47)). This state file powers the `--abort` and `--continue` recovery workflows.

## Executing the Five-Phase Restructuring Pipeline

The `ApplyPlan` function executes modifications through five distinct phases, each handling a specific restructuring operation.

### Phase 1: Branch Renames

The `git.RenameBranch` function updates both Git refs and the in-memory `stack.Stack` structure ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L90-L103)). This ensures the stack metadata remains synchronized with the actual repository state.

### Phase 2: Inserting New Branches

For insertions, the command calls `git.CreateBranch` to generate new branches from the appropriate parent tip, then inserts them into the `s.Branches` slice at calculated positions ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L124-L150).

### Phase 3: Folding Branches

Folding operates in two directions:

- **Fold-down**: Commits are cherry-picked onto the lower branch. If conflicts occur, the system saves a `PhaseConflict` state to enable resumption ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L161-L203)).
- **Fold-up**: Only the `originalParentTips` map is adjusted; the subsequent cascading rebase replays both sets of commits onto the new parent ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L221-L236)).

### Phase 4: Dropping Branches

Dropped branches are removed from the stack metadata. If branches have open PRs, they are recorded for later user cleanup rather than immediately deleted ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L260-L283).

### Phase 5: Reordering the Stack

The reorder phase builds the desired order from remaining nodes, compares it against the current order, and reconstructs the `s.Branches` slice to match the user's visual arrangement ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L306-L349).

## Cascading Rebase and Conflict Handling

After structural changes, each active branch undergoes a cascading rebase using `git.RebaseOnto`. The rebase uses the **original parent tip SHA** stored in `originalParentTips` as the old base, ensuring only the branch's own commits replay onto its new parent ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L368-L420).

If conflicts arise, the system transitions the state file to `PhaseConflict`, recording the conflicted branch, remaining branches, and PR status. Users can resolve conflicts and run `gh stack modify --continue` to resume.

```go
func ApplyPlan(cfg *config.Config, gitDir string, s *stack.Stack,
    sf *stack.StackFile, nodes []modifyview.ModifyBranchNode,
    currentBranch string, updateBaseSHAs func(*stack.Stack)) (*modifyview.ApplyResult, *modifyview.ConflictInfo, error) {

    // snapshot for unwind
    snapshot, _ := BuildSnapshot(s)

    // acquire lock
    lock, _ := stack.Lock(gitDir); defer lock.Unlock()

    // build concrete actions
    plan := BuildPlan(nodes)

    // write state file (PhaseApplying)
    state := &StateFile{Phase: PhaseApplying, Snapshot: snapshot, Plan: plan, …}
    SaveState(gitDir, state)

    // …handle renames, inserts, folds, drops, reorder…

    // cascading rebase
    for i, b := range s.Branches {
        if b.IsMerged() { continue }
        newBase := s.ActiveBaseBranch(b.Branch)
        oldBase := originalParentTips[b.Branch]
        if err := git.RebaseOnto(newBase, oldBase, b.Branch, git.RebaseOpts{}); err != nil {
            // on conflict, persist PhaseConflict and bail
            state.Phase = PhaseConflict
            state.ConflictBranch = b.Branch
            SaveState(gitDir, state)
            return nil, &modifyview.ConflictInfo{Branch: b.Branch}, err
        }
    }

    // final success handling …
}

```

## Finalization and Recovery Paths

Upon successful completion, `resolveCheckoutBranch` selects the best branch to check out, and `updateBaseSHAs` refreshes base SHAs in the stack metadata. If any PRs were modified, the state file transitions to `PhasePendingSubmit`, prompting users to run `gh stack submit`. Otherwise, the state file clears ([[`internal/modify/apply.go`](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go)](https://github.com/github/gh-stack/blob/main/internal/modify/apply.go#L496-L514)). The modified stack saves atomically using `stack.SaveWithLock`.

For recovery:
- **Abort**: `runModifyAbort` reads the state file and invokes `modify.UnwindFromStateFile` to restore branch tips from the snapshot, delete temporary branches, and revert the stack JSON ([[`internal/modify/unwind.go`](https://github.com/github/gh-stack/blob/main/internal/modify/unwind.go)](https://github.com/github/gh-stack/blob/main/internal/modify/unwind.go)).
- **Continue**: `runModifyContinue` loads the state, completes in-progress cherry-picks or rebases, then resumes the cascading rebase for remaining branches via `ContinueApply`.

## Summary

- The `gh stack modify` command validates repository state through `checkModifyPreconditions` before launching the TUI.
- User interactions convert to concrete `modify.Action` structs via `BuildPlan` after the Bubble Tea interface returns.
- A pre-modify snapshot and `StateFile` enable safe recovery through `--abort` and `--continue` workflows.
- Execution follows five phases: rename, insert, fold (up/down), drop, and reorder.
- A cascading rebase using `originalParentTips` ensures clean commit replay onto new parents.
- Conflicts trigger `PhaseConflict` states that preserve the exact position in the operation sequence for resumption.

## Frequently Asked Questions

### What happens if a conflict occurs during `gh stack modify`?

When `git.RebaseOnto` encounters a conflict, the command immediately writes a `PhaseConflict` state to `.git/gh-stack.modify`, recording the conflicted branch and remaining operations. The user resolves the conflict manually, then runs `gh stack modify --continue` to resume the cascading rebase from the exact failure point.

### How does `gh stack modify` ensure I can undo changes?

Before executing any Git operations, the command calls `BuildSnapshot` to capture all branch names, tip SHAs, and the full stack JSON. This snapshot persists in the `StateFile`. If you run `gh stack modify --abort`, the `UnwindFromStateFile` function restores branch tips from this snapshot and reverts the stack metadata to its pre-modify state.

### Can I rename branches while reordering them in the same session?

Yes. The `ModifyBranchNode` structure tracks `OriginalPosition`, `Removed`, and `PendingAction` independently. The `BuildPlan` function processes all modifications simultaneously, generating separate `Action` structs for renames and reorder operations that execute in the correct sequence during the five-phase pipeline.

### What is the difference between folding up and folding down?

Fold-down cherry-picks commits from the upper branch onto the lower branch immediately, creating a new merge base. Fold-up only adjusts the `originalParentTips` mapping; both sets of commits replay during the subsequent cascading rebase onto the new parent, effectively merging the lower branch into the upper one's history.