gitbutlerapp/gitbutler · error · anyhow::Error

Re-merging the conflicting trees of commit {commit_id} yield

Error message

Re-merging the conflicting trees of commit {commit_id} yielded no conflicts to resolve

What it means

build_request() confirmed the commit is conflicted (three trees available), then re-merges base/ours/theirs with diff3 markers and applies the conflicts to a fresh index. If that application reports no changed entries, the re-merge produced nothing addressable - inconsistent with the conflicted shape detected earlier - so it bails defensively rather than fabricate an empty resolution request.

Source

Thrown at crates/but-api/src/resolve/context.rs:168

    let repo = repo.clone().for_tree_diffing()?;
    // Merge without favoring a side to reproduce the actual conflicts, and
    // force diff3-style markers with the sentinel labels so every hunk carries
    // the common ancestor and marker lines are exactly known strings.
    let mut options: gix::merge::plumbing::tree::Options = repo.tree_merge_options()?.into();
    options.blob_merge.text.conflict = gix::merge::blob::builtin_driver::text::Conflict::Keep {
        style: gix::merge::blob::builtin_driver::text::ConflictStyle::Diff3,
        marker_size: 7.try_into().expect("non-zero constant"),
    };
    let mut outcome = repo.merge_trees(base, ours, theirs, merge_labels(), options.into())?;
    let merged_tree_id = outcome.tree.write()?.detach();

    let mut index = repo.index_from_tree(&merged_tree_id)?;
    if !outcome.index_changed_after_applying_conflicts(
        &mut index,
        gix::merge::tree::TreatAsUnresolved::git(),
        gix::merge::tree::apply_index_entries::RemovalMode::Mark,
    ) {
        bail!(
            "Re-merging the conflicting trees of commit {commit_id} yielded no conflicts to resolve"
        );
    }

    let mut sides_by_path: std::collections::BTreeMap<BString, ConflictSides> = Default::default();
    for entry in index.entries() {
        use gix::index::entry::Stage;
        let sides = sides_by_path
            .entry(entry.path(&index).to_owned())
            .or_default();
        match entry.stage() {
            Stage::Unconflicted => continue,
            Stage::Base => sides.base = true,
            Stage::Ours => sides.ours = true,
            Stage::Theirs => sides.theirs = true,
        }
    }
    sides_by_path.retain(|_, sides| sides.base || sides.ours || sides.theirs);

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Resolve this commit manually (edit mode or outside the API) - the request cannot be built
  2. Check .gitattributes and gitconfig for custom merge drivers that interfere with the re-merge
  3. If it reproduces with stock merge config, report it with the commit shape
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await api.resolveCommitConflictHunks(commitId, specs);
} catch (err) {
  if (String(err).includes('yielded no conflicts to resolve')) {
    routeToManualResolution(commitId); // internal inconsistency; not retryable via this API
  } else throw err;
}

Prevention

When it happens

Trigger: Rare: repo configuration makes the re-merge fully clean, e.g. custom merge drivers wired through .gitattributes/drivers that auto-resolve when gix replays the merge with GitButler's labels, or degenerate rename/splice histories where the second merge agrees everywhere.

Common situations: Repos with custom merge drivers (union drivers, lockfile drivers) active during the re-merge; exotic conflict shapes produced by external tools; essentially never on default git configuration.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/99aeaedad2b9306f. Report an issue: GitHub.