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
- Resolve this commit manually (edit mode or outside the API) - the request cannot be built
- Check .gitattributes and gitconfig for custom merge drivers that interfere with the re-merge
- 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
- Check .gitattributes/gitconfig for custom merge drivers before using the resolve APIs on that repo
- Treat this as non-retryable through the API - the request itself cannot be built
- Keep the commit around when it reproduces; it is a bug-report-grade state
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
- Commit {commit_id} has no conflicted files to resolve
- Conflict {} of "{}" was addressed more than once
- "{}" has {} conflict{}, but conflict {} was addressed
- The resolution for "{path}" contains a conflict marker ({mar
- Validation
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/99aeaedad2b9306f.
Report an issue: GitHub.