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

  1. Exclude technical branch names (check `but_branches::is_technical_branch_name`) from deletion logic
  2. Operate on user branches only; let GitButler manage its own internal refs
  3. 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

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


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 rather

View on GitHub (pinned to 58e5313667)