gitbutlerapp/gitbutler · error

No changes to move

Error message

No changes to move

What it means

After constructing the tree of changes to move, if that tree equals the worktree's head tree, the move would relocate nothing (the selection covers everything but resolves to the same content, or the constructed destination tree has no delta). The library bails instead of performing a meaningless checkout.

Solutions

  1. Verify the selection actually captures a non-empty delta against the worktree head before moving
  2. Recompute the diff of the worktree; if it is empty, treat 'nothing to move' as a success no-op in the caller
  3. Adjust the selection so it represents real changes to relocate

Example fix

// before: assuming a move always changes something
move_uncommitted_changes(ctx, &wt, Some(selection), None)?;
// after: short-circuit no-op moves
let diff = worktree_repo.diff_tree_to_workdir_with_index(...)?;
if diff.is_empty() { return Ok(MoveOutcome::Noop); }
move_uncommitted_changes(ctx, &wt, Some(selection), None)?;
Defensive patterns

Strategy: validation

Validate before calling

let workdir_diff = worktree_repo.diff_tree_to_workdir_with_index(Some(&worktree_head_tree), None)?;
anyhow::ensure!(!workdir_diff.is_empty(), "worktree has no delta against head; nothing to move");

Try / catch

match move_uncommitted_changes(ctx, &wt, Some(selection), None) {
    Err(e) if e.to_string().contains("No changes to move") => Ok(MoveOutcome::Noop),
    r => r,
}

Prevention

When it happens

Trigger: move_uncommitted_changes where moved_tree == worktree_head_tree — the moved content is identical to the current worktree head, e.g. the selection math produced a tree with no actual differences, or a preceding context("No changes to move") on the destination tree was bypassed with identical trees.

Common situations: Selecting changes that only revert each other (edit + re-edit netting zero); moving with a selection whose resulting tree matches head because the destination already contains that content; automation passing a full-copy selection with no delta.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/66cddb83ceb5cbdf. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-workspace/src/worktrees.rs:212

                selection,
                context_lines,
            )?;
            let rejected: Vec<_> = outcome
                .rejected_specs
                .iter()
                .map(|(reason, spec)| format!("{reason:?}: {}", spec.path))
                .collect();
            if !rejected.is_empty() {
                bail!(
                    "Some selected changes no longer match the worktree's uncommitted changes: {}",
                    rejected.join(", ")
                );
            }
            outcome.destination_tree.context("No changes to move")?
        }
    };
    if moved_tree == worktree_head_tree {
        bail!("No changes to move");
    }

    let destination_outcome = but_core::worktree::safe_checkout_from_head(
        moved_tree,
        main_repo,
        but_core::worktree::checkout::Options {
            merge_base_override: Some(worktree_head_tree),
            allow_uncommitted_changes_to_conflict_with_new_head: true,
            ..Default::default()
        },
    )?;

    but_core::worktree::safe_checkout_from_head(
        worktree_head_tree,
        worktree_repo,
        but_core::worktree::checkout::Options {
            merge_base_override: Some(moved_tree),
            allow_uncommitted_changes_to_conflict_with_new_head: true,

View on GitHub (pinned to 58e5313667)