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

  1. Choose a non-merge, non-root commit as the copy source.
  2. Pre-check `graph.parents_of(source)` for length 1 before calling the copy operation.
  3. 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

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


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)