GitoxideLabs/gitoxide · error
changes can only be stashed at the current HEAD
Error message
changes can only be stashed at the current HEAD
What it means
Manual stash saving keys the saved worktree state by the current HEAD commit ID. This error fires when the caller supplied a commit ID that does not equal the repository's current HEAD id, so tix cannot attribute the state to HEAD. It guards against stashing changes that belong to a different, unborn, or non-checked-out commit.
Solutions
- Pass the commit id obtained from the repository's current `head_id()` at the time of the call
- Re-checkout or reset HEAD so it matches the id you intend to stash at
- Re-read HEAD and retry the save atomically if HEAD may have moved
Example fix
// before
let target = old_commit_id;
stash::save_manual(path, bare, workdir, target)?;
// after
let repo = gix::open(path)?;
let target = repo.head_id().expect("born HEAD").detach();
stash::save_manual(path, bare, workdir, target)?; Defensive patterns
Strategy: validation
Validate before calling
let repo = gix::open(path)?;
let head = repo.head_id().context("born HEAD required")?.detach();
if head != id {
anyhow::bail!("re-read HEAD; the id to stash must equal the current HEAD");
} Prevention
- Always obtain the id to stash from `repo.head_id()` immediately before the call
- Re-open or refresh the repository handle if other processes may have moved HEAD
- Never pass historical commit IDs to save_manual
When it happens
Trigger: Calling `stash::save_manual` with an `id` argument different from `repo.head_id()` — e.g. after HEAD moved (checkout/rebase) between obtaining the id and saving, or passing an arbitrary commit id.
Common situations: Scripting stashes while HEAD changes concurrently; passing a target commit id from a UI that is not the checked-out HEAD; detached HEAD transitions mid-operation.
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
- changes can only be unstashed at the current HEAD
- cannot split with unresolved conflicts
- splitting requires both staged and worktree changes
- tix stash reference does not use a canonical full commit ID
- cannot drop stashed commit
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/9ef70d453cc78f5e.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/stash.rs:130
}
fn delete_edit(name: gix::refs::FullName, target: Target) -> RefEdit {
RefEdit::delete(name, PreviousValue::MustExistAndMatch(target))
}
#[tracing::instrument(skip_all, fields(commit_id = %id))]
pub(crate) fn save_manual(repository_path: &Path, bare: bool, id: ObjectId) -> Result<String> {
let repo = open_repository(repository_path, bare, false).context("could not open repository to stash changes")?;
let workdir = repo
.workdir()
.context("stashing changes requires a worktree")?
.to_owned();
let head = repo
.head_id()
.context("stashing changes requires a born HEAD")?
.detach();
if head != id {
anyhow::bail!("changes can only be stashed at the current HEAD");
}
if repo
.index_or_empty()
.context("could not inspect the index before stashing")?
.entries()
.iter()
.any(|entry| entry.stage() != gix::index::entry::Stage::Unconflicted)
{
anyhow::bail!("cannot stash changes with unresolved index conflicts");
}
let name = reference(id)?;
if repo.try_find_reference(name.as_ref())?.is_some() {
anyhow::bail!("{} already has saved worktree state", id.to_hex_with_len(7));
}
drop(repo);
if !super::review::is_dirty(&workdir)? {
anyhow::bail!("there are no worktree or index changes to stash");
}View on GitHub (pinned to e73179060b)