gitbutlerapp/gitbutler · error
Expected a branch ID, got
Error message
Expected a branch ID, got {} What it means
`but merge` expects its argument to resolve to a branch (`CliId::Branch`). The match on `resolved_ids[0]` accepts only the `CliId::Branch` variant; any other resolved kind (commit, file, hunk, etc.) is rejected with this message, which includes the human-readable kind via `kind_for_humans()`. The identifier resolved successfully but points at a workspace item that cannot be landed.
Solutions
- Look up the branch containing the item with `but branch list` and pass its branch ID/name instead.
- If you have a commit ID, find its parent branch (e.g. via `but log` or `but branch list`) and merge that branch.
- Remember `but merge` lands whole branches onto the target; use the appropriate command (e.g. `but commit` operations) for commit-level actions.
- Use the full branch CLI ID (`br#N`) copied from `but branch list` output to avoid resolving to other item kinds.
Example fix
// before: commit id passed to merge but merge cm#2 // error: Expected a branch ID, got a commit // after: merge the branch that owns the commit but branch list # feature-x is br#1 but merge feature-x
Defensive patterns
Strategy: validation
Validate before calling
// Ensure the identifier is a branch before calling but merge:
const branches = JSON.parse(runBut(['branch', 'list', '--json']).stdout);
if (!branches.some(b => b.id === id || b.name === id)) {
throw new Error(`${id} is not a branch ID; but merge only accepts branch identifiers.`);
} Type guard
function isBranchId(id, branches) {
return branches.some(b => b.id === id || b.name === id);
} Try / catch
try {
runBut(['merge', id]);
} catch (e) {
if (e.stderr.startsWith('Expected a branch ID, got')) {
// id resolved to a commit/file/hunk; map it to its owning branch and retry
runBut(['merge', owningBranchName]);
} else {
throw e;
}
} Prevention
- Only pass IDs copied from branch listings (but branch list) to but merge.
- Do not paste commit IDs (cm#N) from but log into but merge.
- Remember but merge operates on whole branches; use commit-level commands for commits.
- When automating, validate the id against the branch list before invoking.
When it happens
Trigger: Calling `but merge <id>` with an identifier that resolves to exactly one non-branch item: a commit ID (e.g. from `but log`), a file or hunk ID, or a workspace/target reference. `parse_using_context` returns a single result, but it is not `CliId::Branch`, so the `other =>` bail arm fires.
Common situations: Copy-pasting a commit ID from `but log` output into `but merge` instead of a branch ID. Selecting a file ID from TUI output when intending to merge its branch. Confusion between `but merge` (lands branches) and other commands that accept commit IDs.
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
- Ambiguous branch ' ', matches multiple items
- Could not find branch
- Ambiguous branch ' ', matches multiple items
- `but land` requires an active GitButler workspace…
- `but merge` requires an active GitButler workspace…
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/989b7ecaa010248a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but/src/command/legacy/merge/mod.rs:53
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()
} else {
base_branch.push_remote_name.clone()
};
let target_display = format!("{push_remote_name}/{target_branch_name}");
View on GitHub (pinned to 58e5313667)