# How the gitbutler-* Crates Interact: A Deep Dive into GitButler's Modular Rust Architecture

> Explore how gitbutler-* Rust crates interact within GitButler. Understand its modular architecture, clean dependencies, and fast compilation for efficient Git workflows.

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

---

**The GitButler codebase organizes functionality into small, single-purpose crates prefixed with `gitbutler-`, where each crate owns a specific domain and exposes minimal APIs consumed by others to keep dependencies clean and compilation fast.**

GitButler is a Tauri-based Git client that manages virtual branches and workspace states through a sophisticated Rust backend. Understanding the interaction between `gitbutler-*` crates is essential for contributors extending the platform or integrating with its APIs. This article examines the architectural relationships between the workspace, repository, stack, watcher, and core crates, demonstrating how they compose to implement complex Git operations.

## The Core Architecture of gitbutler-* Crates

The repository follows a layered architecture where dependencies flow from low-level primitives toward high-level feature implementations. The `but-core` crate sits at the foundation, providing language-agnostic data structures like `TreeChange` and `UnifiedPatch` that higher-level crates consume without circular dependencies.

Above the core, `gitbutler-repo` provides Git repository access patterns, while `gitbutler-stack` manages virtual branch metadata. The `gitbutler-workspace` crate computes workspace states by coordinating between these lower layers. Finally, `gitbutler-watcher` monitors the filesystem and triggers updates through the stack and workspace APIs.

## Key Interactions Between gitbutler-* Crates

### Workspace and Stack Integration

The `gitbutler-workspace` crate depends on `gitbutler-stack` to enumerate virtual branches when calculating the workspace base. In [`crates/gitbutler-workspace/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-workspace/src/lib.rs), the workspace logic retrieves stack heads through the `VirtualBranchesHandle`:

```rust
let vb_state = VirtualBranchesHandle::new(ctx.project_data_dir());
let stacks = vb_state.list_stacks_in_workspace()?;
let stack_heads = stacks.iter().map(|b| b.head_oid(ctx)).collect::<Result<Vec<_>>>()?;

```

These heads feed into `workspace_base_from_heads`, which computes the merge base of all virtual branches plus the default target. This design keeps stack metadata management encapsulated within `gitbutler-stack` while allowing the workspace crate to perform complex merge-base calculations without duplicating stack logic.

### Repository Utilities

The `gitbutler-repo` crate provides shared Git operations that prevent code duplication across feature crates. Its `signature` function in [`crates/gitbutler-repo/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-repo/src/lib.rs) generates canonical Git signatures for commits:

```rust
pub fn signature(purpose: SignaturePurpose) -> anyhow::Result<git2::Signature<'static>> {
    let signature = gix::actor::Signature {
        name: GITBUTLER_COMMIT_AUTHOR_NAME.into(),
        email: GITBUTLER_COMMIT_AUTHOR_EMAIL.into(),
        time: commit_time(match purpose {
            SignaturePurpose::Author => "GIT_AUTHOR_DATE",
            SignaturePurpose::Committer => "GIT_COMMITTER_DATE",
        }),
    };
    gix_to_git2_signature(signature.to_ref(&mut TimeBuf::default()))
}

```

Higher-level crates import this via `use gitbutler_repo::signature;`, ensuring consistent author metadata across all GitButler-generated commits without each crate reimplementing environment variable handling and signature formatting.

### File System Monitoring

The `gitbutler-watcher` crate operates independently but integrates tightly with stack and workspace logic through its event handler trait. In [`crates/gitbutler-watcher/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-watcher/src/lib.rs), the watcher spawns a background task:

```rust
let monitor = gitbutler_filemonitor::spawn(
    project_id,
    worktree_path.as_ref(),
    events_out.clone(),
    watch_mode,
)?;

```

When filesystem events occur, the handler implementation (typically in the UI layer) calls into `gitbutler-stack` to refresh virtual branch metadata and `gitbutler-workspace` to recompute the workspace base, creating a reactive update cycle that keeps the UI synchronized with disk state.

### Core Primitives

The `but-core` crate defines foundational data structures that enable inter-crate communication without circular dependencies. Located in [`crates/but-core/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-core/src/lib.rs), it provides types like `TreeChange` that represent worktree modifications:

```rust
pub struct TreeChange {
    pub path: BString,
    pub status: TreeStatus,
}

```

All higher-level crates—workspace, watcher, stack, and feature crates—use these types to exchange information about repository state, ensuring type safety across crate boundaries while maintaining loose coupling.

## How Feature Crates Compose Lower-Level Primitives

Feature-specific crates like `gitbutler-commit` and `gitbutler-cherry-pick` demonstrate the architectural pattern of composing lower-level crates to implement user-visible operations. These crates typically follow a four-step pattern:

1. **Gather context** by calling `gitbutler-workspace` to obtain the current workspace base
2. **Manipulate stacks** through `gitbutler-stack` APIs to update virtual branch metadata
3. **Execute Git operations** using helpers from `gitbutler-repo` for signatures and configuration
4. **Return results** using data structures from `but-core` for diff and change representation

This composition pattern appears consistently across [`crates/gitbutler-commit/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-commit/src/lib.rs) and [`crates/gitbutler-cherry-pick/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-cherry-pick/src/lib.rs), demonstrating how the modular architecture enables code reuse while keeping each crate's API surface minimal.

