GitoxideLabs/gitoxide · error

the move source must be the current HEAD

Error message

the move source must be the current HEAD

What it means

The move/stack-insert operation relocates the stack of commits ending at HEAD, so its notion of 'the source' is whatever HEAD currently points to. The plan is computed against the on-disk repository state; if the caller-provided `head` id does not match `repo.head_id()`, the operation would move the wrong commits, so it bails immediately.

Solutions

  1. Re-read the current HEAD id and pass it (or derive `base`/`head` from it) right before the move.
  2. Refresh the loaded history graph and retry the operation with up-to-date ids.
  3. Ensure no other process or in-session operation moves HEAD between snapshot and plan.

Example fix

// before
let head = cached_head_id;
stack_insert_plan(&repo, &graph, base, head, target)?

// after
let head = repo.head_id()?.detach();
stack_insert_plan(&repo, &graph, base, head, target)?
Defensive patterns

Strategy: validation

Validate before calling

let current_head = repo.head_id()?.detach();
if current_head != head {
    eprintln!("HEAD moved; re-read repository state before moving");
    return Ok(());
}

Type guard

fn head_is_current(repo: &gix::Repository, head: ObjectId) -> bool {
    repo.head_id().map(|id| id.detach() == head).unwrap_or(false)
}

Try / catch

match stack_insert_plan(&repo, &graph, base, head, target) {
    Err(e) if e.to_string().contains("current HEAD") => {
        let head = repo.head_id()?.detach();
        stack_insert_plan(&repo, &graph, base, head, target)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `move_insert_plan`/`stack_insert_plan(repo, graph, base, head, target)` where `head` is stale: HEAD moved (new commit, checkout, reset, rebase) between the caller reading it and invoking the operation, or the caller passes an unrelated commit id.

Common situations: Long-running UI sessions (like tix) where the repository changed while the plan request was built; scripts that cached a HEAD id earlier in execution; concurrent modification by another git process.

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

Appendix: source

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

pub(crate) fn move_insert_plan(
    repo: &gix::Repository,
    graph: &HistoryGraph,
    source: ObjectId,
    target: ObjectId,
) -> Result<Plan> {
    stack_insert_plan(repo, graph, source, source, target)
}

pub(crate) fn stack_insert_plan(
    repo: &gix::Repository,
    graph: &HistoryGraph,
    base: ObjectId,
    head: ObjectId,
    target: ObjectId,
) -> Result<Plan> {
    if repo.head_id()?.detach() != head {
        anyhow::bail!("the move source must be the current HEAD");
    }
    if !graph.is_ancestor(base, head) {
        anyhow::bail!("the stack base must be an ancestor of HEAD");
    }
    graph
        .parents_of(target)
        .context("the move target is not in the loaded history")?;

    let mut stack = vec![head];
    let mut stack_parent = HashMap::new();
    loop {
        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 {

View on GitHub (pinned to e73179060b)