# How GitButler Manages Its Staging Area: Virtual Index Architecture

> Discover how GitButler uses a virtual staging area and assignment database to manage changes and track hunk-to-branch relationships without altering the Git index for efficient commits.

- Repository: [GitButler/gitbutler](https://github.com/gitbutlerapp/gitbutler)
- Tags: internals
- Published: 2026-02-16

---

**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`](https://github.com/gitbutlerapp/gitbutler/blob/main/staging.rs))

The [`crates/gitbutler-repo/src/staging.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/assign.rs))

At the heart of the staging system lies [`crates/but/src/command/legacy/rub/assign.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/mod.rs))

The [`crates/but/src/command/legacy/rub/mod.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/stage_viewer.rs))

For interactive use, [`crates/but/src/tui/stage_viewer.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/hooks.rs))

The [`crates/gitbutler-repo/src/hooks.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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:

```bash

# 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`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but/src/command/legacy/rub/mod.rs):

```rust
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:

```rust
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:

```rust
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:

```rust
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`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-repo/src/staging.rs) and [`crates/but/src/command/legacy/rub/assign.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/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.