gitbutlerapp/gitbutler · error
Cannot delete GitButler technical branch
Error message
Cannot delete GitButler technical branch '{}' What it means
`branch_remove_with_perm` protects GitButler's internal technical branches (e.g. `gitbutler/workspace`, oplog/snapshot refs) from deletion. If the shortened refname is classified as a technical branch, deletion is refused because removing it would corrupt workspace state.
Solutions
- Exclude technical branch names (check `but_branches::is_technical_branch_name`) from deletion logic
- Operate on user branches only; let GitButler manage its own internal refs
- If the workspace itself should go away, use the workspace/unassign or project-removal flows instead of deleting the technical branch
Example fix
// before
for b in branches { repo.delete_branch(b)?; }
// after
for b in branches {
if !is_technical_branch_name(b) {
repo.delete_branch(b)?;
}
} Defensive patterns
Strategy: validation
Validate before calling
const TECHNICAL = /^(gitbutler\/)/;
if (TECHNICAL.test(shortName)) throw new Error(`refusing to delete GitButler technical branch: ${shortName}`); Type guard
function isTechnicalBranch(shortName: string): boolean {
return shortName === "gitbutler/workspace" || shortName.startsWith("gitbutler/");
} Try / catch
try {
await api.branchRemove(ref);
} catch (e) {
if (String(e).includes("Cannot delete GitButler technical branch")) {
console.warn("skipping GitButler-internal branch");
} else { throw e; }
} Prevention
- Exclude gitbutler/* names from any bulk branch cleanup
- Treat GitButler internal refs as owned by the tool
- Use workspace/unassign flows instead of deleting the workspace branch
- Check is_technical_branch_name before scripting deletions
When it happens
Trigger: Attempting to delete a branch whose name matches `but_branches::is_technical_branch_name` — typically `gitbutler/workspace` or other `gitbutler/*` internal refs — through the branch-delete API.
Common situations: Scripted cleanup deleting all `refs/heads/*` branches and hitting GitButler internals; users trying to remove the workspace branch manually; bulk deletes from tooling unaware of GitButler's reserved names.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Branch ' ' already exists
- Branch ' ' not found
- Branch ' ' not found when checking for conflicts
- Branch name ' ' collides with existing branch
- branch ' ' no longer exists
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/946da802f26210e5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-api/src/branch.rs:1117
/// Remove the local branch `ref_name`, whether or not it is part of the current
/// workspace projection, under caller-held exclusive repository access and
/// record an oplog snapshot on success.
///
/// See [`branch_remove()`] for the checked-out-reference behaviour and
/// [`but_workspace::branch::remove_reference()`] for the lower-level deletion.
pub fn branch_remove_with_perm(
ctx: &mut but_ctx::Context,
ref_name: gix::refs::FullName,
perm: &mut RepoExclusive,
) -> anyhow::Result<BranchRemoveResult> {
if ref_name.category() != Some(gix::refs::Category::LocalBranch) {
bail!(
"Can only delete local branches under refs/heads, got '{}'",
ref_name.as_bstr()
);
}
if but_branches::is_technical_branch_name(ref_name.shorten()) {
bail!(
"Cannot delete GitButler technical branch '{}'",
ref_name.shorten()
);
}
let maybe_oplog_entry = but_oplog::UnmaterializedOplogSnapshot::from_details_with_perm(
ctx,
SnapshotDetails::new(OperationKind::DeleteBranch)
.with_trailers([Trailer::Name(ref_name.to_string())]),
perm.read_permission(),
DryRun::No,
);
// Decide whether we must move `HEAD` off `ref_name` before deleting it. In an
// ad-hoc workspace the checked-out reference is the projection tip; we only
// allow removing it when it owns no commits and has another named reference
// underneath to land `HEAD` on. This reverses the "create an empty branch
// above the checked-out reference" flow. We look at the projection ratherView on GitHub (pinned to 58e5313667)