gitbutlerapp/gitbutler · error

An octopus merge commits must have at least two parents

Error message

An octopus merge commits must have at least two parents

What it means

`but_rebase::merge::octopus` replays a merge commit during a rebase by computing the octopus merge base of its parents. The function requires the target merge commit to have at least two parents; a commit with fewer parents is not a real merge and cannot be octopus-replayed, so it bails out. This is an internal precondition guard (normally only reachable through a bug, since the caller only routes multi-parent commits here).

Solutions

  1. Verify that only commits with `parents.len() > 1` are routed to `merge::octopus` (see the `commit.parents.len() > 1` branch in `rebase`).
  2. If you are calling the helper directly, pass an actual merge commit with two or more parents.
  3. Check for corrupted or rewritten commit objects that lost a parent and re-derive the commit list.

Example fix

// before
merge::octopus(repo, ordinary_commit, &mut graph)?; // 1 parent
// after
if commit.parents.len() > 1 {
    merge::octopus(repo, commit, &mut graph)?;
} else {
    /* normal cherry-pick path */
}
Defensive patterns

Strategy: validation

Validate before calling

fn is_merge_commit(commit: &gix::Commit) -> bool {
    commit.parent_ids().count() >= 2
}

Type guard

fn assert_octopus_input(commit: &but_core::Commit) -> Option<&Vec<gix::ObjectId>> {
    if commit.parents.len() >= 2 { Some(&commit.parents) } else { None }
}

Try / catch

match merge::octopus(repo, merge_commit, &mut graph) {
    Err(e) if e.to_string().contains("at least two parents") => {
        // fall back to the normal cherry-pick path for non-merge commits
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling `merge::octopus` (directly or via the Pick path in `rebase`) with a `target_merge_commit` whose `parents.len() < 2` — i.e. a root or normal commit was passed as a merge commit (crates/but-rebase/src/merge.rs:40).

Common situations: Upstream logic bug where a single-parent commit is misclassified as a merge (e.g. after a mapping/graph error); direct use of the internal `octopus` helper in tests or tooling with a non-merge commit.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/01948b92ee4e52c1. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-rebase/src/merge.rs:40

/// which are part of the branches or stacks.
///
/// ### About Signing
///
/// Merges are special, and we will *not* sign it if it wasn't yet signed. That way workspace commits will naturally
/// remain unsigned.
/// However, if we re-merge a commit that was signed before it's likely a user-commit that should be treated accordingly.
/// Thanks to this logic, the caller shouldn't have to steer signing.
pub(crate) fn octopus(
    repo: &gix::Repository,
    mut target_merge_commit: gix::objs::Commit,
    graph: &mut gix::revwalk::Graph<
        '_,
        '_,
        gix::revwalk::graph::Commit<gix::revision::plumbing::merge_base::Flags>,
    >,
) -> Result<gix::ObjectId> {
    if target_merge_commit.parents.len() < 2 {
        bail!("An octopus merge commits must have at least two parents");
    }
    let parents_to_merge = target_merge_commit.parents.iter().copied();
    let merge_base = but_core::Commit::from_id(
        repo.merge_base_octopus_with_graph(parents_to_merge.clone(), graph)?,
    )?
    .tree_id_or_kind(TreeKind::Base)?
    .detach();
    let mut trees_to_merge = parents_to_merge
        .clone()
        .map(|commit_id| -> Result<_> {
            // TODO: as long as only cherry-picking is creating these trees, THEIRS
            //       is the original 'to_rebase'. However, if that changes we must know
            //       what created the special merge commit.
            Ok(but_core::Commit::from_id(commit_id.attach(repo))?
                .tree_id_or_kind(TreeKind::Theirs)?
                .detach())
        })
        .collect::<Result<Vec<_>, _>>()?

View on GitHub (pinned to 58e5313667)