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

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 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#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#L85-L100)).

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#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#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#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#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#L124-L150).

Phase 3: Folding Branches

Folding operates in two directions:

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#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#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#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.

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#L496-L514)). The modified stack saves atomically using stack.SaveWithLock.

For recovery:

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.

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 →