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

  1. Choose a copy target whose descendants are all linear (single-parent) commits.
  2. Rebase the merge-heavy section linearly first, then perform the copy.
  3. 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

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


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)