How GitButler Manages Its Staging Area: Virtual Index Architecture

GitButler implements a virtual staging area that maps hunks to specific branches or stacks without modifying the underlying Git index, using an assignment database to track hunk-to-branch relationships until commit time.

GitButler revolutionizes Git workflows by reimagining how developers stage changes for commit. Unlike traditional Git that relies on a single index, GitButler manages its staging area through a sophisticated virtual index system that allows multiple virtual branches to coexist while keeping the working directory clean. This architecture enables users to assign individual hunks to different virtual branches without ever touching the repository's actual Git index.

What Is the GitButler Staging Area?

Traditional Git uses a single staging area (the index) to prepare changes for commit. GitButler replaces this with a virtual index that records hunk-to-stack/branch assignments rather than file states.

In this model, uncommitted changes live only in the working tree. The GitButler staging area tracks metadata that says "hunk X belongs to stack Y" or "file Z belongs to branch W." This allows multiple virtual branches to share the same working directory without interfering with each other or the real Git index.

Core Components of the GitButler Staging System

The virtual staging implementation spans several crates in the GitButler codebase, each handling specific responsibilities from CLI parsing to index restoration.

Staging Reset Helper (staging.rs)

The crates/gitbutler-repo/src/staging.rs file provides the reset_index helper function. This utility restores the repository's real Git index to a known tree after operations like pre-commit hooks. It guarantees the working tree never remains in a half-committed state by ensuring the index returns to its original condition.

Hunk Assignment Engine (assign.rs)

At the heart of the staging system lies crates/but/src/command/legacy/rub/assign.rs. This module builds HunkAssignmentRequest objects that map hunks to AssignTarget instances—either branch names or stack IDs (StackId).

Key functions include:

  • branch_name_to_stack_id: Resolves a branch name to its internal stack identifier
  • to_assignment_request: Converts hunk headers and paths into formal assignment requests
  • do_assignments: Persists requests to the assignment database
  • assign_all: Provides bulk operations for staging or unstaging entire branches

CLI Interface (mod.rs)

The crates/but/src/command/legacy/rub/mod.rs file implements the user-facing but stage command. It resolves CLI arguments, validates that targets refer to uncommitted hunks, and calls assign_uncommitted_to_branch (or assign_uncommitted_to_stack) to create the virtual staging entries.

Interactive TUI (stage_viewer.rs)

For interactive use, crates/but/src/tui/stage_viewer.rs provides a terminal UI that lets users pick hunks visually. It creates assignment requests via to_assignment_request and calls do_assignments, reusing the same core logic as the CLI.

Pre-Commit Hook Management (hooks.rs)

The crates/gitbutler-repo/src/hooks.rs file handles the transition from virtual staging to actual Git commits. Before committing, it swaps the repository index to the tree supplied by the virtual stack. A scope guard (_guard) automatically invokes staging::reset_index after the pre-commit hook runs, restoring the original index state regardless of whether the hook succeeds or panics.

How the GitButler Staging Area Works: Step-by-Step

The virtual staging system operates through a distinct four-phase workflow that keeps the working directory clean while preparing commits for multiple branches.

  1. Uncommitted changes remain in the working tree only.
    Unlike traditional Git, but stage does not modify the Git index. Instead, GitButler records assignments (HunkAssignment) that declare "hunk X belongs to stack Y" or "file Z belongs to branch W."

  2. Staging creates metadata assignments.
    When you run but stage <file> <branch>, the CLI resolves arguments to CliId::Uncommitted identifiers. The system calls assign_uncommitted_to_branch (or assign_uncommitted_to_stack) to build (hunk_header, path) pairs, converts them to HunkAssignmentRequest objects via to_assignment_request, and persists them via do_assignments to the assignment database.

  3. Committing builds trees from assignments.
    When you commit, the core logic queries the assignment database for all hunks belonging to the active stack. It constructs a Git tree reflecting exactly those hunks and writes it via index.write_tree_to. The pre-commit hook (hooks::pre_commit_with_tree) temporarily swaps the real index to this tree, executes user-defined hooks, then a scope guard runs staging::reset_index to restore the original index state.

  4. Unstaging removes assignments.
    To unstage, the system sets the target to None (no stack), which removes the stack_id from the assignment and leaves the hunk "unassigned" in the working tree.

Practical Examples: Using the GitButler Staging Area

The following examples demonstrate how to interact with GitButler's virtual staging system via CLI and programmatic APIs.

