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
- Verify that only commits with `parents.len() > 1` are routed to `merge::octopus` (see the `commit.parents.len() > 1` branch in `rebase`).
- If you are calling the helper directly, pass an actual merge commit with two or more parents.
- 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
- Only call merge::octopus from the `parents.len() > 1` branch of the rebase loop.
- When calling the helper directly, assert merge-ness with a parent-count check first.
- Investigate upstream classification bugs if a non-merge commit reaches this path.
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
- Branch not found
- cannot currently handle more than 1 parent
- Cannot merge : it shares no history with /
- Expected target tip selector to point to a pick
- Failed to find commit
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)