## Practical Example: Refreshing Workspace State

The following example demonstrates the end-to-end interaction between `gitbutler-watcher`, `gitbutler-stack`, and `gitbutler-workspace` when reacting to a file change:

```rust
use gitbutler_watcher::{watch_in_background, Handler, WatcherHandle};
use gitbutler_workspace::workspace_base;
use gitbutler_repo::RepoCommands;
use gitbutler_stack::VirtualBranchesHandle;
use gitbutler_project::ProjectId;
use but_settings::AppSettingsWithDiskSync;

// 1️⃣  Set up a handler that recomputes the workspace base
#[derive(Clone)]
struct RefreshHandler {
    ctx: Context, // from but-ctx
}
impl Handler for RefreshHandler {
    fn handle(&self, _event: InternalEvent, _settings: AppSettingsWithDiskSync) -> anyhow::Result<()> {
        // Re‑enumerate the stacks
        let vb = VirtualBranchesHandle::new(self.ctx.project_data_dir());
        let stacks = vb.list_stacks_in_workspace()?;
        let heads: Vec<_> = stacks.iter().map(|s| s.head_oid(&self.ctx)).collect::<Result<_>>()?;

        // Compute the new workspace base
        let base = workspace_base_from_heads(&self.ctx, &self.ctx.repo_shared(), &heads)?;
        println!("New workspace base: {}", base);
        Ok(())
    }
}

// 2️⃣  Launch the watcher in the background
fn start_watcher(project_id: ProjectId, worktree_path: impl AsRef<std::path::Path>) -> anyhow::Result<WatcherHandle> {
    let ctx = Context::new(project_id); // simplified
    let handler = RefreshHandler { ctx };
    watch_in_background(
        handler,
        worktree_path,
        project_id,
        AppSettingsWithDiskSync::default(),
        gitbutler_filemonitor::WatchMode::Recursive,
    )
}

```

This example illustrates how the `RefreshHandler` lives in the UI layer but only imports types from `gitbutler-workspace`, `gitbutler-stack`, and `gitbutler-repo`. The watcher spawns a background task that triggers workspace recomputation whenever filesystem events occur, maintaining synchronization between the disk state and GitButler's virtual branch model.

## Summary

- **Modular Architecture**: GitButler organizes code into small, single-purpose `gitbutler-*` crates that minimize dependencies and enable independent testing.
- **Layered Dependencies**: The architecture flows from `but-core` (primitives) → `gitbutler-repo` (Git access) → `gitbutler-stack` (virtual branches) → `gitbutler-workspace` (workspace state) → feature crates (user operations).
- **Workspace-Stack Interaction**: The workspace crate uses `VirtualBranchesHandle` from `gitbutler-stack` to enumerate virtual branches and compute merge bases without duplicating stack logic.
- **Shared Utilities**: `gitbutler-repo` provides canonical Git operations like `signature()` that ensure consistent commit metadata across all crates.
- **Reactive Updates**: `gitbutler-watcher` monitors the filesystem and triggers workspace refreshes through the stack and workspace APIs, keeping the UI synchronized with disk changes.

## Frequently Asked Questions

### How do the gitbutler-* crates avoid circular dependencies?

The crates avoid circular dependencies by strictly layering their architecture. The `but-core` crate sits at the bottom with zero dependencies on other `gitbutler-*` crates, providing primitive data types like `TreeChange` and `UnifiedPatch`. Higher-level crates like `gitbutler-repo` and `gitbutler-stack` depend only on `but-core` or lower layers, while `gitbutler-workspace` consumes both stack and repo crates. This unidirectional dependency graph ensures that no crate imports a higher-level module, eliminating circular references.

### What is the relationship between gitbutler-workspace and gitbutler-stack?

The `gitbutler-workspace` crate depends on `gitbutler-stack` to enumerate virtual branches when calculating workspace state. Specifically, the workspace code uses `VirtualBranchesHandle::list_stacks_in_workspace()` to retrieve all active virtual branches, then extracts their head OIDs to compute the workspace base via `workspace_base_from_heads`. This interaction allows the workspace crate to perform complex merge-base calculations without duplicating the stack metadata management logic encapsulated in `gitbutler-stack`.

### How does gitbutler-watcher integrate with the rest of the system?

The `gitbutler-watcher` crate operates as a background service that monitors the project's worktree for filesystem changes. It spawns a tokio task via `watch_in_background` that emits `InternalEvent` instances to a user-provided `Handler` implementation. This handler typically lives in the UI layer and calls into `gitbutler-stack` to refresh virtual branch metadata and `gitbutler-workspace` to recompute the workspace base. This reactive pattern ensures that GitButler's virtual branch model stays synchronized with the actual filesystem state without polling.

### Why is but-core separate from the gitbutler-* crates?

The `but-core` crate is deliberately isolated from the `gitbutler-*` naming convention and dependency tree to serve as a foundational layer for the entire system. It defines primitive data structures like `TreeChange`, `UnifiedPatch`, and `RefMetadata` that must be shared across all higher-level crates without introducing circular dependencies. By keeping `but-core` free of any `gitbutler-*` dependencies, the architecture ensures that fundamental types can be used anywhere in the codebase—including within the `gitbutler-*` crates themselves—while maintaining strict compile-time boundaries between functional domains.