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
- Create a pin covering HEAD: `tix pin HEAD` (or pin a descendant), then retry the travel
- Attach HEAD to a branch (`git switch <branch>` / checkout) so the position is retained by the ref
- 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
- Pin HEAD whenever working detached
- Attach to a branch before sideways/backward travel
- Travel only to descendants while detached
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
- the reword target or one of its descendants must be pinned
- exactly one time-travel destination is required
- {notice}
- time-travel would conflict; retry with…
- HEAD is not present in the default Tix view
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)