GitoxideLabs/gitoxide · warning

the stack is already directly above the move target

Error message

the stack is already directly above the move target

What it means

The stack is moved to sit directly on top of `target`. If `base`'s current parent is already `target`, the stack already occupies exactly that position and the move is a no-op; the plan builder bails instead of producing an empty rewrite.

Solutions

  1. Skip the operation if the stack is already in the requested position.
  2. Choose a different target commit.
  3. Make the operation idempotent in caller code by checking `parent_of(base) == target` before invoking.

Example fix

// before
stack_insert_plan(&repo, &graph, base, head, target)?

// after
if graph.parents_of(base)?[0] != target {
    stack_insert_plan(&repo, &graph, base, head, target)?;
}
Defensive patterns

Strategy: validation

Validate before calling

if graph.parents_of(base)?[0] == target {
    eprintln!("stack is already directly above target; nothing to do");
    return Ok(());
}

Type guard

fn move_changes_position(graph: &HistoryGraph, base: ObjectId, target: ObjectId) -> bool {
    graph.parents_of(base).map(|p| p.first() != Some(&target)).unwrap_or(false)
}

Try / catch

match stack_insert_plan(&repo, &graph, base, head, target) {
    Err(e) if e.to_string().contains("already directly above") => { /* treat as success/no-op */ }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `move_insert_plan`/`stack_insert_plan(repo, graph, base, head, target)` where the parent of `base` equals `target` — i.e. repeating an already-applied move, or passing the natural current parent as target.

Common situations: Re-running the same move command twice (e.g. after a retried script); user selecting the commit the stack already sits on; automation replaying recorded operations.

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/3b4ef31dc84dbfe2. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/rebase.rs:680

        let id = *stack.last().expect("a stack always contains HEAD");
        let parents = graph.parents_of(id).context("a moved stack commit is incomplete")?;
        let [parent] = parents.as_slice() else {
            anyhow::bail!("moving a stack requires every commit to have exactly one parent");
        };
        stack_parent.insert(id, *parent);
        if id == base {
            break;
        }
        stack.push(*parent);
    }
    stack.reverse();
    let stack_set: HashSet<_> = stack.iter().copied().collect();
    if stack_set.contains(&target) {
        anyhow::bail!("the move target must not be part of the moved stack");
    }
    let base_parent = stack_parent[&base];
    if base_parent == target {
        anyhow::bail!("the stack is already directly above the move target");
    }

    let target_rewritten = graph.is_ancestor(base, target);
    let mut scope = Vec::new();
    let mut scope_set = HashSet::new();
    for id in graph
        .descendants_in_parent_order(base)
        .context("the stack base is not in the loaded history")?
        .into_iter()
        .chain(
            graph
                .descendants_in_parent_order(target)
                .context("the move target is not in the loaded history")?,
        )
    {
        if (id != target || target_rewritten) && scope_set.insert(id) {
            scope.push(id);
        }

View on GitHub (pinned to e73179060b)