GitoxideLabs/gitoxide · error
stashes at and would converge on
Error message
stashes at {} and {} would converge on {} What it means
When rewriting history, tix re-points each stash reference from its old commit to the rewritten commit. This error fires when two different old stash references would both be re-pointed at the same new commit, which would make one reference clobber the other and lose saved worktree state. It reports the short hashes of both converging stash commits and the shared destination commit.
Solutions
- Unstash the saved worktree state of one of the two stash commits before squashing them together
- Drop or manually merge the contents of one of the stash references, then re-run the rewrite
- Reorder the rewrite so the two commits are not collapsed into one destination
Example fix
// before squashing two stash-owning commits plan.squash(a_id, b_id); // after: rescue one stash first stash::restore_manual(path, bare, b_id)?; plan.squash(a_id, b_id);
Defensive patterns
Strategy: try-catch
Validate before calling
let mut destinations = std::collections::HashSet::new();
for old in stash_owning_commits(&repo)? {
if let Some(new) = plan.rewritten().get(&old) {
if !destinations.insert(*new) {
anyhow::bail!("two stashes converge on {} after rewrite", new);
}
}
} Try / catch
match stash::rewrite_edits(&repo, plan) {
Err(e) if e.to_string().starts_with("stashes at ") => {
// two stash refs collapse; unstash one and retry
}
other => other?,
} Prevention
- Avoid squashing commits that each have saved tix worktree state
- Unstash before any interactive rebase that may squash or fix up stash-owning commits
- Check the rewrite plan's mapping for duplicate destinations before executing
When it happens
Trigger: Calling `stash::rewrite_edits` when the rewrite mapping `rewritten` sends two stash-owning commits to the same new commit ID (e.g. commits were squashed together), and both had stash references.
Common situations: Squashing or fixing-up commits during an interactive rebase when both squashed commits had saved tix worktree state; cherry-pick style rewrites collapsing distinct commits.
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
- cannot drop stashed commit
- is already saved
- Tree conflicted
- File conflicted
- Tree conflicted, refusing to write commit
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/729b11d5906fd589.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/stash.rs:81
Ok(Some(id)) => id,
Ok(None) => continue,
Err(err) => {
tracing::warn!(name = %reference.name(), error = %err, "ignored malformed tix stash reference");
continue;
}
};
let Some(new) = rewritten.get(&old).copied() else {
continue;
};
if removed.contains(&old) {
anyhow::bail!("cannot drop stashed commit {}", old.to_hex_with_len(7));
}
let new = new.context("a stashed commit cannot disappear during a rewrite")?;
if new == old {
continue;
}
if let Some(other) = destinations.insert(new, old) {
anyhow::bail!(
"stashes at {} and {} would converge on {}",
other.to_hex_with_len(7),
old.to_hex_with_len(7),
new.to_hex_with_len(7)
);
}
moves.push((reference.name().to_owned(), reference.target().into_owned(), old, new));
}
let mut forward = Vec::with_capacity(moves.len() * 2);
let mut rollback = Vec::with_capacity(moves.len() * 2);
for (old_name, target, old, new) in moves {
let new_name = reference(new)?;
if repo.try_find_reference(new_name.as_ref())?.is_some() {
anyhow::bail!(
"rewritten commit {} already has saved worktree state",
new.to_hex_with_len(7)
);View on GitHub (pinned to e73179060b)