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
- Linearize the descendants (rebase their merges away) before rewriting the target commit
- Restrict the rewrite to commits without merge descendants (compute descendants first and abort/flatten merges)
- 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
- Compute descendants and check for merges before any history rewrite
- Linearize merge-heavy branches before reword/drop/cherry-pick operations
- Restrict cherry-picks to commits not merged downstream
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
- the squash source must have exactly one parent
- the squash target must have exactly one parent
- descendant merge commits cannot be squashed
- copying a commit requires it to have exactly one parent
- copying a commit cannot rewrite root or merge commits
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)