gitbutlerapp/gitbutler · error

Ambiguous branch ' ', matches multiple items

Error message

Ambiguous branch '{branch_id}', matches multiple items

What it means

When the branch identifier passed to `but merge` resolves through the `IdMap` to more than one item, the command cannot pick a target safely and bails with this message. `parse_using_context` performs fuzzy/prefix-style matching over workspace items (branches, commits, files, etc.), so a short or generic identifier can match several entries; the CLI refuses to guess. The input is ambiguous, not missing.

Solutions

  1. Re-run with a longer, more specific identifier: the full CLI ID (e.g. `br#3`) or the exact branch name.
  2. Run `but branch list` to see all matching items and disambiguate by picking the exact ID.
  3. Use a full unique branch name instead of a prefix or fragment.
  4. If two branches genuinely share a name, rename one (`but branch rename`) so identifiers are unique.

Example fix

// before: ambiguous prefix
but merge feature
// error: Ambiguous branch 'feature', matches multiple items
// after: exact branch name or full id
but merge feature-2
// or
but merge br#3
Defensive patterns

Strategy: validation

Validate before calling

// Require an exact, unambiguous branch name before invoking:
const branches = JSON.parse(runBut(['branch', 'list', '--json']).stdout);
const matches = branches.filter(b => b.name === branchId);
if (matches.length !== 1) {
  throw new Error(`Use an exact branch name or full CLI ID; candidates: ${branches.map(b => b.id).join(', ')}`);
}

Try / catch

try {
  runBut(['merge', branchId]);
} catch (e) {
  if (e.stderr.includes('matches multiple items')) {
    // retry with a full branch id resolved from the listing
    runBut(['merge', fullBranchIdFromListing]);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling `but merge <branch_id>` where `id_map.parse_using_context` returns `resolved_ids.len() > 1`, e.g. a short prefix that matches multiple branch IDs, a branch name that collides with another item type or another branch, or a fragment matched by several workspace segments.

Common situations: Typing a shortened ID prefix like `br` or `b1` that matches more than one item. Two branches with similar names (e.g. `feature` and `feature-2`) where a partial name matches both. Using a commit/file ID in a context where it also matches a branch. Large workspaces where short positional IDs wrap around.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/cd1b7baf89276017. Report an issue: GitHub.

Appendix: source

Thrown at crates/but/src/command/legacy/merge/mod.rs:48

        let mut guard = ctx.exclusive_worktree_access();

        {
            let (_repo, ws, _db) = ctx.workspace_and_db_with_perm(guard.read_permission())?;
            if !ws.kind.has_managed_ref() {
                bail!(
                    "`but merge` requires an active GitButler workspace (`gitbutler/workspace`). \
                     Switch into the workspace and try again."
                );
            }
        }

        let id_map = IdMap::new_from_context(ctx, guard.read_permission())?;
        let resolved_ids = id_map.parse_using_context(branch_id, ctx)?;
        if resolved_ids.is_empty() {
            bail!("Could not find branch: {branch_id}");
        }
        if resolved_ids.len() > 1 {
            bail!("Ambiguous branch '{branch_id}', matches multiple items");
        }

        let branch_name = match &resolved_ids[0] {
            CliId::Branch(branch) => branch.name.clone(),
            other => bail!("Expected a branch ID, got {}", other.kind_for_humans()),
        };

        let base_branch =
            but_api::legacy::virtual_branches::get_base_branch_data(ctx, guard.write_permission())?
                .ok_or_else(|| anyhow::anyhow!("No base branch configured"))?;
        (branch_name, base_branch)
    };

    // Display strings for the prompt and the final report. The API recomputes the target/remote
    // configuration internally; the CLI only needs these names to describe what's about to happen.
    let target_branch_name = base_branch.short_name.clone();
    let push_remote_name = if base_branch.push_remote_name.is_empty() {
        base_branch.remote_name.clone()

View on GitHub (pinned to 58e5313667)