GitoxideLabs/gitoxide · error
commit belongs to multiple unrelated review trees
Error message
commit belongs to multiple unrelated review trees
What it means
`review_tree` locates the single nearest review-tree root that is an ancestor of the given commit. If the commit is a descendant of two or more review roots where neither is an ancestor of the other, the roots are unrelated and there is no unique review tree to attribute the commit to. The library throws this instead of picking an arbitrary root.
Solutions
- Rebase or linearize the branch so the target commit descends from only one review root.
- Remove or re-create one of the conflicting review roots so the roots nest (one ancestor of the other).
- Time-travel to a commit that lies within a single review tree instead of a merge point.
- Restructure the history (e.g. squash the merge) so review roots form a chain.
Example fix
// before: commit is a merge of two unrelated review roots let dest = repo.find_commit(merge_id)?; review_tree(&repo, &graph, &roots, dest)?; // bails // after: target a commit on one review line let dest = repo.find_commit(side_a_tip)?; review_tree(&repo, &graph, &roots, dest)?; // unique nearest root
Defensive patterns
Strategy: validation
Validate before calling
let review_roots: Vec<_> = roots.iter().copied()
.filter(|r| graph.is_ancestor(*r, target_commit)).collect();
// must be 0 or 1, or a nesting chain of exactly one root
if review_roots.len() > 1 { /* restructure history first */ } Try / catch
match review_tree(&repo, &graph, &roots, commit) {
Ok(Some(tree)) => /* use tree */,
Ok(None) => /* commit is outside any review tree */,
Err(e) if e.to_string().contains("multiple unrelated review trees") => {
// rebase/merge restructure, then retry
}
Err(e) => return Err(e),
} Prevention
- Keep review roots on a single line of history; avoid merging independent review branches
- Deduplicate review roots when creating them (check ancestry first)
- Time-travel only to commits inside a single review tree
When it happens
Trigger: Calling `perform_reporting_rebased` (time-travel) targeting a commit whose ancestry includes two review-tree roots that don't nest (e.g. a merge commit joining two independent review branches, each with its own review root).
Common situations: Merging two independently-created review branches into one; a review root created on a branch that was later merged with another review line; accidental duplicate review roots on divergent histories.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- change ID prefix is ambiguous in the default Tix view
- the hidden and visible revisions have no editable fork point
- the revisions have multiple editable fork points
- --to is ambiguous; candidates: travel to one directly with…
- review commits can amend only staged paths
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/4025b452e5b21b37.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/time_travel.rs:924
#[derive(Clone, Debug, Eq, PartialEq)]
struct ReviewTree {
root: ObjectId,
reference: gix::refs::FullName,
}
fn review_tree(
repo: &gix::Repository,
graph: &history::HistoryGraph,
roots: &[ObjectId],
commit: ObjectId,
) -> Result<Option<ReviewTree>> {
let mut nearest = None;
for root in roots.iter().copied().filter(|root| graph.is_ancestor(*root, commit)) {
nearest = match nearest {
None => Some(root),
Some(current) if graph.is_ancestor(current, root) => Some(root),
Some(current) if graph.is_ancestor(root, current) => Some(current),
Some(_) => anyhow::bail!("commit belongs to multiple unrelated review trees"),
};
}
let Some(root) = nearest else { return Ok(None) };
let commit = repo.find_commit(root)?.decode()?.into_owned()?;
let reference = super::review::reference(&commit)?.context("review root lost its review identity")?;
Ok(Some(ReviewTree { root, reference }))
}
#[tracing::instrument(skip_all, fields(review = %review.reference))]
fn save_review_stash(
repository_path: &Path,
bare: bool,
workdir: &Path,
review: &ReviewTree,
) -> Result<Option<super::stash::SavedStash>> {
if !super::review::is_dirty(workdir)? {
return Ok(None);
}View on GitHub (pinned to e73179060b)