{"record":{"id":"df802947edc7c492","repo":"gitbutlerapp/gitbutler","slug":"in-commit-number-of-sides-is-not-exactly","errorCode":null,"errorMessage":"in commit {}, number of sides ({}) is not exactly one more than number of bases ({})","messagePattern":"in commit (.+?), number of sides \\((.+?)\\) is not exactly one more than number of bases \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-core/src/commit/tree_expression.rs","lineNumber":26,"sourceCode":"\n/// Sum of sides minus sum of bases. All functions enforce the invariant that\n/// the count of sides is exactly one more than the count of bases.\n#[derive(Debug, Clone, PartialEq)]\npub struct TreeExpression {\n    /// Base tree IDs.\n    pub base_tree_ids: Vec<gix::ObjectId>,\n    /// Side tree IDs.\n    pub side_tree_ids: SmallVec<[gix::ObjectId; 1]>,\n}\n\nimpl TryFrom<&crate::Commit<'_>> for TreeExpression {\n    type Error = anyhow::Error;\n\n    fn try_from(commit: &crate::Commit<'_>) -> Result<Self, Self::Error> {\n        let base_tree_ids = commit.base_tree_ids()?;\n        let side_tree_ids = commit.side_tree_ids()?;\n        if base_tree_ids.len() + 1 != side_tree_ids.len() {\n            anyhow::bail!(\n                \"in commit {}, number of sides ({}) is not exactly one more than number of bases ({})\",\n                commit.id.to_hex(),\n                side_tree_ids.len(),\n                base_tree_ids.len()\n            );\n        }\n        Ok(Self {\n            base_tree_ids,\n            side_tree_ids,\n        })\n    }\n}\n\nimpl From<gix::ObjectId> for TreeExpression {\n    fn from(side: gix::ObjectId) -> Self {\n        Self {\n            base_tree_ids: Vec::new(),\n            side_tree_ids: smallvec![side],","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but-core/src/commit/tree_expression.rs#L8-L44","documentation":"TreeExpression::try_from(&Commit) enforces GitButler's merge-shape invariant: a commit's side trees must number exactly one more than its base trees (a normal two-sided merge is 1 base + 2 sides). Commits that violate it — octopus-shaped merges, or workspace commits whose recorded tree data has mismatched arity — fail conversion with the commit id and both counts. Consumers of TreeExpression (diff/preview code paths) assume two-sided merges.","triggerScenarios":"Converting a workspace commit created by merging three or more branches at once (bases + 1 != sides), or any commit whose base_tree_ids()/side_tree_ids() counts break the invariant, into a TreeExpression.","commonSituations":"Users stacking or merging many branches into one workspace commit; imported history containing octopus merges; new code that assumes every conflicted commit is two-sided.","solutions":["Check the counts before converting and fall back to a plain two-tree diff when sides != bases + 1.","Re-create the offending workspace commit as successive two-branch merges.","Inspect the commit with `git ls-tree` against the reported counts; if the data looks valid, file a bug with the commit id."],"exampleFix":"// before\nlet expr = TreeExpression::try_from(&commit)?;\n\n// after\nlet (bases, sides) = (commit.base_tree_ids()?, commit.side_tree_ids()?);\nif bases.len() + 1 != sides.len() {\n    // not a two-sided merge shape: degrade to a plain diff\n    return plain_diff(&repo, &commit);\n}\nlet expr = TreeExpression::try_from(&commit)?;","handlingStrategy":"validation","validationCode":"let commit = but_core::Commit::from_id(id.attach(&repo))?;\nlet (bases, sides) = (commit.base_tree_ids()?, commit.side_tree_ids()?);\nif bases.len() + 1 != sides.len() {\n    // not two-sided: skip TreeExpression, use a plain diff\n}","typeGuard":null,"tryCatchPattern":"match TreeExpression::try_from(&commit) {\n    Err(err) if err.to_string().contains(\"number of sides\") => plain_diff(&repo, &commit),\n    r => r.map(TreeExpression::from),\n}?","preventionTips":["Never assume merge arity — probe base/side counts before conversion.","Handle the mismatch branch explicitly instead of propagating the invariant error to users.","Log commit ids when invariants fail so the shape can be reproduced."],"tags":["git","merge","octopus-merge","tree","invariant"],"backgroundTag":"unsupported-merge-shape","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}