GitoxideLabs/gitoxide · error

merge commits cannot be picked by the rebase editor

Error message

merge commits cannot be picked by the rebase editor

What it means

The rebase editor can only rewrite linear history, so picking a commit with more than one parent (a merge) into the plan is forbidden. The validator checks `parents_of(id).len() > 1` for each picked or squashed id and bails.

Solutions

  1. Exclude merge commits from the pick list (use no-merges enumeration)
  2. Linearize the history before rebasing (rebase merges into linear commits first)
  3. Remove the merge pick step from the plan and keep the merge out of the rewritten range

Example fix

// before
steps.push(pick(merge_id));
// after
if graph.parents_of(merge_id)?.len() == 1 {
    steps.push(pick(merge_id));
} // merges skipped
Defensive patterns

Strategy: validation

Validate before calling

fn all_linear(graph: &Graph, picks: &[ObjectId]) -> bool {
    picks.iter().all(|p| graph.parents_of(p).map(|ps| ps.len() <= 1).unwrap_or(false))
}

Prevention

When it happens

Trigger: A plan includes a `PlanCommit::Pick` or a squash entry whose commit is a merge commit; scripts that build todo lists from `git log` output including merge entries.

Common situations: Rebasing across merge-heavy feature branches; copying a todo list generated without `--no-merges`; users explicitly reordering merge commits expecting git-preserve-merges semantics that this editor does not implement.

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/4d251adb63ea99c4. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/rebase.rs:1655

    let scope: HashSet<_> = plan.scope.iter().copied().collect();
    let mut picked = HashSet::new();
    for step in &plan.steps {
        if let PlanCommit::Copy(id) = step.commit
            && graph.parents_of(id).context("a copied commit is incomplete")?.len() != 1
        {
            anyhow::bail!("copying a commit requires it to have exactly one parent");
        }
        let ids = match step.commit {
            PlanCommit::Pick(id) | PlanCommit::Resolved(id) => Some(id).into_iter().chain(step.squash.iter().copied()),
            PlanCommit::Copy(_) | PlanCommit::Empty(_) => None.into_iter().chain(step.squash.iter().copied()),
        };
        for id in ids {
            if !scope.contains(&id) || !picked.insert(id) {
                anyhow::bail!("a rebase plan contains an invalid or duplicate pick");
            }
            if graph.parents_of(id).context("a picked commit is incomplete")?.len() > 1 {
                anyhow::bail!("merge commits cannot be picked by the rebase editor");
            }
        }
    }

    let checkout_target = if repo.workdir().is_some() {
        plan.checkout
            .as_ref()
            .map(|checkout| checkout.target)
            .or(infer_plan_checkout(&repo, graph, &plan)?)
    } else {
        None
    };
    if plan.checkout.is_none()
        && checkout_target.is_some()
        && !plan
            .steps
            .iter()
            .any(|step| matches!(step.commit, PlanCommit::Resolved(_)))

View on GitHub (pinned to e73179060b)