gitbutlerapp/gitbutler · error · anyhow::Error

Cherry picks can only be done for single-parent commits. Mer

Error message

Cherry picks can only be done for single-parent commits. Merge-commits need to be re-merged

What it means

During graph rebase, cherry-picking rewrites a commit's parent to its new base. `set_parent` refuses commits with more than one parent: a merge commit cannot be recreated by substituting a single parent — its multiple parents must be re-merged relative to the new base.

Source

Thrown at crates/but-rebase/src/cherry_pick.rs:81

        && commit_to_rebase.parents.contains(&base.id.detach())
    {
        return Ok(commit_to_rebase.id);
    };

    let mut cherry_pick = cherry_pick_tree(&base, &commit_to_rebase)?;
    let tree_id = cherry_pick.tree.write()?;

    let conflict_kind = gix::merge::tree::TreatAsUnresolved::forced_resolution();
    if cherry_pick.has_unresolved_conflicts(conflict_kind) {
        commit_from_conflicted_tree(base, commit_to_rebase, tree_id, cherry_pick, conflict_kind)
    } else {
        commit_from_unconflicted_tree(base, commit_to_rebase, tree_id, empty_commit)
    }
}

fn set_parent(to_rebase: &mut gix::objs::Commit, new_parent: gix::ObjectId) -> anyhow::Result<()> {
    if to_rebase.parents.len() > 1 {
        bail!(
            "Cherry picks can only be done for single-parent commits. Merge-commits need to be re-merged"
        )
    }
    to_rebase.parents.clear();
    to_rebase.parents.push(new_parent);
    Ok(())
}

/// Rebase `to_rebase` onto `new_base`, dealing with the intricacies of conflicted trees, and return the newly
/// merged tree.
/// Note that all merges are made to succeed, possibly recording the original trees in a special tree.
fn cherry_pick_tree<'repo>(
    new_base: &but_core::Commit<'repo>,
    to_rebase: &but_core::Commit<'repo>,
) -> anyhow::Result<gix::merge::tree::Outcome<'repo>> {
    let repo = to_rebase.id.repo;
    let (base, ours, theirs) = find_cherry_pick_trees(new_base, to_rebase)?;
    use but_core::RepositoryExt;

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Model merge commits as re-merge steps in the graph plan/editor instead of picks
  2. Skip or drop merge commits from the pick sequence where the semantics allow
  3. Fall back to git rebase strategies that preserve merges, or re-merge the commits manually

Example fix

// before: merge commit scheduled as a plain pick
plan.push(Step::Pick { id: merge_commit, ..Default::default() }); // bails in set_parent

// after: detect merges when building the plan and handle them separately
if parents_of(repo, commit_id)?.len() > 1 {
    plan.push(Step::ReMerge { id: commit_id, .. }); // or skip
} else {
    plan.push(Step::Pick { id: commit_id, ..Default::default() });
}
Defensive patterns

Strategy: validation

Validate before calling

// Rust: classify merge commits while building the plan
let commit = repo.find_object(commit_id)?.try_into_commit()?;
let is_merge = commit.parent_ids().count() > 1;
if is_merge {
    plan.push_re_merge(commit_id); // or skip
} else {
    plan.push_pick(commit_id);
}

Try / catch

Catch the bail, identify the merge commit from the plan, and either drop it from the pick sequence or switch it to a re-merge step before rebuilding.

Prevention

When it happens

Trigger: A Pick step in the rebase plan whose commit is a merge commit (parents.len() > 1) reaching `set_parent` — usually a plan built without merge handling or with merge steps misclassified as picks.

Common situations: Histories containing merge commits (PR merges, criss-cross merges) fed into a pick-only plan; custom graph tooling that forgets to special-case merges.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/fbccdec4383bb62f. Report an issue: GitHub.