GitButler Uncommitted File Tracking Mechanism: Real-Time Redux Streaming Architecture

GitButler tracks uncommitted files by streaming worktree changes from a backend service into a normalized Redux store that maintains tree changes, hunk assignments, and user selections while preserving partial line selections across updates.

The GitButler uncommitted file tracking mechanism provides developers with a live, interactive view of their working directory changes. Implemented in the gitbutlerapp/gitbutler repository, this system combines a Rust-powered backend event stream with a sophisticated frontend architecture using RTK-Query and Redux entity adapters. The result is a responsive interface that displays file diffs, tracks hunk assignments, and maintains user selections even as the underlying worktree changes in real time.

Backend Source of Truth with WorktreeService

The tracking system begins with the WorktreeService in apps/desktop/src/lib/worktree/worktreeService.svelte.ts, which creates an RTK-Query endpoint named worktreeChanges. This endpoint opens a subscription to the backend event stream project://<id>/worktree_changes【L86-L112】.

When the backend pushes a WorktreeChanges payload, the service transforms the raw data into normalized entity adapters:

  • changes → stored in worktreeAdapter (entities of type TreeChange) containing one entry per file path for raw diff lookups
  • assignments → stored in hunkAssignmentAdapter (entities of type HunkAssignment) describing each hunk's line ranges that can be selected
  • dependencies and errors → stored unchanged for error UI rendering

The transformed result is cached and automatically invalidates the Diff tag, ensuring any UI component reading the diff list refreshes automatically【L119-L132】.

Normalizing Data in the Client Redux Slice

The normalized data flows into the uncommitted Redux slice defined in apps/desktop/src/lib/selection/uncommitted.ts【L41-L48】. This slice maintains three distinct entity collections using Redux Toolkit's entity adapters:

Collection Adapter Purpose
treeChanges treeChangeAdapter Raw diff for each file (path → TreeChange)
hunkAssignments hunkAssignmentAdapter Line ranges for every file/hunk that can be selected (lineNumsAdded/Removed)
hunkSelection hunkSelectionAdapter User's current selection state; empty lines array indicates the whole hunk is selected

Preserving Selections Across Backend Updates

When the backend pushes a new payload, the slice's update reducer (uncommittedSlice.reducers.update) calls the helper updateAssignments【L59-L62】. This function performs a three-way merge:

  1. Replaces treeChanges and hunkAssignments entirely with the new backend data
  2. Walks the previous hunkSelection entries, matching them to new assignments by stable ID or composite key, and preserves line selections where possible using the updateLines helper
  3. Removes any selections that no longer have a matching assignment

This ensures the UI never loses a user's partial line selections between worktree refreshes, even as files change on disk.

UI-Level State and Selectors

Selectors under uncommittedSelectors expose the slice in a UI-friendly format:

  • treeChanges.selectByPath – fetches a file’s diff by path
  • hunkSelection.fileCheckStatus, folderCheckStatus, stackCheckStatus – compute the tri-state (checked, indeterminate, unchecked) for files, folders, or entire stacks based on underlying hunk selections【L99-L124】
  • hunkCheckStatus – returns whether a specific hunk is selected and which lines are selected

These selectors drive the Uncommitted Changes panel (referenced by test-IDs uncommitted-changes-file-list and file-list-item in packages/ui/src/lib/utils/testIds.ts【L40-L42】) and enable bulk actions such as "check all files in a stack."

Actions That Mutate the Selection

The slice exports uncommittedActions including checkFile, uncheckFile, checkHunk, uncheckLine, checkStack, and others【L200-L559】. When a user clicks a checkbox, the corresponding action updates hunkSelection via entity adapters. The reducers contain logic to:

  • Add a whole-hunk selection (checkHunklines: [])
  • Add a single line (checkLine → appends the line, merges to full-hunk when all lines become selected)
  • Remove selections and collapse empty line arrays back to "no selection"

End-to-End Data Flow

The complete GitButler uncommitted file tracking mechanism operates as follows:

  1. App initializationWorktreeService creates the worktreeChanges query subscription
  2. Backend emits a worktree_changes event → RTK-Query cache updates with normalized entities
  3. Redux update → The uncommitted slice receives the payload via its update reducer, preserving existing UI selections
  4. UI rendering → Components read the slice through selectors, render the file list, and compute checkbox tri-state
  5. User interaction → Dispatches uncommittedActions; reducers mutate hunkSelection
  6. Commit preparation → Selected hunks are extracted from hunkSelection and sent to the backend for committing

Code Examples

Subscribing to the Worktree Changes Stream

import { WORKTREE_SERVICE } from '$lib/worktree/worktreeService.svelte';
import type { ClientState } from '$lib/state/clientState.svelte';

