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

> Discover GitButler's real-time uncommitted file tracking powered by Redux streaming. Efficiently manage worktree changes, hunks, and selections with a normalized store.

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

---

**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`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/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`](https://github.com/gitbutlerapp/gitbutler/blob/main/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 (`checkHunk` → `lines: []`)
- 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 initialization** → `WorktreeService` 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

```typescript
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`](https://github.com/gitbutlerapp/gitbutler/blob/main/worktreeService.svelte.ts)).*

### Dispatching File Selection Actions

```typescript
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

```typescript
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

```typescript
// 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`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/worktree/worktreeService.svelte.ts) | RTK-Query endpoint that streams uncommitted changes from the backend. | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/apps/desktop/src/lib/worktree/worktreeService.svelte.ts) |
| [`apps/desktop/src/lib/selection/uncommitted.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/selection/uncommitted.ts) | Redux slice (`uncommittedSlice`) holding tree changes, hunk assignments, and user selections plus reducers and selectors. | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/apps/desktop/src/lib/selection/uncommitted.ts) |
| [`apps/desktop/src/lib/selection/uncommittedService.svelte.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/selection/uncommittedService.svelte.ts) | Thin wrapper that injects the slice's actions/selectors into Svelte components. | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/apps/desktop/src/lib/selection/uncommittedService.svelte.ts) |
| [`packages/ui/src/lib/utils/testIds.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/packages/ui/src/lib/utils/testIds.ts) | Test-ID constants for the Uncommitted Changes UI (`uncommitted-changes-file-list`). | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/packages/ui/src/lib/utils/testIds.ts) |
| [`apps/desktop/src/lib/dependencies/dependencies.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/dependencies/dependencies.ts) | Provides the `DependencyError` and `HunkDependencies` types used by the worktree payload. | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/apps/desktop/src/lib/dependencies/dependencies.ts) |
| [`apps/desktop/src/lib/hunks/hunk.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/hunks/hunk.ts) | Definition of `HunkAssignment`, `HunkHeader`, and error helpers. | [view](https://github.com/gitbutlerapp/gitbutler/blob/master/apps/desktop/src/lib/hunks/hunk.ts) |

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