GitoxideLabs/gitoxide · error
copying a commit requires it to have exactly one parent
Error message
copying a commit requires it to have exactly one parent
What it means
The `copy_insert_plan` operation copies a commit onto another commit and rewrites the affected history, but the rebase plan machinery only knows how to handle linear history (one-parent commits). The copy SOURCE commit must have exactly one parent; root commits (no parent) and merge commits (multiple parents) cannot be copied. The library raises this before planning any rewrite when the source commit's parent count differs from 1.
Solutions
- Choose a non-merge, non-root commit as the copy source.
- Pre-check `graph.parents_of(source)` for length 1 before calling the copy operation.
- To duplicate a merge or root commit, cherry-pick its diff manually and commit the result instead of using the copy operation.
Example fix
// before
tix copy <root-or-merge-commit-id> <target>
// after
if graph.parents_of(source)?.len() != 1 {
eprintln!("cannot copy root or merge commit");
return Ok(());
}
tix copy(source, target) Defensive patterns
Strategy: validation
Validate before calling
let parents = graph.parents_of(source).context("source not in history")?;
if parents.len() != 1 {
eprintln!("cannot copy: source must have exactly one parent");
return Ok(());
} Type guard
fn is_linear_commit(graph: &HistoryGraph, id: ObjectId) -> bool {
graph.parents_of(id).map(|p| p.len() == 1).unwrap_or(false)
} Try / catch
match copy_insert_plan(&repo, &graph, source, target) {
Err(e) if e.to_string().contains("exactly one parent") => eprintln!("source is a root or merge commit"),
other => other?,
} Prevention
- Filter candidate commits to single-parent commits before offering copy operations
- Visually mark root and merge commits in tooling so users avoid selecting them
- Reuse one `is_copyable` predicate across copy/stack operations
When it happens
Trigger: Calling `copy_insert_plan(repo, graph, source, target)` where `source` resolves in the loaded history graph but its `parents_of(source)` list is empty (root commit) or has 2+ entries (merge commit).
Common situations: Attempting to copy the repository's initial (root) commit; attempting to copy a merge commit produced by `git merge`; automation that passes commit IDs gathered from a log walk without filtering non-linear commits.
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 cannot rewrite root or merge commits
- 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/4aa21e107d4e0e9a.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:552
base,
scope,
steps,
checkout,
expected_refs,
})
}
pub(crate) fn copy_insert_plan(
repo: &gix::Repository,
graph: &HistoryGraph,
source: ObjectId,
target: ObjectId,
) -> Result<Plan> {
let source_parents = graph
.parents_of(source)
.context("the copy source is not in the loaded history")?;
let [_source_parent] = source_parents.as_slice() else {
anyhow::bail!("copying a commit requires it to have exactly one parent");
};
let source_commit = repo
.find_commit(source)
.context("could not find the copy source")?
.decode()
.context("could not decode the copy source")?
.into_owned()
.context("could not own the copy source")?;
if super::review::reference(&source_commit)?.is_some() {
anyhow::bail!("review commits cannot be copied");
}
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")?;View on GitHub (pinned to e73179060b)