GitoxideLabs/gitoxide · error

a recorded scope commit is disconnected from the rebase base

Error message

a recorded scope commit is disconnected from the rebase base

What it means

Every commit in the recorded rebase scope must connect back to the rebase base: its first parent must be the base, another scope commit, or the commit itself must be a continuation source (a conflict resolution point). Otherwise the captured scope is disconnected from the history being rewritten, indicating corrupt or tampered state.

Solutions

  1. Remove the stale rebase state and restart the rebase
  2. Re-record state so all scope commits are captured together with their connecting history
  3. If the commit legitimately roots a new history, register it as a continuation-source in the state
Defensive patterns

Strategy: validation

Validate before calling

// ensure each scope commit's first parent connects to base or scope,
// or the commit is a continuation source
for id in &state.scope {
    let parent = repo.find_commit(*id)?.parent_ids().next().unwrap();
    assert!(parent == state.base || scope.contains(&parent)
        || state.continuation_sources.contains(id));
}

Try / catch

match result {
    Err(e) if e.to_string().contains("disconnected from the rebase base") => {
        // state graph is stale: discard and restart the rebase
    }
    r => r?,
}

Prevention

When it happens

Trigger: validate_state walks each scope commit via `parent_ids().next()`; the bail fires when a scope commit's first parent is not `state.base`, not in `scope`, and the commit is not listed in `continuation_sources`. Happens with hand-edited state, merged-in unrelated commits, or state persisted before external history modification.

Common situations: Amending or rebasing history with plain git while a tix rebase state exists; copying state between repos where the DAG differs; incomplete state files after a crash.

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

Appendix: source

Thrown at gix-tix/src/edit/todo.rs:817

            .iter()
            .any(|reference| reference.editable && reference.name == *name)
    {
        anyhow::bail!("the recorded HEAD ref is not editable");
    }
    for tip in &state.tips {
        repo.find_commit(*tip).context("could not find a recorded rebase tip")?;
    }
    for id in &state.scope {
        let commit = repo
            .find_commit(*id)
            .context("could not find a recorded scope commit")?;
        let parent = commit
            .parent_ids()
            .next()
            .map(gix::Id::detach)
            .context("a recorded scope commit has no parent")?;
        if parent != state.base && !scope.contains(&parent) && !continuation_sources.contains(id) {
            anyhow::bail!("a recorded scope commit is disconnected from the rebase base");
        }
    }
    if state.resolved.is_some_and(|id| !scope.contains(&id)) {
        anyhow::bail!("the resolved conflict is outside the rebase scope");
    }
    if !continuation_sources.is_subset(&scope) {
        anyhow::bail!("a continuation source is outside the rebase scope");
    }
    Ok(())
}

pub(crate) fn parse(repo: &gix::Repository, edited: &[u8]) -> Result<Option<Parsed>> {
    repo.head()?.id().context("rebase todos require a born HEAD")?;
    let input = std::str::from_utf8(edited).context("the rebase todo is not UTF-8")?;
    let Some(mut state) = parse_state(repo, input)? else {
        return Ok(None);
    };
    let scope: HashSet<_> = state.scope.iter().copied().collect();

View on GitHub (pinned to e73179060b)