{"record":{"id":"01948b92ee4e52c1","repo":"gitbutlerapp/gitbutler","slug":"an-octopus-merge-commits-must-have-at-least-two-parents","errorCode":null,"errorMessage":"An octopus merge commits must have at least two parents","messagePattern":"An octopus merge commits must have at least two parents","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-rebase/src/merge.rs","lineNumber":40,"sourceCode":"/// which are part of the branches or stacks.\n///\n/// ### About Signing\n///\n/// Merges are special, and we will *not* sign it if it wasn't yet signed. That way workspace commits will naturally\n/// remain unsigned.\n/// However, if we re-merge a commit that was signed before it's likely a user-commit that should be treated accordingly.\n/// Thanks to this logic, the caller shouldn't have to steer signing.\npub(crate) fn octopus(\n    repo: &gix::Repository,\n    mut target_merge_commit: gix::objs::Commit,\n    graph: &mut gix::revwalk::Graph<\n        '_,\n        '_,\n        gix::revwalk::graph::Commit<gix::revision::plumbing::merge_base::Flags>,\n    >,\n) -> Result<gix::ObjectId> {\n    if target_merge_commit.parents.len() < 2 {\n        bail!(\"An octopus merge commits must have at least two parents\");\n    }\n    let parents_to_merge = target_merge_commit.parents.iter().copied();\n    let merge_base = but_core::Commit::from_id(\n        repo.merge_base_octopus_with_graph(parents_to_merge.clone(), graph)?,\n    )?\n    .tree_id_or_kind(TreeKind::Base)?\n    .detach();\n    let mut trees_to_merge = parents_to_merge\n        .clone()\n        .map(|commit_id| -> Result<_> {\n            // TODO: as long as only cherry-picking is creating these trees, THEIRS\n            //       is the original 'to_rebase'. However, if that changes we must know\n            //       what created the special merge commit.\n            Ok(but_core::Commit::from_id(commit_id.attach(repo))?\n                .tree_id_or_kind(TreeKind::Theirs)?\n                .detach())\n        })\n        .collect::<Result<Vec<_>, _>>()?","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-rebase/src/merge.rs#L22-L58","documentation":"`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).","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nmerge::octopus(repo, ordinary_commit, &mut graph)?; // 1 parent\n// after\nif commit.parents.len() > 1 {\n    merge::octopus(repo, commit, &mut graph)?;\n} else {\n    /* normal cherry-pick path */\n}","handlingStrategy":"validation","validationCode":"fn is_merge_commit(commit: &gix::Commit) -> bool {\n    commit.parent_ids().count() >= 2\n}","typeGuard":"fn assert_octopus_input(commit: &but_core::Commit) -> Option<&Vec<gix::ObjectId>> {\n    if commit.parents.len() >= 2 { Some(&commit.parents) } else { None }\n}","tryCatchPattern":"match merge::octopus(repo, merge_commit, &mut graph) {\n    Err(e) if e.to_string().contains(\"at least two parents\") => {\n        // fall back to the normal cherry-pick path for non-merge commits\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["git","merge","internal-invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}