gitbutlerapp/gitbutler · error

Cannot unapply a branch from an ad-hoc detached workspace

Error message

Cannot unapply a branch from an ad-hoc detached workspace

What it means

unapply needs a workspace reference name to update workspace metadata, but ws.ref_name() returned None — this is a detached, unnamed (ad-hoc) workspace with no ref to hang metadata or ref updates on. Unapply fundamentally requires a named workspace to record its effects, so it refuses.

Source

Thrown at crates/but-workspace/src/branch/unapply.rs:252

        let branch_in_ws = ws.find_segment_and_stack_by_refname(branch);
        if branch_in_ws.is_none() {
            if branch_ref.is_none() {
                bail!(
                    "Cannot unapply non-existing branch '{branch}'",
                    branch = branch.shorten()
                );
            }
            // The branch exists in Git, but does not in the workspace: Nothing to do.
            return Ok(Outcome::new(Cow::Borrowed(workspace)));
        }
        let branch_stack_was_entrypoint = branch_in_ws
            .is_some_and(|(stack, _)| stack.segments.iter().any(|segment| segment.is_entrypoint));
        let workspace_tip_was_entrypoint = ws.is_entrypoint();

        let Some(workspace_ref_name) = workspace_ref_name else {
            // This is an ad-hoc workspace by merit of being unnamed.
            bail!("Cannot unapply a branch from an ad-hoc detached workspace");
        };
        let mut ws_md = meta.workspace(workspace_ref_name.as_ref())?;
        if ws.kind.has_managed_ref() || ws.has_metadata() {
            ws.reconcile_metadata(&mut ws_md)?;
        }
        let branch_removed_from_ws_meta = ws_md.unapply_branch(branch);
        if !branch_removed_from_ws_meta {
            // The branch wasn't in workspace metadata, yet it was present, so also delete its branch metadata
            // as it could be used to disambiguate the segment.
            // TODO: this will actually be observable even if it doens't work, unless it's run in a transaction, which right now it's not!
            //       Should be able to redo the traversal with an overlay that hides branch metadata, but I'd say it's not important enough.
            meta.remove(branch)?;
            let graph = ws
                .graph
                .redo_traversal_with_overlay(repo, meta, Overlay::default())?;
            let workspace = graph.into_workspace()?;
            if workspace.refname_is_segment(branch) {
                bail!(

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Put the workspace on a named reference first (create/re-checkout the workspace ref), then retry.
  2. If HEAD is detached, attach it to a branch before doing workspace operations.
  3. For workspaces you want gone, delete the workspace ref and metadata explicitly rather than unapplying branches.

Example fix

// before
unapply(repo, meta, &ws, branch, opts)?;

// after
ensure!(ws.ref_name().is_some(), "unnamed/detached workspace: attach a workspace ref first");
unapply(repo, meta, &ws, branch, opts)?;
Defensive patterns

Strategy: validation

Validate before calling

if ws.ref_name().is_none() {
    // detached/unnamed workspace: attach a workspace ref before unapply
    return attach_workspace_ref_and_retry();
}

Type guard

fn workspace_is_named(ws: &but_graph::Workspace) -> bool {
    ws.ref_name().is_some()
}

Try / catch

match unapply(repo, meta, &ws, branch, opts) {
    Err(err) if err.to_string().contains("ad-hoc detached workspace") => {
        Err(anyhow!("attach HEAD to a branch / create the workspace ref first"))
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling unapply on a workspace whose kind is ad-hoc and which carries no reference name (anonymous entrypoint, e.g. HEAD detached or an unborn workspace ref).

Common situations: Repository opened while HEAD is detached; workspace ref (refs/heads/<workspace>) deleted outside GitButler; fresh repo with no commits where the workspace ref is unborn.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/cbed589739afb545. Report an issue: GitHub.