Staging a File from the CLI

Stage an entire file to a specific virtual branch:


# Stage the whole file `src/main.rs` onto virtual branch `feature/login`

but stage src/main.rs feature/login

Internally, this executes the logic found in crates/but/src/command/legacy/rub/mod.rs:

let files = parse_sources_with_disambiguation(ctx, &id_map, file_or_hunk_str, out)?;
let branch = resolve_single_id(ctx, &id_map, branch_str, "Branch", out)?;
assign_uncommitted_to_branch(ctx, uncommitted_cli_id, branch_name, out)?;

Staging a Hunk Programmatically

For custom tooling, use the assignment API directly:

use but_ctx::Context;
use but_hunk_assignment::HunkAssignmentRequest;
use gitbutler_core::assign::{to_assignment_request, do_assignments};

fn stage_hunk(ctx: &mut Context, header: Option<HunkHeader>, path: BString, target_branch: &str) -> anyhow::Result<()> {
    // Convert branch name → stack_id
    let reqs = to_assignment_request(ctx, std::iter::once((header, path)), Some(target_branch))?;
    // Persist the assignment
    do_assignments(ctx, reqs, &mut OutputChannel::null())
}

Resetting the Real Git Index After Hooks

The system ensures index integrity during commit operations:

use gitbutler_repo::staging::reset_index;
use git2::Repository;

// In `gitbutler-repo/src/hooks.rs` (pre‑commit wrapper)
let original_tree = repo.index()?.write_tree()?;   // capture original index
let _guard = scopeguard::guard((), |_| {
    // This runs even if the hook panics
    let _ = reset_index(repo, original_tree);
});

Unstaging All Changes from a Branch

To remove all assignments from a specific branch:

use but::command::legacy::rub::assign::assign_all;
use but::command::legacy::rub::assign::AssignTarget;

fn unstage_all_from_branch(ctx: &mut Context, branch: &str) -> anyhow::Result<()> {
    // from = that branch, to = None (unassigned)
    assign_all(
        ctx,
        Some(AssignTarget::Branch(branch)),
        None,
        &mut OutputChannel::null(),
    )
}

Summary

GitButler's virtual staging area reimagines version control by decoupling change tracking from Git's traditional index:

  • Virtual Index Architecture: The system uses a metadata layer (HunkAssignment records) to map hunks to stacks or branches without touching the real Git index.
  • Core Components: Key files like crates/gitbutler-repo/src/staging.rs and crates/but/src/command/legacy/rub/assign.rs handle index restoration and hunk assignment logic.
  • Assignment Workflow: Staging creates HunkAssignmentRequest objects persisted via do_assignments, while committing builds trees from these assignments and uses reset_index to maintain index integrity.
  • Multi-Branch Support: The design allows simultaneous work on multiple virtual branches by keeping all changes in the working tree until explicitly assigned and committed.

Frequently Asked Questions

How does GitButler's staging area differ from Git's traditional index?

Git's traditional index is a single binary file that represents the proposed next commit state. GitButler's staging area is a virtual metadata layer that tracks which hunks belong to which virtual branch or stack without modifying the underlying Git index. This allows multiple virtual branches to coexist in the same working directory, with hunks assigned to different targets until commit time.

What happens to the real Git index when I stage files in GitButler?

When you run but stage or use the TUI to stage hunks, the real Git index remains unchanged. The system creates HunkAssignmentRequest objects that are persisted to an assignment database via do_assignments. The actual Git index is only temporarily modified during the commit process when hooks::pre_commit_with_tree swaps it to a tree built from the assigned hunks, then immediately restores it using staging::reset_index.

Can I unstage changes after assigning them to a branch in GitButler?

Yes, unstaging removes the assignment metadata rather than manipulating the Git index. You can unstage specific hunks or entire branches by calling assign_all with AssignTarget::None, which removes the stack_id from the assignment records. The hunks remain in your working tree as unassigned changes that you can then assign to different branches or stacks.

How does GitButler prevent index corruption during pre-commit hooks?

GitButler prevents index corruption through a defensive scope guard pattern implemented in crates/gitbutler-repo/src/hooks.rs. Before executing pre-commit hooks, the system captures the original index tree using repo.index()?.write_tree()?. It then creates a scope guard that automatically invokes staging::reset_index when the scope exits, ensuring the original index is restored even if the hook panics or fails. This guarantees the working tree and index remain consistent regardless of hook execution outcomes.

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 →