function initWorktree(state: ClientState) {
  const service = new WorktreeService(state);
  // Subscribe to the RTK-Query endpoint – data stays live while mounted
  const { data, isLoading } = service.worktreeData('my-project-id');
  $: if (data) {
    console.log('Uncommitted files:', data.rawChanges);
  }
}

The subscription is established in the worktreeChanges endpoint (lines 86-112 of worktreeService.svelte.ts).

Dispatching File Selection Actions

import { uncommittedActions } from '$lib/selection/uncommitted';
import { store } from '$lib/state/store';

// When the user clicks the file-level checkbox:
function onFileCheck(stackId: string | null, path: string) {
  store.dispatch(uncommittedActions.checkFile({ stackId, path }));
}

The reducer adds a selection entry for every hunk belonging to the file (lines = [] indicates the whole hunk is selected) (lines 200-218).

Reading File Check Status

import { uncommittedSelectors } from '$lib/selection/uncommitted';
import type { RootState } from '$lib/state/store';

function fileStatus(state: RootState, stackId: string | null, path: string) {
  return uncommittedSelectors.hunkSelection.fileCheckStatus(state, { stackId, path });
}

The selector returns 'checked' | 'indeterminate' | 'unchecked' based on the current hunkSelection entries (lines 99-124).

Handling Backend Updates

// Internally triggered by RTK-Query cache update:
store.dispatch(uncommittedActions.update({ assignments, changes }));

The updateAssignments helper walks old selections and maps them to fresh assignments, preserving line selections when possible (lines 29-50).

Key Files

File Role Link
apps/desktop/src/lib/worktree/worktreeService.svelte.ts RTK-Query endpoint that streams uncommitted changes from the backend. view
apps/desktop/src/lib/selection/uncommitted.ts Redux slice (uncommittedSlice) holding tree changes, hunk assignments, and user selections plus reducers and selectors. view
apps/desktop/src/lib/selection/uncommittedService.svelte.ts Thin wrapper that injects the slice's actions/selectors into Svelte components. view
packages/ui/src/lib/utils/testIds.ts Test-ID constants for the Uncommitted Changes UI (uncommitted-changes-file-list). view
apps/desktop/src/lib/dependencies/dependencies.ts Provides the DependencyError and HunkDependencies types used by the worktree payload. view
apps/desktop/src/lib/hunks/hunk.ts Definition of HunkAssignment, HunkHeader, and error helpers. view

These files together implement GitButler's uncommitted file tracking, turning a live backend stream into a consistent, selectable UI state.

Summary

  • GitButler uncommitted file tracking mechanism relies on a streaming architecture where the backend pushes worktree changes via the project://<id>/worktree_changes event stream.
  • The WorktreeService normalizes incoming payloads into entity adapters for TreeChange and HunkAssignment, invalidating the Diff tag to trigger UI refreshes.
  • The uncommitted Redux slice maintains three entity collections: treeChanges for file diffs, hunkAssignments for selectable line ranges, and hunkSelection for user choices.
  • The updateAssignments helper preserves partial line selections when the backend refreshes data, ensuring the UI state survives file modifications.
  • Selectors like fileCheckStatus and hunkCheckStatus compute tri-state checkboxes (checked, indeterminate, unchecked) for the Uncommitted Changes panel.

Frequently Asked Questions

How does GitButler keep the uncommitted changes list synchronized with the filesystem?

GitButler maintains synchronization through a persistent event stream. The WorktreeService subscribes to the backend endpoint project://<id>/worktree_changes, which pushes updates whenever the working directory changes. These updates flow through RTK-Query into the Redux store, where entity adapters normalize the data and automatically invalidate cached diffs to refresh the UI.

What happens to my hunk selections when the file changes on disk?

The system preserves your selections through the updateAssignments helper function in apps/desktop/src/lib/selection/uncommitted.ts. When new data arrives from the backend, this helper walks your existing hunkSelection entries and maps them to the fresh assignments using stable IDs or composite keys. It preserves individual line selections where possible and removes only those selections that no longer have matching assignments.

How does the UI determine whether a file checkbox is checked, unchecked, or indeterminate?

The uncommittedSelectors module provides specialized selectors like fileCheckStatus, folderCheckStatus, and stackCheckStatus that compute the tri-state value. These selectors examine the underlying hunkSelection entities: if all hunks in a file are selected, the status is checked; if some but not all are selected, it returns indeterminate; if none are selected, it returns unchecked.

Where does the uncommitted changes data originate in the backend?

The data originates from GitButler's Rust backend, which monitors the working directory for changes. The frontend receives this data through the worktree_changes event stream as WorktreeChanges payloads. These payloads contain the raw changes (file diffs), assignments (hunk metadata), and any dependency errors, which the frontend then normalizes into the Redux store.

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 →