gitbutlerapp/gitbutler · error · but_error::Code

BranchNotFound

BranchNotFound

Error message

branch with ID {stack_id} not found

What it means

While applying BranchUpdateRequests (e.g. stack reordering), every update must reference a stack that exists in the current workspace metadata and is_in_workspace() (applied). A request carrying the id of a deleted, unapplied, or unknown stack fails with Code::BranchNotFound before any order change is written.

Source

Thrown at crates/but-api/src/legacy/virtual_branches.rs:311

    }

    Ok(())
}

fn apply_stack_order_updates(
    workspace: &mut but_core::ref_metadata::Workspace,
    updates: Vec<BranchUpdateRequest>,
) -> Result<bool> {
    let mut requested_orders = HashMap::new();

    for update in updates {
        let stack_id = update.id.context("BUG(opt-stack-id)")?;
        if !workspace
            .stacks
            .iter()
            .any(|stack| stack.id == stack_id && stack.is_in_workspace())
        {
            return Err(anyhow!("branch with ID {stack_id} not found")
                .context(but_error::Code::BranchNotFound));
        }

        if let Some(order) = update.order {
            requested_orders.insert(stack_id, order);
        }
    }

    if requested_orders.is_empty() {
        return Ok(false);
    }

    let original_stack_ids = workspace
        .stacks
        .iter()
        .map(|stack| stack.id)
        .collect::<Vec<_>>();
    let original_orders = original_stack_ids

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Refetch the current stacks and resend the update with live ids
  2. Re-apply the stack if it should still be orderable
  3. Filter out ids of stacks no longer in the workspace before sending the batch

Example fix

// before: batch built from a stale snapshot
update_stack_orders(ctx, stale_updates)?;

// after: intersect with live workspace stacks
let live: HashSet<StackId> = workspace_stacks(ctx)?.iter().filter_map(|s| s.id).collect();
let updates: Vec<_> = stale_updates.into_iter()
    .filter(|u| u.id.map(|id| live.contains(&id)).unwrap_or(false))
    .collect();
if !updates.is_empty() { update_stack_orders(ctx, updates)?; }
Defensive patterns

Strategy: validation

Validate before calling

let ws = current_workspace(ctx)?;
let live: HashSet<_> = ws.stacks.iter()
    .filter(|s| s.is_in_workspace())
    .filter_map(|s| s.id)
    .collect();
let updates: Vec<_> = requested.into_iter()
    .filter(|u| u.id.is_some_and(|id| live.contains(&id)))
    .collect();
if updates.is_empty() { return Ok(false); }

Type guard

use but_error::{AnyhowContextExt, Code};

fn is_branch_not_found(err: &anyhow::Error) -> bool {
    err.custom_context().is_some_and(|c| c.code == Code::BranchNotFound)
}

Try / catch

match update_stack_orders(ctx, updates) {
    Err(err) if is_branch_not_found(&err) => {
        // refetch stacks and retry with refreshed ids; drop deleted ones
    }
    other => other,
}

Prevention

When it happens

Trigger: Sending stack order updates containing an id that was unapplied or deleted after the client fetched the list; two clients racing where one deletes a stack while the other reorders; SDK scripts reusing cached ids.

Common situations: Drag-to-reorder racing an unapply; frontend state holding stale stack ids; batch updates built from an old workspace snapshot.

Related errors


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