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

  1. Check the counts before converting and fall back to a plain two-tree diff when sides != bases + 1.
  2. Re-create the offending workspace commit as successive two-branch merges.
  3. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/df802947edc7c492. Report an issue: GitHub.