gitbutlerapp/gitbutler · error

Operation not possible while HEAD is detached

Error message

Operation not possible while HEAD is detached

What it means

Thrown by integrate_upstream_with_hints when HEAD is detached such that the workspace is Ad-hoc and has no ref name. Upstream integration operates on a named workspace/stack ref, so with no branch checked out there is nothing to integrate into. The library refuses rather than guessing a ref.

Solutions

  1. Check out a branch first (git switch <branch>) before integrating upstream changes.
  2. In code, detect detached HEAD (workspace.kind == AdHoc && ref_name().is_none()) and fail fast with a clear user instruction.
  3. For CI, create a branch at the checked-out commit before invoking integration.

Example fix

# before (detached)
git checkout 1a2b3c4
but integrate  # fails

# after
git switch -c work-branch  # or: git switch main
but integrate
Defensive patterns

Strategy: validation

Validate before calling

fn can_integrate(workspace: &Workspace) -> bool {
    !(workspace.kind == but_graph::workspace::WorkspaceKind::AdHoc
        && workspace.ref_name().is_none())
}

Type guard

fn workspace_ref(ws: &Workspace) -> Option<&gix::refs::FullNameRef> {
    (ws.kind != WorkspaceKind::AdHoc).then_some(()).and(ws.ref_name())
}

Try / catch

if let Err(e) = integrate_upstream(...) {
    if e.to_string().contains("HEAD is detached") {
        eprintln!("checkout a branch before integrating");
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: Calling integrate_upstream (or its hint-based variant) while the repository is in detached-HEAD state (e.g. after checkout of a commit), producing WorkspaceKind::AdHoc with workspace.ref_name() == None.

Common situations: CI pipelines that check out a raw commit SHA; users who ran `git checkout <sha>` or a bisect; tools that create worktrees in detached state and then attempt stack integration.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at crates/but-workspace/src/upstream_integration.rs:187

///
/// With `single_branch_mode`, a managed workspace whose applied stacks were all integrated is
/// replaced by a checked-out canned branch at the target tip. Otherwise the emptied managed
/// workspace stays checked out, reparented onto the target.
#[allow(clippy::too_many_arguments)]
pub fn integrate_upstream_with_hints<'ws, 'meta, M: RefMetadata>(
    workspace: &'ws mut but_graph::Workspace,
    meta: &'meta mut M,
    project_meta: ProjectMeta,
    repo: &gix::Repository,
    db: &'meta mut but_db::DbHandle,
    updates: Vec<BottomUpdate>,
    review_hints: &[ReviewIntegrationHint],
    single_branch_mode: bool,
) -> Result<IntegrateUpstreamOutcome<'ws, 'meta, M>> {
    if matches!(workspace.kind, but_graph::workspace::WorkspaceKind::AdHoc)
        && workspace.ref_name().is_none()
    {
        bail!("Operation not possible while HEAD is detached");
    }

    let mut ws_meta = workspace.metadata.clone();
    let target_sha = project_meta
        .target_commit_id
        .context("Cannot update a workspace without a target sha")?;
    let target_ref = workspace
        .target_ref
        .clone()
        .context("Cannot update a workspace with no target ref")?;
    let target_ref_commit = repo.find_reference(&target_ref.ref_name)?.id();

    let entrypoint = workspace.graph.entrypoint()?;
    let head_commit = entrypoint
        .commit()
        .context("Cannot update workspace without head commit")?;
    let head_commit = repo.find_commit(head_commit.id)?;
    let head_commit_id = head_commit.id;

View on GitHub (pinned to 58e5313667)