gitbutlerapp/gitbutler · warning

HEAD points to the GitButler workspace branch

Error message

HEAD points to the GitButler workspace branch

What it means

current_branch_targets resolves the repository's HEAD to a local branch target for the agentlog environment snapshot. GitButler manages a special 'workspace' ref that holds virtual-branch state; HEAD on that ref is not a real local branch the caller can report, so the function bails with this error. It is an intentional guard: the caller (workspace_snapshot_with_current_branch_fallback) uses the bail to decide the repo is in GitButler-managed mode rather than on a plain branch.

Solutions

  1. Detach from or switch to a normal branch (git switch <feature-branch>) so HEAD no longer points at the GitButler workspace ref
  2. Deactivate GitButler's workspace management for the repo (quit/unapply in the GitButler UI or `but` CLI) before snapshotting
  3. Treat this error in the caller as an expected signal that the repo is GitButler-managed and skip plain-branch snapshotting rather than surfacing it as a failure
  4. Check `git symbolic-ref HEAD` first and only run current_branch_targets when the ref is not the GitButler workspace ref

Example fix

// before
let targets = workspace_snapshot_with_current_branch_fallback(&repo)?;
// after
match workspace_snapshot_with_current_branch_fallback(&repo) {
    Ok(t) => t,
    Err(e) if e.to_string().contains("GitButler workspace branch") => {
        // repo is GitButler-managed; skip plain-branch snapshot
        ObservedTargets::default()
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: fallback

Validate before calling

let head_ref = repo.head()?.referent_name().map(|r| r.to_string());
let is_gitbutler_workspace = head_ref
    .as_deref()
    .map(is_workspace_ref_name)
    .unwrap_or(false);
if is_gitbutler_workspace { /* skip plain-branch snapshot path */ }

Type guard

fn is_plain_local_branch(repo: &gix::Repository) -> Option<String> {
    let name = repo.head().ok()?.referent_name()?.to_string();
    (name.starts_with("refs/headers/") || name.starts_with("refs/heads/"))
        .then_some(name)
        .filter(|n| !is_workspace_ref_name_short(n))
}

Try / catch

match current_branch_targets(repo) {
    Ok(targets) => targets,
    Err(e) if e.to_string().contains("GitButler workspace branch") => {
        // expected: GitButler-managed repo; use workspace snapshot path
        ObservedTargets::default()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling workspace_snapshot_with_current_branch_fallback (via current_branch_targets) on a repository where HEAD points to refs/heads/<gitbutler workspace branch> (is_workspace_ref_name returns true) — i.e. the GitButler workspace/apply flow has checked out its managed ref.

Common situations: Developer has GitButler installed and its workspace is active in the repo; running agentlog snapshotting while GitButler holds HEAD; mixing plain-git tooling with GitButler-managed repos; GitButler left the workspace ref checked out after an interrupted session.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at crates/but-agentlog/src/environment.rs:392

                observed_targets: fallback,
                error_kind: Some(SnapshotErrorKind::Workspace),
            }),
            _ => Err(error),
        },
    }
}

fn current_branch_targets(repo: &gix::Repository) -> anyhow::Result<ObservedTargets> {
    let head = repo.head()?;
    let Some(ref_name) = head.referent_name() else {
        anyhow::bail!("HEAD is detached");
    };
    let full_ref_name = ref_name.to_string();
    if !full_ref_name.starts_with("refs/heads/") {
        anyhow::bail!("HEAD does not point to a local branch");
    }
    if is_workspace_ref_name(ref_name) {
        anyhow::bail!("HEAD points to the GitButler workspace branch");
    }
    let branch = BranchTarget {
        key: format!("ref:{full_ref_name}"),
        name: ref_name.shorten().to_string(),
    };
    // Derive the PR association from the forge review cache rather than stored
    // metadata. Synthesize a metadata entry carrying the resolved number so a
    // branch with a cached PR is reported even without stored branch metadata.
    let reviews = match resolve_branch_pr(repo, ref_name, &forge_prs_by_head(repo)) {
        Some(pull_request) => {
            let mut metadata = Branch::default();
            metadata.review.pull_request = Some(pull_request);
            review_targets(&branch.key, Some(&metadata))
        }
        None => Vec::new(),
    };
    let mut observed_targets = ObservedTargets {
        branches: vec![branch],

View on GitHub (pinned to 58e5313667)