gitbutlerapp/gitbutler · error
in commit {}, number of sides ({}) is not exactly one more t
Error message
in commit {}, number of sides ({}) is not exactly one more than number of bases ({}) What it means
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.
Source
Thrown at crates/but-core/src/commit/tree_expression.rs:26
/// Sum of sides minus sum of bases. All functions enforce the invariant that
/// the count of sides is exactly one more than the count of bases.
#[derive(Debug, Clone, PartialEq)]
pub struct TreeExpression {
/// Base tree IDs.
pub base_tree_ids: Vec<gix::ObjectId>,
/// Side tree IDs.
pub side_tree_ids: SmallVec<[gix::ObjectId; 1]>,
}
impl TryFrom<&crate::Commit<'_>> for TreeExpression {
type Error = anyhow::Error;
fn try_from(commit: &crate::Commit<'_>) -> Result<Self, Self::Error> {
let base_tree_ids = commit.base_tree_ids()?;
let side_tree_ids = commit.side_tree_ids()?;
if base_tree_ids.len() + 1 != side_tree_ids.len() {
anyhow::bail!(
"in commit {}, number of sides ({}) is not exactly one more than number of bases ({})",
commit.id.to_hex(),
side_tree_ids.len(),
base_tree_ids.len()
);
}
Ok(Self {
base_tree_ids,
side_tree_ids,
})
}
}
impl From<gix::ObjectId> for TreeExpression {
fn from(side: gix::ObjectId) -> Self {
Self {
base_tree_ids: Vec::new(),
side_tree_ids: smallvec![side],View on GitHub (pinned to 2497b8007a)
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.
Example fix
// before
let expr = TreeExpression::try_from(&commit)?;
// after
let (bases, sides) = (commit.base_tree_ids()?, commit.side_tree_ids()?);
if bases.len() + 1 != sides.len() {
// not a two-sided merge shape: degrade to a plain diff
return plain_diff(&repo, &commit);
}
let expr = TreeExpression::try_from(&commit)?; Defensive patterns
Strategy: validation
Validate before calling
let commit = but_core::Commit::from_id(id.attach(&repo))?;
let (bases, sides) = (commit.base_tree_ids()?, commit.side_tree_ids()?);
if bases.len() + 1 != sides.len() {
// not two-sided: skip TreeExpression, use a plain diff
} Try / catch
match TreeExpression::try_from(&commit) {
Err(err) if err.to_string().contains("number of sides") => plain_diff(&repo, &commit),
r => r.map(TreeExpression::from),
}? Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- No stacks found in the workspace
- Stack has no ID
- No uncommitted changes found for the selected files
- Failed to determine target commit for hunk absorption due to
- Stack has no branches
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/df802947edc7c492.
Report an issue: GitHub.