gitbutlerapp/gitbutler · error

Can only delete local branches under refs/heads, got

Error message

Can only delete local branches under refs/heads, got '{}'

What it means

`branch_remove_with_perm` refuses to delete any reference whose category is not a local branch under `refs/heads`. GitButler branch deletion only operates on local branches; remote-tracking refs, tags, and other ref categories are rejected up front with the offending full refname in the message.

Solutions

  1. Pass a fully-qualified local branch ref, e.g. `refs/heads/my-branch`
  2. Strip remote prefixes and delete via the appropriate remote-deletion API instead
  3. Verify `ref_name.category()` is `LocalBranch` before calling

Example fix

// before
ctx.branch_remove("refs/remotes/origin/feature")?;
// after
ctx.branch_remove("refs/heads/feature")?; // local branch only
Defensive patterns

Strategy: validation

Validate before calling

function isLocalBranch(ref: string): boolean {
  return ref.startsWith("refs/heads/");
}
if (!isLocalBranch(refName)) throw new Error(`refuse to delete non-local branch: ${refName}`);

Type guard

function isLocalBranchRef(ref: string): ref is `refs/heads/${string}` {
  return ref.startsWith("refs/heads/");
}

Try / catch

try {
  await api.branchRemove(ref);
} catch (e) {
  if (String(e).includes("Can only delete local branches under refs/heads")) {
    console.warn(`${ref} is not a local branch; use remote deletion API`);
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling `branch_remove_with_perm` (or the SDK/server branch-delete endpoint) with a ref like `refs/remotes/origin/foo`, a tag, or another non-`refs/heads` reference.

Common situations: Passing a remote-tracking branch name to the delete API; a client resolving a branch to its full refname of the wrong category; scripting deletes against tags by mistake.

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


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

Appendix: source

Thrown at crates/but-api/src/branch.rs:1111

    #[but_api(crate::json::FullNameBytes)] ref_name: gix::refs::FullName,
) -> anyhow::Result<BranchRemoveResult> {
    let mut guard = ctx.exclusive_worktree_access();
    branch_remove_with_perm(ctx, ref_name, guard.write_permission())
}

/// 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,
    );

View on GitHub (pinned to 58e5313667)