Stacked git branches with git-spice

Stacked git branches with git-spice

June 5, 2025
A Golang gopher dressed as a worm from the movie/book Dune

Ever find yourself working on a feature that requires either creating a massive PR, which is hard to review, or waiting for each small change to be merged before you can proceed? Implementations that can naturally be broken into smaller PRs, but it is hard to keep them in sync as you get reviews requiring changes.

git-spice is a CLI tool written in Go that significantly simplifies this process. It lets you create branches that build on top of each other while keeping individual changes focused and reviewable. It also makes it very easy to re-stack the branches, keeping them in sync, along with other quality of life improvements.

What is Branch Stacking?

Think of it like this: instead of cramming everything into one huge PR, you create a series of small, connected PRs that tell a story. Each one builds on the previous, but reviewers can understand and approve them individually.

main branch

feat/payment-abstraction
Add payment provider interface

feat/stripe-integration
Implement Stripe provider

feat/payment-ui
Add payment selection UI

PR #1
payment-abstraction

PR #2
stripe-integration
(depends on PR #1)

PR #3
payment-ui
(depends on PR #2)

How git-spice Works

git-spice operates as a layer above Git, tracking branch relationships and automating dependency management. It stores metadata locally in your repository, requiring no external services or configuration changes.

ℹ️
A common issue is that the gs command often conflicts with ghostscript. I created an alias spice for git-spice instead.

Basic Workflow

# Initialize tracking and create base branch
gs branch create payment-abstraction
git commit -m "Add payment provider interface"

# Create dependent branches automatically
gs branch create stripe-integration
git commit -m "Implement Stripe provider"

gs branch create payment-ui
git commit -m "Add payment selection UI"

Each gs branch create command establishes the current branch as the parent for the new branch, building the dependency chain automatically.

Handling Changes and Re-stacking

The key advantage becomes apparent when handling review feedback:

# Return to base branch to address review comments
gs branch checkout payment-abstraction
git commit -m "Fix payment validation based on review"

# Automatically rebase all dependent branches
gs stack restack

The restack command identifies all branches that depend on the current branch and rebases them to include the new changes. This eliminates manual Git operations and potential conflicts.

Submitting Pull Requests

# Submit entire stack with proper dependencies
gs stack submit

# Or submit individual branches with correct base references
gs branch submit

git-spice handles PR creation with appropriate base branch references and dependency links, ensuring reviewers understand the relationship between changes.

Technical Benefits

Dependency Automation: Eliminates manual tracking and rebasing of dependent branches when changes occur in the stack.

Conflict Resolution: Centralizes conflict resolution to the restack operation, preventing cascading merge conflicts across the stack.

State Persistence: Maintains branch relationships locally using Git references, enabling collaboration without external dependencies.

Integration Flexibility: Works with existing Git workflows and hosting platforms (GitHub, GitLab) without requiring workflow changes.

Implementation Strategy

  1. Install git-spice following the installation guide
  2. Identify a feature with natural logical breakpoints for your first stack
  3. Focus on mastering the core commands: gs branch create, gs stack restack, gs stack submit
  4. Gradually incorporate additional features like branch navigation and stack manipulation

Limitation: Squash-and-Merge

One key limitation to understand is how git-spice handles squash-and-merge operations. When a PR is squash-merged into the main branch, GitHub/GitLab replace all commits in that PR with a single commit (squashing) that has a different hash.

The problem is that upstream branches in your stack still reference the old, unsquashed commit history. The hosting platforms don’t automatically reconcile this new squashed commit with the dependent branches, even though the content is identical.

This means when a branch gets squash-merged, all branches that depend on it need to be restacked using gs stack restack, and their PRs will be updated accordingly.

Also something to be aware: depending on your settings, restacking changes in base branches will dismiss already approved PRs.