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

  1. Unstash the saved worktree state of one of the two stash commits before squashing them together
  2. Drop or manually merge the contents of one of the stash references, then re-run the rewrite
  3. 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

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


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)