GitoxideLabs/gitoxide · error

attaching requires detached HEAD

Error message

attaching requires detached HEAD

What it means

The attach operation can only start from a detached HEAD, but the repository's HEAD currently points at a branch. The library throws this in `attach_reporting` before capturing the head id. Attaching onto a branch checkout would overwrite branch state, so it is refused.

Solutions

  1. Detach HEAD first, e.g. `git checkout --detach` or `git checkout <commit>`, then retry attach.
  2. In code, verify `repository.head()?.is_detached()` before calling attach.
  3. Use the time-travel `perform` entry point instead if the intent is to switch states rather than attach to a remembered branch.

Example fix

// before
$ git attach  # HEAD on 'main'
// after
$ git checkout --detach  # or git checkout <sha>
$ git attach
Defensive patterns

Strategy: validation

Validate before calling

let repo = gix::open(repo_path)?;
if !repo.head()?.is_detached() {
    return Err(anyhow!("detach HEAD before attaching"));
}

Type guard

fn head_detached(repo: &gix::Repository) -> bool {
    repo.head().map(|h| h.is_detached()).unwrap_or(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("attaching requires detached HEAD") => {
        // run `git checkout --detach` and retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling attach (via attach_reporting) on a repo where `repository.head()` is not detached — i.e. HEAD is symbolic to `refs/heads/*` (or unborn on a branch).

Common situations: User forgot to detach (e.g. still on `main`); a prior step intended to detach HEAD failed silently; running attach in a fresh clone where a branch is checked out by default.

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 GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/80fe5adb230c6805. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/time_travel.rs:552

    bare: bool,
    revisions: &[OsString],
    include_worktrees: bool,
) -> Result<String> {
    Ok(attach_reporting(repository_path, bare, revisions, include_worktrees)?.0)
}

pub(crate) fn attach_reporting(
    repository_path: &Path,
    bare: bool,
    revisions: &[OsString],
    include_worktrees: bool,
) -> Result<(String, Vec<super::undo::RefChange>)> {
    let repository = open_repository(repository_path, bare, false)
        .context("could not open repository to attach the remembered branch")?;
    repository.workdir().context("attaching requires a worktree")?;
    let head = repository.head().context("could not read HEAD before attaching")?;
    if !head.is_detached() {
        anyhow::bail!("attaching requires detached HEAD");
    }
    let head_id = head
        .id()
        .map(gix::Id::detach)
        .context("attaching requires an existing HEAD commit")?;
    drop(head);
    let remembered = remembered_branch(&repository)?;
    validate_attach(&repository, head_id, &remembered)?;

    let pins = history::all_pins(&repository)?;
    let destination_pin = selected_pin(&pins, head_id);
    let mut ref_changes = Vec::new();
    let provisional = if remembered.branch_tip != head_id && !contains(&repository, remembered.branch_tip, head_id) {
        let (pin, created, mut changes) = create_or_reuse_pin_reporting(
            &repository,
            Target::Object(remembered.branch_tip),
            remembered.branch_tip,
            "tix attach departure",

View on GitHub (pinned to e73179060b)