How GitButler's Parallel Branch System Works: A Deep Dive into Virtual Stacking
GitButler's parallel branch system allows developers to work on multiple independent feature branches simultaneously by maintaining isolated virtual stacks that are dynamically merged into a single gitbutler/workspace branch using an octopus merge strategy.
The gitbutlerapp/gitbutler repository implements this system through a sophisticated Rust-based architecture that separates branch metadata from Git's native refs. This approach enables developers to apply, unapply, and reorder feature work without rewriting commit history or managing complex rebase operations.
Core Concepts of the Parallel Branch System
The parallel branch system rests on three foundational abstractions that separate user-visible branches from Git's internal representation.
Virtual Branches (Stacks)
A virtual branch (internally called a Stack) is a lightweight metadata structure describing a series of commits belonging to a user-visible branch. Unlike traditional Git branches, these exist outside the .git/refs namespace.
In crates/gitbutler-stack/src/stack.rs, the Stack struct defines this abstraction:
pub struct Stack {
pub id: StackId,
pub heads: Vec<StackBranch>,
pub in_workspace: bool, // Key flag for parallel operation
// ... additional fields
}
The in_workspace boolean determines whether the stack participates in the parallel merge. Stacks persist in a TOML file (virtual_branches.toml) managed by the VirtualBranchesHandle API in crates/gitbutler-stack/src/state.rs.
Workspace State
The WorkspaceState represents a snapshot of all currently applied parallel branches. It captures the base commit and the tree objects of every active stack.
Defined in crates/gitbutler-workspace/src/branch_trees.rs:
pub struct WorkspaceState {
heads: Vec<git2::Oid>, // Tree IDs of each applied stack
base: git2::Oid, // Common ancestor commit tree
}
This state is ephemeral—recomputed whenever branches are applied or unapplied—and serves as the input for the merge algorithm that produces the working directory view.
The gitbutler/workspace Branch
The gitbutler/workspace branch is a real Git ref that contains a single synthetic commit representing the union of all applied parallel branches. Created during setup in crates/but/src/setup.rs, this branch never holds user changes directly.
Instead, its commit is the result of an octopus merge combining every stack's tree. This allows Git's standard tools to view the parallel work as a single coherent state while preserving the isolation of individual feature branches in the virtual layer.
How Virtual Branches Are Stored
When users create a branch without supplying a name, GitButler generates a parallel branch automatically. As documented in crates/but/src/args/branch.rs:
/// If no branch name is provided, a new parallel branch with a generated
/// name will be created.
The persistence layer uses the VirtualBranchesHandle to serialize stack metadata:
// crates/gitbutler-stack/src/state.rs
pub fn set_stack(&self, stack: Stack) -> Result<()> {
let mut vb = self.read_file()?;
vb.branches.insert(stack.id, stack);
self.write_file(&vb)
}
Each Stack contains:
heads– A list ofStackBranchobjects pointing to specific commitsin_workspace– true when the stack is actively applied to the workspace
The Workspace Merge Algorithm
When at least one stack is marked in_workspace = true, GitButler rebuilds the workspace view through a multi-step merge process.
Building the Workspace State
The system first collects the tree objects of all applied stacks via list_stacks_in_workspace in crates/gitbutler-stack/src/state.rs:
pub fn list_stacks_in_workspace(&self) -> Result<Vec<Stack>> {
self.list_all_stacks()
.map(|stacks| stacks.into_iter().filter(|s| s.in_workspace).collect())
}
For each stack, GitButler reads the tree associated with its tip commit. These trees, combined with the workspace base commit, form the WorkspaceState.
Octopus Merge Implementation
The actual merge occurs in crates/gitbutler-workspace/src/branch_trees.rs through iterative pairwise merging:
pub fn merge_workspace(repo: &git2::Repository, workspace: WorkspaceState) -> Result<git2::Oid> {
let mut output = workspace.base;
for head in workspace.heads {
let mut merge_options = git2::MergeOptions::new();
merge_options.fail_on_conflict(true);
let mut merge = repo.merge_trees(
&repo.find_tree(workspace.base)?,
&repo.find_tree(output)?,
&repo.find_tree(head)?,
Some(&merge_options),
)?;
output = merge.write_tree_to(repo)?;
}
Ok(output)
}
This octopus merge strategy combines all branch trees into a single tree object. Git's native merge algorithm handles file-level conflicts, ensuring the workspace presents a coherent filesystem state even when parallel branches touch the same files.
Applying and Unapplying Parallel Branches
The but apply and but unapply commands toggle a stack's participation in the workspace.
Applying a Branch
When running but apply <branch>, the system executes the logic in crates/but-workspace/src/branch/apply.rs:
- Validate the target reference (local or remote tracking)
- Mark the stack
in_workspace = trueviaVirtualBranchesHandle::set_stack - Re-create the workspace state with
WorkspaceState::create, now including the new stack's tree - Octopus-merge all heads via
merge_workspace - Update the
gitbutler/workspacereference to the new merge commit
Key implementation snippet:
// crates/but-workspace/src/branch/apply.rs -> function::apply
let ws = workspace; // current Workspace projection
let new_state = WorkspaceState::create(ctx, perm)?; // includes all in‑workspace stacks
let merged_oid = merge_workspace(repo, new_state)?;
repo.reference(
"refs/heads/gitbutler/workspace", merged_oid, true, "apply parallel branch"
)?;
Unapplying a Branch
but unapply <branch> performs the inverse operation, as implemented in crates/but-workspace/src/branch/unapply.rs:
- Find the stack ID
- Set
in_workspace = falseviaVirtualBranchesHandle::mark_as_not_in_workspace - Re-compute the workspace state without that stack
- Rewrite the
gitbutler/workspacebranch with the reduced merge
If the unapplied branch was the only applied stack, the workspace branch is deleted, returning the repository to a standard Git state.
Garbage Collection and Cleanup
The system automatically removes stale virtual branches through garbage collection logic in crates/gitbutler-stack/src/state.rs:
// crates/gitbutler-stack/src/state.rs -> fn garbage_collect
if !stack.in_workspace && branch_head == repo.merge_base(branch_head, target.sha)? {
to_remove.push(stack.id);
}
Stacks are eligible for removal when they are not in the workspace and their head commit has been fully merged into the target branch (indicated by the merge base check).
Parallel vs. Stacked Branches
Understanding the distinction between parallel and stacked workflows clarifies GitButler's design philosophy:
- Stacked branches form a linear series where each new branch builds directly on top of the previous one. This creates a dependency chain that requires careful rebasing when reordering.
- Parallel branches remain independent stacks that are merged together only within the workspace. They never rewrite each other's history, making it safe to apply many features simultaneously and switch contexts without destructive rebase operations.
This architecture allows developers to treat feature branches as truly independent workstreams while presenting a unified working directory view.
Summary
GitButler's parallel branch system reimagines branch management through virtual abstraction:
- Virtual branches (
Stackobjects) store branch metadata invirtual_branches.tomlwith anin_workspaceflag controlling visibility - Workspace state captures the base commit and tree objects of all applied stacks via
WorkspaceStateincrates/gitbutler-workspace/src/branch_trees.rs - Octopus merging combines parallel branch trees into a single coherent commit on the
gitbutler/workspacebranch - Apply/unapply operations toggle the
in_workspaceflag and rebuild the merged workspace view without rewriting Git history - Garbage collection automatically removes virtual branches that have been fully merged and are no longer in the workspace
Frequently Asked Questions
How does GitButler differ from traditional Git worktrees?
GitButler uses a single working directory managed through virtual branch metadata and octopus merges, whereas Git worktrees create separate directory checkouts for each branch. The parallel branch system allows instantaneous context switching by toggling the in_workspace flag and rebuilding the merged tree, without requiring disk operations or directory changes.
What happens when two parallel branches modify the same file?
When conflicts occur during the octopus merge in merge_workspace, Git's standard merge algorithm attempts automatic resolution using the three-way merge strategy between the workspace base, current output tree, and the incoming branch tree. If automatic resolution fails, the fail_on_conflict(true) option ensures the operation reports the conflict rather than producing a corrupted state, allowing the user to resolve it through GitButler's interface.
Can I use GitButler's parallel branches with a standard Git workflow?
Yes, the gitbutler/workspace branch is a standard Git ref that can be pushed, pulled, and inspected with normal Git commands. However, the virtual branch metadata stored in virtual_branches.toml is specific to GitButler. Without GitButler, collaborators see only the merged workspace commit, not the individual parallel branches, making this workflow most effective when team members also use GitButler or when parallel branches are used for personal organization before squashing to traditional branches.
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 →