GitoxideLabs/gitoxide · error

descendant merge commits cannot be rebased

Error message

descendant merge commits cannot be rebased

What it means

When rebasing/cherry-picking a set of affected commits, a descendant merge commit can only be tolerated at the tip (position 0) of an unremoved, non-cherry-pick rewrite. Any merge found deeper in the affected set — or at position 0 when commits are removed or the mode is cherry-pick — cannot be rewritten, so the operation bails.

Solutions

  1. Linearize the descendants (rebase their merges away) before rewriting the target commit
  2. Restrict the rewrite to commits without merge descendants (compute descendants first and abort/flatten merges)
  3. For cherry-pick, copy only commits that are not ancestors of any merge

Example fix

// before
reword(repo, old_commit)?; // old_commit has merge descendants
// after
if has_merge_descendants(&graph, old_commit) {
    linearize_descendants(&repo, old_commit)?;
}
reword(repo, old_commit)?;
Defensive patterns

Strategy: validation

Validate before calling

fn rewrite_safe(graph: &Graph, affected: &[ObjectId], position: usize, removed: bool, cherry_pick: bool) -> bool {
    affected.iter().enumerate().all(|(i, id)| {
        let merge = graph.parents_of(id).map(|p| p.len() > 1).unwrap_or(false);
        !merge || (i == 0 && !removed && !cherry_pick && position == 0)
    })
}

Try / catch

match result {
    Err(e) if e.to_string().contains("descendant merge commits") => {
        eprintln!("linearize or exclude commits with merge descendants before rewriting");
    }
    r => r?,
}

Prevention

When it happens

Trigger: Rewriting (reword/drop/squash/cherry-pick) a commit that has merge commits among its descendants, with `position > 0`, or `removed == true`, or `tree == Tree::CherryPick`, detected via `parents_of(id).len() > 1`.

Common situations: Dropping or rewording an old commit when feature branches were merged into the line since; cherry-picking commits that later merges depend on; automated history edits over merge-heavy team histories.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/1cf4b9c619e0a4ab. Report an issue: GitHub.

Appendix: source

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

    index.sort_entries();
    index.remove_tree();
    index
        .write(gix::index::write::Options::default())
        .context("could not update selected index paths")
}

fn validate(
    repo: &gix::Repository,
    graph: &HistoryGraph,
    affected: &[ObjectId],
    removed: bool,
    repeat: bool,
    tree: Tree,
) -> Result<()> {
    for (position, id) in affected.iter().enumerate() {
        let parents = graph.parents_of(*id).context("an affected commit is incomplete")?;
        if parents.len() > 1 && (position > 0 || removed || tree == Tree::CherryPick) {
            anyhow::bail!("descendant merge commits cannot be rebased");
        }
        if repeat && position == 0 {
            let commit = repo.find_commit(*id)?.decode()?.into_owned()?;
            if !is_pending(&commit) {
                anyhow::bail!("the root of a repeated rebase must be pending");
            }
        }
    }
    if repeat
        && let Some(base) = affected.first()
        && let Some(parent) = graph.parents_of(*base).and_then(|parents| parents.first().copied())
        && is_pending(&repo.find_commit(parent)?.decode()?.into_owned()?)
    {
        anyhow::bail!("the parent of a repeated rebase must not be pending");
    }
    Ok(())
}

View on GitHub (pinned to e73179060b)