GitoxideLabs/gitoxide · error

detached HEAD or one of its descendants must be pinned…

Error message

detached HEAD or one of its descendants must be pinned before travelling into the past or sideways

What it means

Travelling into the past or sideways from a detached HEAD rewrites what HEAD points at; without a pin covering detached HEAD or one of its descendants, the previous position could become unreachable. run() refuses the move unless the source is pinned (head_id is an ancestor of some pin).

Solutions

  1. Create a pin covering HEAD: `tix pin HEAD` (or pin a descendant), then retry the travel
  2. Attach HEAD to a branch (`git switch <branch>` / checkout) so the position is retained by the ref
  3. Travel only to descendants while detached, which is always allowed

Example fix

// before
tix travel HEAD~3        # detached, unpinned
// after
tix pin HEAD
tix travel HEAD~3
Defensive patterns

Strategy: validation

Validate before calling

let detached = repo.head_detached()?;
let forward = graph.is_ancestor(head_id, selected);
if detached && !forward {
    let pinned = crate::history::all_pins(&repo)?.iter().any(|p| graph.is_ancestor(head_id, p.id));
    if !pinned { /* pin before travelling */ }
}

Try / catch

if let Err(e) = travel(dest) {
    if e.to_string().contains("must be pinned") {
        pin_head()?;
        travel(dest)?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: Detached HEAD state + travel to a non-descendant destination (forward == false) while no pin in all_pins() has head_id as an ancestor.

Common situations: Browsing history in detached state then jumping to an older commit; pins never created in a fresh clone; sideways moves between branches while detached.

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/125897bdcfce5238. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/command/travel.rs:85

        _ => anyhow::bail!("exactly one time-travel destination is required"),
    };
    if selected == head_id {
        println!("already at {}", crate::change_id::display(&repository, selected, 7)?);
        return Ok(());
    }

    let revisions = vec![OsString::from("HEAD"), OsString::from(selected.to_string())];
    let graph = match resolved_graph {
        Some(graph) => graph,
        None => crate::edit::loaded_view_graph_with(&repository, &revisions)?,
    };
    let forward = graph.is_ancestor(head_id, selected);
    if detached && !forward {
        let source_is_pinned = crate::history::all_pins(&repository)?
            .into_iter()
            .any(|pin| graph.is_ancestor(head_id, pin.id));
        if !source_is_pinned {
            anyhow::bail!(
                "detached HEAD or one of its descendants must be pinned before travelling into the past or sideways"
            );
        }
    }

    let reviews = crate::history::all_reviews(&repository)?
        .into_iter()
        .map(|review| review.id)
        .collect::<Vec<_>>();
    let repository_path = repository.git_dir().to_owned();
    let bare = repository.is_bare();
    drop(repository);
    match crate::edit::time_travel::perform(&repository_path, bare, selected, &graph, &reviews, &[], false)? {
        crate::edit::time_travel::Perform::Complete {
            notice,
            selected,
            ref_rewrites,
            ref_changes,

View on GitHub (pinned to e73179060b)