gitbutlerapp/gitbutler · error

Could not find branch

Error message

Could not find branch: {branch_id}

What it means

`but merge <branch-id>` resolves its branch argument through the `IdMap` (`id_map.parse_using_context`), which maps user-supplied identifiers (CLI IDs like `br#1`, branch names, or other accepted forms) onto workspace items. When resolution returns an empty list, no branch in the current workspace matches the given identifier, and the command bails with this message naming the original input. Nothing has been mutated at this point.

Solutions

  1. Run `but branch list` (or `but status`) to see current branches and their exact CLI IDs/names, then use a valid identifier.
  2. Re-resolve the identifier at time of use: prefer stable branch names over positional CLI IDs like `br#2` in scripts.
  3. Create or apply the branch into the workspace if it exists in Git but is not a workspace branch.
  4. Verify you are in the intended repository (the ID map is per-repository/per-workspace).

Example fix

// before: stale positional id
but merge br#2
// error: Could not find branch: br#2
// after: resolve current id first
but branch list   # shows feature-x as br#1
but merge feature-x
Defensive patterns

Strategy: validation

Validate before calling

// Resolve the identifier against the current workspace before merging:
const branches = JSON.parse(runBut(['branch', 'list', '--json']).stdout);
const match = branches.find(b => b.name === branchId || b.id === branchId);
if (!match) {
  throw new Error(`Branch ${branchId} not in workspace; pick one of: ${branches.map(b => b.id).join(', ')}`);
}

Try / catch

try {
  runBut(['merge', branchId]);
} catch (e) {
  if (e.stderr.startsWith('Could not find branch:')) {
    // refresh workspace state and re-resolve the id, then retry once
    runBut(['branch', 'list']);
    runBut(['merge', resolvedFreshId]);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling `but merge <branch_id>` with an identifier that matches zero items in the current workspace: a branch name that does not exist, a stale CLI ID from a previous workspace state, an ID from a different repository, or a typo. The check is `resolved_ids.is_empty()` immediately after `parse_using_context`.

Common situations: Branch was renamed or deleted before the merge ran. A script caches a CLI ID (which is positional and changes as the workspace changes) and reuses it after the workspace was reordered. The user types a local branch name that exists in Git but is not applied as a workspace branch. Wrong repo checked out.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

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

    // runs here for a friendly message before the prompt; the API enforces it again, along with the
    // bottom-segment, conflicted-commit, and triangular-remote guards, before mutating anything.
    let (branch_name, base_branch) = {
        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.

View on GitHub (pinned to 58e5313667)