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
- Run `but branch list` (or `but status`) to see current branches and their exact CLI IDs/names, then use a valid identifier.
- Re-resolve the identifier at time of use: prefer stable branch names over positional CLI IDs like `br#2` in scripts.
- Create or apply the branch into the workspace if it exists in Git but is not a workspace branch.
- 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
- Fetch fresh branch IDs (but branch list) immediately before merging; never cache CLI IDs across workspace changes.
- Prefer stable branch names over positional IDs like br#2 in scripts.
- Ensure the branch is applied in the workspace, not just present as a plain Git ref.
- Confirm you are in the intended repository before resolving IDs.
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
- Ambiguous branch ' ', matches multiple items
- Expected a branch ID, got
- Ambiguous branch ' ', matches multiple items
- Branch ' ' not found
- Branch ' ' not found when checking for conflicts
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)