GitoxideLabs/gitoxide · error
the reword target or one of its descendants must be pinned
Error message
the reword target or one of its descendants must be pinned
What it means
Rewording a commit that is not reachable from HEAD and not covered by any pin would leave the old commit garbage-collectable with no way back. ensure_retained_target requires that the reword target (or a pinned descendant of it) is retained — pinned — unless HEAD itself is attached to the history containing it. This protects against rewriting commits that nothing references.
Solutions
- Pin the target (or a descendant) with `tix pin` before rewording
- Re-attach HEAD to the branch containing the target, then reword
- Verify pins with `tix history` / pin listing to see why retention check fails
Example fix
// before tix reword <oid> -m "new message" # detached HEAD, no pins // after tix pin <oid> tix reword <oid> -m "new message"
Defensive patterns
Strategy: validation
Validate before calling
// Before rewording, ensure the target is retained
let pins = crate::history::all_pins(&repo)?;
let attached = repo.head_detached() == Ok(false);
if !attached && !pins.iter().any(|p| graph.is_ancestor(target, p.id)) {
// create a pin first
} Try / catch
if let Err(e) = tix_reword(target, msg) {
if e.to_string().contains("must be pinned") {
tix_pin(target)?;
tix_reword(target, msg)?;
} else { return Err(e); }
} Prevention
- Pin targets before rewriting them when detached
- Avoid rewording commits on deleted/rewritten branches
- Regularly review and refresh pins
When it happens
Trigger: Calling `tix reword` for a target OID while in detached HEAD state and no pin in `tix history` pins covers the target via is_ancestor.
Common situations: Rewording an old commit on a detached HEAD; pins expired or never created; editing a commit from a branch that was since deleted or rewritten.
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
- detached HEAD or one of its descendants must be pinned…
- Cannot run without any task to perform on the repositories
- interrupted by user
- At least one operation failed
- Cancelled by user
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/2b6b6a7f15487743.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/command/reword.rs:114
let head = repository.head().context("could not read HEAD after editing commit")?;
let attached_head = !head.is_detached() && head.id().map(gix::Id::detach) == Some(target);
drop(head);
ensure_retained_target(&graph, target, &pins, attached_head)?;
let output_repository = repository.clone();
finish_editor(
&output_repository,
crate::edit::reword::apply(repository, &graph, target, &edited)?,
)
}
fn ensure_retained_target(
graph: &crate::history::HistoryGraph,
target: gix::ObjectId,
pins: &[crate::history::Pin],
attached_head: bool,
) -> Result<()> {
if !attached_head && !pins.iter().any(|pin| graph.is_ancestor(target, pin.id)) {
anyhow::bail!("the reword target or one of its descendants must be pinned");
}
Ok(())
}
pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Result<Option<Vec<u8>>> {
if !args.message.is_empty() {
let mut out = Vec::new();
for (index, message) in args.message.iter().enumerate() {
if index > 0 {
out.extend_from_slice(b"\n\n");
}
out.extend_from_slice(
gix::path::os_str_into_bstr(message)
.with_context(|| format!("message {} is not valid UTF-8", index + 1))?,
);
}
return Ok(Some(out));
}View on GitHub (pinned to e73179060b)