GitoxideLabs/gitoxide · error
copying a commit cannot rewrite root or merge commits
Error message
copying a commit cannot rewrite root or merge commits
What it means
After inserting the copied commit under `target`, the plan rewrites all descendants of the target so they sit on top of the copy. Each rewritten commit must have exactly one parent: the rebase plan can only re-parent linear commits. If any affected descendant is a root or merge commit, the plan cannot represent the rewrite and this error is raised.
Solutions
- Choose a copy target whose descendants are all linear (single-parent) commits.
- Rebase the merge-heavy section linearly first, then perform the copy.
- Manually cherry-pick the source commit onto the target with git tooling instead of using tix copy.
Example fix
// before
tix copy <source> <target-with-merge-descendants>
// after
let scope = graph.descendants_in_parent_order(target)?;
if scope.iter().any(|id| graph.parents_of(*id)?.len() != 1) {
eprintln!("target has merge descendants; cannot copy here");
return Ok(());
}
tix copy(source, target) Defensive patterns
Strategy: validation
Validate before calling
for id in graph.descendants_in_parent_order(target)? {
if graph.parents_of(id)?.len() != 1 {
eprintln!("target has non-linear descendants; copy would rewrite a merge/root commit");
return Ok(());
}
} Type guard
fn target_scope_is_linear(graph: &HistoryGraph, target: ObjectId) -> bool {
graph.descendants_in_parent_order(target)
.map(|ds| ds.iter().all(|id| graph.parents_of(*id).map(|p| p.len() == 1).unwrap_or(false)))
.unwrap_or(false)
} Try / catch
match copy_insert_plan(&repo, &graph, source, target) {
Err(e) if e.to_string().contains("root or merge commits") => eprintln!("choose a target without merge descendants"),
other => other?,
} Prevention
- Prefer insertion points in linear segments of history
- Show merge topology when choosing a copy target
- Check descendants' parent counts before initiating copy operations in merge-heavy repos
When it happens
Trigger: Calling `copy_insert_plan(repo, graph, source, target)` where any commit in `graph.descendants_in_parent_order(target)` (excluding target itself) has zero or multiple parents — e.g. the target's descendants contain a merge commit that spans the target.
Common situations: Copying a commit into a branch of history where a later merge brings those changes together; repositories with frequent merges where the insertion point has merge descendants.
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
- copying a commit requires it to have exactly one parent
- moving a stack cannot rewrite root or merge commits
- the squash source must have exactly one parent
- the squash target must have exactly one parent
- descendant merge commits cannot be squashed
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/2b6bef764038b535.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:581
}
if source == target {
anyhow::bail!("the copy source and target must differ");
}
let mut scope = graph
.descendants_in_parent_order(target)
.context("the copy target is not in the loaded history")?;
scope.retain(|id| *id != target);
let mut steps = vec![PlanStep {
parent: PlanParent::Existing(target),
commit: PlanCommit::Copy(source),
squash: Vec::new(),
}];
let mut step_by_id = HashMap::with_capacity(scope.len());
for id in &scope {
let parents = graph.parents_of(*id).context("an affected copy commit is incomplete")?;
let [parent] = parents.as_slice() else {
anyhow::bail!("copying a commit cannot rewrite root or merge commits");
};
let parent = if *parent == target {
PlanParent::Step(0)
} else {
step_by_id
.get(parent)
.copied()
.map_or(PlanParent::Existing(*parent), PlanParent::Step)
};
step_by_id.insert(*id, steps.len());
steps.push(PlanStep {
parent,
commit: PlanCommit::Pick(*id),
squash: Vec::new(),
});
}
let mut ref_scope = Vec::with_capacity(scope.len() + 1);View on GitHub (pinned to e73179060b)