How gh-stack Navigation Commands Work: up, down, top, bottom, and trunk Explained
The gh-stack navigation commands treat branches as a linear stack starting from the trunk, allowing you to move up, down, or jump to ends while automatically skipping merged branches.
The github/gh-stack extension models your feature branches as an ordered list anchored to a base branch. Understanding how the gh-stack navigation commands function helps you move efficiently between branches without manually typing checkout commands or worrying about merged branches cluttering your workflow.
Understanding the Stack Structure
gh-stack conceptualizes your work as a linear stack that begins at the trunk (typically main or develop) and extends upward, with each successive branch farther from the base. Every branch in this structure tracks whether it has been merged, allowing navigation commands to skip inactive branches automatically.
The Five Core Navigation Commands
up [n] - Move Away From the Trunk
The up command checks out a branch n steps farther from the trunk. If you omit the number, it defaults to one step.
According to the source code in cmd/navigate.go, the UpCmd function calls runNavigate(cfg, n) with a positive delta. The implementation at lines 11-34 calculates the new position, filters out merged branches using s.ActiveBranchIndices(), and checks out the target via git.CheckoutBranch.
down [n] - Move Toward the Trunk
The down command checks out a branch n steps closer to the trunk, defaulting to one step if no number is provided.
As implemented in cmd/navigate.go at lines 38-62, DownCmd invokes runNavigate(cfg, -n) with a negative delta. The same core logic handles the movement but in the reverse direction, ensuring merged branches are bypassed.
top - Jump to the Farthest Branch
The top command jumps directly to the branch farthest from the trunk that remains part of the stack.
In cmd/navigate.go at lines 65-78, TopCmd calls runNavigateToEnd(cfg, true), which selects the last branch in the ordered slice returned by loadStack.
bottom - Jump to the Closest Active Branch
The bottom command jumps to the branch closest to the trunk that is still active (not merged). If every branch has been merged, it falls back to the first branch with a warning message.
This logic resides in cmd/navigate.go at lines 79-92, where BottomCmd invokes runNavigateToEnd(cfg, false) to select the first active branch rather than the absolute first branch.
trunk - Return to the Base Branch
The trunk command switches to the trunk branch of the current stack (e.g., main or develop). If the trunk does not exist locally, the command fetches the appropriate remote and creates a local copy before checking it out.
As shown in cmd/trunk.go at lines 11-66, TrunkCmd calls runTrunk, which resolves the trunk name, ensures the branch exists locally using git.BranchExists, and performs the checkout.
Internal Implementation Details
Loading the Stack State
All navigation commands begin by calling loadStack(cfg, "") from cmd/utils.go. This function reads the .git/gh-stack file and returns a Stack object containing:
- Trunk: The base branch name
- Branches: An ordered slice of
BranchRefobjects, each tracking merge status
Position Calculation and Merge Skipping
The runNavigate function in cmd/navigate.go determines your current position using s.IndexOf(currentBranch). If you are currently on the trunk, special handling moves you to the first active branch.
When the current branch is not merged, the code builds a list of active indices via s.ActiveBranchIndices(). Navigation occurs within this filtered list, and the system counts how many merged branches were bypassed to display a helpful message to the user.
Boundary Handling and Checkout
The navigation logic clamps target indices to prevent moving beyond the stack boundaries. If the calculated target equals your current branch, the system displays a friendly "already at the..." message via cfg.Printf.
Once a valid target is determined, the branch name is passed to git.CheckoutBranch from internal/git/gitops.go, which executes git checkout <branch>. Success and warning messages are rendered through cfg.Successf and cfg.Warningf defined in internal/config/config.go.
Practical Usage Examples
# Move one branch up (skip any merged branches)
gh stack up
# Move three branches up the stack
gh stack up 3
# Move one branch down toward trunk
gh stack down
# Jump straight to the topmost branch
gh stack top
# Jump to the bottom (first active) branch
gh stack bottom
# Switch to the trunk (e.g., main) of the current stack
gh stack trunk
Summary
- gh-stack models branches as a linear stack starting from the trunk and extending upward.
- The
upanddowncommands move relative to your current position, accepting optional step counts and automatically skipping merged branches. topandbottomprovide absolute navigation to the ends of the stack, withbottomspecifically targeting the first non-merged branch.trunkchecks out the base branch, fetching it from remote if it does not exist locally.- Navigation logic in
cmd/navigate.gohandles boundary clamping, merge detection viaActiveBranchIndices(), and delegates checkout operations tointernal/git/gitops.go.
Frequently Asked Questions
What happens if I try to navigate past the end of the stack?
The runNavigate function clamps the target index to the bounds of the active branch list. If you are already at the top or bottom, the command prints a message indicating you are already at that position and does not attempt an invalid checkout.
How does gh-stack handle merged branches during navigation?
When executing up or down, the system calls s.ActiveBranchIndices() to build a list of non-merged branches. Navigation moves through this filtered list, effectively skipping merged branches. The system tracks how many merged branches were bypassed and reports this count to provide visibility into the skipped entries.
What is the difference between the bottom and trunk commands?
The trunk command always checks out the base branch (e.g., main) defined in the stack configuration. The bottom command checks out the first active feature branch—the one closest to the trunk but not yet merged. If all feature branches are merged, bottom falls back to the first branch with a warning, whereas trunk always targets the base branch.
Why does the trunk command fetch from remote?
As implemented in cmd/trunk.go, the runTrunk function checks if the trunk branch exists locally using git.BranchExists. If the branch is missing, it fetches the appropriate remote and creates a local tracking branch before checking it out. This ensures you can always return to the trunk even if you have not checked it out previously in the current repository state.
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 →