GitoxideLabs/gitoxide · error · anyhow::Error

Tree conflicted

Error message

Tree conflicted

What it means

At the end of the merge-tree run, after printing conflict details, the command fails with 'Tree conflicted' if any conflicts remain unresolved (`has_unresolved_conflicts`). Unlike error 112 this path also fires when no commit message was given, i.e. it marks the overall merge as conflicted even for dry-run style invocations.

Solutions

  1. Read the conflict list printed on stderr and resolve those paths before re-running.
  2. Treat a non-zero exit as the signal to abort or escalate the automated merge.
  3. Adjust `TreatAsUnresolved` semantics only if your tooling can safely handle those conflict kinds.

Example fix

// caller side
match merge_result {
    Ok(id) => use_tree(id),
    Err(e) if e.to_string().contains("Tree conflicted") => {
        // parse conflicted paths from stderr and resolve
        resolve_conflicts();
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Try / catch

let res = tree_merge(...)?;
if res.has_unresolved_conflicts(TreatAsUnresolved::default()) {
    // handle conflicts before treating the run as successful
} else {
    persist(res)?;
}

Prevention

When it happens

Trigger: `tree()` completes the merge, prints conflicts (and optionally `--debug` dumps), and `has_unresolved_conflicts` is true; the function then bails instead of returning Ok.

Common situations: Scripting merges whose inputs genuinely conflict; using a `TreatAsUnresolved` configuration that classifies certain conflict kinds as blocking; CI jobs merging feature branches that overlap with main.

Understand the failure class

Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/70ebfdfa1a851aef. Report an issue: GitHub.

Appendix: source

Thrown at gitoxide-core/src/repository/merge/tree.rs:134

                let commit_id = repo.commit("HEAD", message, tree_id, Some(head_id))?;
                let mut index = repo.index_from_tree(&tree_id)?;
                index.write(Default::default())?;
                commit_id
            } else {
                repo.new_commit(message, tree_id, Some(head_id))?.id()
            };
            writeln!(out, "{commit_id} (commit)")?;
            return Ok(());
        }

        if debug {
            writeln!(err, "{conflicts:#?}")?;
        }
        if has_conflicts {
            writeln!(err, "{} possibly resolved conflicts", conflicts.len())?;
        }
        if has_unresolved_conflicts {
            bail!("Tree conflicted")
        }
        Ok(())
    }

    fn persist_in_memory_objects(repo: &mut gix::Repository) -> anyhow::Result<()> {
        let objects = repo.objects.take_object_memory().expect("always write in memory first");
        for (_id, (kind, data)) in objects.iter() {
            repo.write_buf(*kind, data).map_err(|err| anyhow!("{err}"))?;
        }
        Ok(())
    }

    fn write_unresolved_conflict_paths(
        err: &mut dyn std::io::Write,
        conflicts: &[gix::merge::tree::Conflict],
    ) -> std::io::Result<()> {
        let how = TreatAsUnresolved::default();
        let mut paths = BTreeSet::new();

View on GitHub (pinned to e73179060b)