gitbutlerapp/gitbutler · error

No uncommitted changes found for the selected files

Error message

No uncommitted changes found for the selected files

What it means

Thrown by absorption_plan()/absorption_plan_with_perm() when AbsorptionTarget::TreeChanges is used: the selected files' paths (matched byte-exact via path_bytes) must have at least one worktree change assignment that is either unassigned or assigned to assigned_stack_id. An empty intersection aborts planning before any commit is touched.

Source

Thrown at crates/but-api/src/legacy/absorb.rs:195

                true,
                perm.read_permission(),
            )?;
            let all_assignments = worktree_changes.assignments;
            let dependencies = worktree_changes.dependencies;

            // Include hunks that are unassigned or assigned to the acting stack,
            // so that dependency locks can route unassigned hunks correctly.
            let candidates: Vec<AbsorbCandidate> = all_assignments
                .into_iter()
                .filter(|a| {
                    changes.iter().any(|c| c.path_bytes == a.path_bytes)
                        && (a.stack_id.is_none() || a.stack_id == assigned_stack_id)
                })
                .map(Into::into)
                .collect();

            if candidates.is_empty() {
                anyhow::bail!("No uncommitted changes found for the selected files");
            }

            (candidates, dependencies)
        }
        AbsorptionTarget::Hunks { hunks } => {
            // Compute hunk dependencies only for this target since changes_in_worktree isn't called
            let (repo, ws, _db) = ctx.workspace_and_db_with_perm(perm.read_permission())?;
            let dependencies =
                hunk_dependencies_for_workspace_changes_by_worktree_dir(&repo, &ws, None).ok();
            drop((repo, ws, _db));
            (hunks.into_iter().map(Into::into).collect(), dependencies)
        }
        AbsorptionTarget::All => {
            // Get all worktree changes, assignments, and dependencies
            // TODO: Ideally, there's a simpler way of getting the worktree changes without passing the context to it.
            // At this time, the context is passed pretty deep into the function.
            let worktree_changes = crate::diff::changes_in_worktree_with_perm(
                ctx,

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Re-run changes_in_worktree and build the changes list from its current output for the same files
  2. Pass the assigned_stack_id of the stack the hunks are actually assigned to, or unassign the hunks first
  3. Make sure the paths are repo-relative and byte-identical to the diff output (no absolute prefixes or different separators)
  4. If the changes were already absorbed or committed, skip the call

Example fix

// before: paths captured from an older diff view
let plan = absorption_plan(ctx, AbsorptionTarget::TreeChanges { changes: stale_changes, assigned_stack_id })?;

// after: rebuild the selection from the live worktree diff
let wt = changes_in_worktree_with_perm(ctx, ChangesSource::Head, true, perm.read_permission())?;
let changes = wt.assignments.iter()
    .filter(|a| selected_files.iter().any(|f| f.as_bytes() == &a.path_bytes[..]))
    .map(|a| change_for(&a.path_bytes))
    .collect::<Vec<_>>();
if changes.is_empty() { return Ok(vec![]); } // nothing to absorb: no-op instead of error
let plan = absorption_plan_with_perm(ctx, AbsorptionTarget::TreeChanges { changes, assigned_stack_id }, perm)?;
Defensive patterns

Strategy: validation

Validate before calling

let wt = changes_in_worktree_with_perm(ctx, ChangesSource::Head, true, perm.read_permission())?;
let selectable = wt.assignments.iter().any(|a|
    selected.iter().any(|p| p.as_bytes() == &a.path_bytes[..])
        && (a.stack_id.is_none() || a.stack_id == Some(assigned_stack_id)));
if !selectable {
    // refresh the selection from wt; do not call absorption_plan yet
}

Try / catch

match absorption_plan(ctx, target) {
    Ok(plan) => { /* proceed */ }
    Err(err) if err.to_string().contains("No uncommitted changes found for the selected files") => {
        // stale selection: re-fetch the diff, rebuild the target, or no-op
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Calling absorption_plan with AbsorptionTarget::TreeChanges where every hunk of the selected files is assigned to a stack other than assigned_stack_id; the selected files no longer have uncommitted worktree changes; or the supplied path bytes are not exactly equal to the assignment paths (case, separator, or encoding differences).

Common situations: An 'absorb selected files' action invoked after the user already assigned those hunks to another branch; a stale file list captured before the hunks were committed or absorbed; renamed or moved files; the worktree changed between listing the diff and calling absorb.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/8beead263627b00b. Report an issue: GitHub.