gitbutlerapp/gitbutler · error · anyhow::Error

No merge-base found between '{ref_name}' and its tracking br

Error message

No merge-base found between '{ref_name}' and its tracking branch '{upstream_ref_name}'

What it means

The divergence computation walks the upstream's first-parent ancestors and uses a first-parent merge-base search to relate a branch to its tracking branch. If no merge-base exists, the two histories are unrelated and divergence cannot be computed — typically because the upstream was force-pushed to a completely different history, was recreated, or tracks the wrong ref.

Source

Thrown at crates/but-workspace/src/divergence.rs:66

/// preserved parentage consistently within the current operation.
///
/// Returns the local-only selectors, upstream-only selectors, and the selector
/// for their shared merge base.
pub(crate) fn get_commits_until_merge_base<'a, M: RefMetadata>(
    ref_name: &'a gix::refs::FullNameRef,
    upstream_ref_name: Cow<'a, gix::refs::FullNameRef>,
    editor: &Editor<'_, '_, M>,
) -> Result<BranchMergeBaseCommits> {
    let local_tip = tip_for_ref(editor, ref_name, editor.repo())
        .with_context(|| format!("Could not determine tip commit for '{ref_name}'"))?;
    let upstream_tip = tip_for_ref(editor, upstream_ref_name.as_ref(), editor.repo())
        .with_context(|| {
            format!("Could not determine tip commit for upstream '{upstream_ref_name}'")
        })?;
    let upstream_ancestor_ids = traverse_pick_ancestor_ids(editor, upstream_tip)?;
    let merge_base = find_first_parent_merge_base(editor, local_tip, &upstream_ancestor_ids)?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "No merge-base found between '{ref_name}' and its tracking branch '{upstream_ref_name}'"
            )
        })?;
    let merge_base_selector = editor.select_commit(merge_base)?;
    let local_commits = first_parent_path_until(editor, local_tip, |selector| {
        editor.lookup_pick(*selector).ok() == Some(merge_base)
    })?
    .into_iter()
    .take_while(|selector| *selector != merge_base_selector)
    .collect::<Vec<_>>();
    let upstream_commits = first_parent_path_until(editor, upstream_tip, |selector| {
        editor.lookup_pick(*selector).ok() == Some(merge_base)
    })?
    .into_iter()
    .take_while(|selector| *selector != merge_base_selector)
    .collect::<Vec<_>>();
    Ok(BranchMergeBaseCommits {
        local_commits,

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Fetch and inspect: git merge-base <branch> <branch>@{upstream} to confirm unrelated histories
  2. If the upstream was legitimately rewritten, re-target or re-create the branch on top of the new upstream (e.g. re-integrate/anchor onto the new tip)
  3. Fix wrong tracking config: git branch --set-upstream-to=<correct-remote-branch>
  4. If the old upstream is gone, remove the upstream association for the branch

Example fix

# before — upstream rewritten, no shared history
git fetch origin
git merge-base main origin/main   # → empty output (no merge base)
# after — re-anchor the branch onto the new upstream
git rebase --onto origin/main --root my-branch  # or re-create branch from new upstream
Defensive patterns

Strategy: validation

Validate before calling

// before computing divergence, confirm the two tips share history
let local = tip_for_ref(editor, ref_name, repo)?;
let upstream = tip_for_ref(editor, upstream_ref_name, repo)?;
ensure!(find_first_parent_merge_base(editor, local, &traverse_pick_ancestor_ids(editor, upstream)?)?.is_some(),
    "unrelated histories — fix tracking or re-anchor first");

Type guard

fn has_merge_base(editor: &Editor<'_, '_, M>, local: gix::Id, upstream: gix::Id) -> bool {
    traverse_pick_ancestor_ids(editor, upstream).ok()
        .and_then(|anc| find_first_parent_merge_base(editor, local, &anc).ok().flatten())
        .is_some()
}

Try / catch

match compute_divergence(...) {
    Err(e) if e.to_string().contains("No merge-base found") => {
        // treat as unrelated: prompt user to re-integrate onto new upstream or clear tracking
    }
    r => r,
}

Prevention

When it happens

Trigger: Computing divergence for a branch whose upstream ref no longer shares any history: upstream recreated from an orphan root, hard force-push replacing all commits, or a tracking configuration pointing at an unrelated branch (e.g. upstream renamed and a new unrelated branch took the old name).

Common situations: Upstream repo did a history rewrite; someone deleted and recreated the remote branch; .git branch tracking metadata stale after a rename; fork relationships changed.

Related errors


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