GitoxideLabs/gitoxide · error

the HEAD pin must point to a local branch

Error message

the HEAD pin must point to a local branch

What it means

The remembered HEAD pin's target is not a symbolic reference to a local branch: either `try_name()` returned None (detached HEAD) or the name does not start with `refs/heads/`. Attaching time-travel state requires HEAD to be anchored to a local branch so it can be re-attached later. The library throws this when snapshotting/remembering the current branch.

Solutions

  1. Check out a local branch first (`git checkout -b <name>` to create one from the current commit).
  2. Ensure HEAD resolves to a `refs/heads/*` ref before starting the time-travel/attach flow.
  3. If on a remote-tracking state, create a local branch tracking it.

Example fix

// before (detached HEAD)
$ git checkout 1a2b3c4  # attach fails
// after
$ git checkout -b work  # local branch, attach succeeds
Defensive patterns

Strategy: validation

Validate before calling

let head = repo.head()?;
let is_local_branch = !head.is_detached()
    && head.referent_name().map(|n| n.as_bstr().starts_with(b"refs/heads/")).unwrap_or(false);
if !is_local_branch { return Err(anyhow!("must be on a local branch")); }

Type guard

fn on_local_branch(head: &gix::reference::Reference<'_>) -> bool {
    !head.is_detached()
        && head
            .referent_name()
            .map(|n| n.as_bstr().starts_with(b"refs/heads/"))
            .unwrap_or(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("must point to a local branch") => {
        eprintln!("detached HEAD; create/checkout a local branch first");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling remembered_branch (via attach_reporting) while HEAD is detached, or while HEAD points at something other than a local branch (e.g. `refs/remotes/origin/main`, a tag, or another pseudo-ref).

Common situations: Operating in a repo checked out at a bare commit (detached HEAD, e.g. after `git checkout <sha>` or CI checkouts); HEAD pointing to a remote-tracking branch; a pin captured on a tag.

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/957a5a935242d081. Report an issue: GitHub.

Appendix: source

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

        } else {
            notice = format!("{notice}; saved {}", pin_label(&provisional));
        }
    }
    Ok((Some(notice), ref_changes))
}

fn remembered_branch(repository: &gix::Repository) -> Result<RememberedBranch> {
    let pin = history::all_pins(repository)?
        .into_iter()
        .find(history::Pin::is_head)
        .context("attaching requires a valid HEAD pin")?;
    let branch = pin
        .target
        .try_name()
        .context("the HEAD pin must point to a local branch")?
        .to_owned();
    if !branch.as_bstr().starts_with(b"refs/heads/") {
        anyhow::bail!("the HEAD pin must point to a local branch");
    }
    Ok(RememberedBranch {
        branch,
        branch_tip: pin.id,
    })
}

fn validate_attach(repository: &gix::Repository, head_id: ObjectId, remembered: &RememberedBranch) -> Result<()> {
    let head = repository.head().context("could not read HEAD before attaching")?;
    if !head.is_detached() || head.id().map(gix::Id::detach) != Some(head_id) {
        anyhow::bail!("HEAD changed while preparing to attach");
    }
    drop(head);
    let pin = history::all_pins(repository)?
        .into_iter()
        .find(history::Pin::is_head)
        .context("the HEAD pin disappeared while preparing to attach")?;
    if pin.target.try_name() != Some(remembered.branch.as_ref()) || pin.id != remembered.branch_tip {

View on GitHub (pinned to e73179060b)