gitbutlerapp/gitbutler · error

Can only rename local branches under refs/heads, got

Error message

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

What it means

`branch_rename` (mirroring `branch_checkout_with_perm`) only renames local branches under `refs/heads`. The guard exists so an SDK/server caller cannot pass e.g. `refs/remotes/origin/foo` and have GitButler create a local branch while deleting the remote-tracking ref. Non-local-branch refs are rejected with the offending refname in the message.

Solutions

  1. Pass a `refs/heads/...` full name to the rename API
  2. If the intent is to rename a remote branch, rename the local branch and push the new name, then delete the old remote ref
  3. Check the ref category before invoking rename

Example fix

// before
await api.renameBranch("refs/remotes/origin/old-name", "new-name");
// after
await api.renameBranch("refs/heads/old-name", "new-name");
await api.pushBranch("new-name");
await api.deleteRemoteBranch("origin/old-name");
Defensive patterns

Strategy: validation

Validate before calling

if (!ref.startsWith("refs/heads/")) throw new Error(`rename requires a local branch ref, got: ${ref}`);

Type guard

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

Try / catch

try {
  await api.branchRename(ref, newName);
} catch (e) {
  if (String(e).includes("Can only rename local branches under refs/heads")) {
    console.warn("rename a local branch, then push/delete on the remote");
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling branch rename with a remote-tracking ref (`refs/remotes/origin/x`), a tag, or any ref whose `category()` is not `LocalBranch`.

Common situations: Renaming from a UI bound to a remote-tracking branch; scripts iterating all refs including remotes; SDK callers passing raw ref names from `git branch -r` output.

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/05a6056164ce4ced. Report an issue: GitHub.

Appendix: source

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

    branch_rename_with_perm(ctx, ref_name, new_name, guard.write_permission())
}

/// Rename the local branch `ref_name` to `new_name` under caller-held exclusive
/// repository access and record an oplog snapshot on success.
///
/// See [`branch_rename()`] for the higher-level behaviour.
pub fn branch_rename_with_perm(
    ctx: &mut but_ctx::Context,
    ref_name: gix::refs::FullName,
    new_name: String,
    perm: &mut RepoExclusive,
) -> anyhow::Result<BranchRenameResult> {
    // This only renames local branches. Reject anything else up front (mirroring
    // `branch_checkout_with_perm`) so an SDK/server caller can't pass e.g.
    // `refs/remotes/origin/foo` and have us create a local branch while deleting the
    // remote-tracking ref.
    if ref_name.category() != Some(gix::refs::Category::LocalBranch) {
        bail!(
            "Can only rename local branches under refs/heads, got '{}'",
            ref_name.as_bstr()
        );
    }

    // Normalize the requested name into a valid local branch reference. This is the non-legacy
    // counterpart to the old `normalize_branch_name`, so any caller can pass a raw, human-entered
    // name and get a valid ref.
    let normalized = but_core::branch::normalize_short_name(new_name.as_str())?;
    let new_ref = gix::refs::Category::LocalBranch.to_full_name(normalized.as_bstr())?;

    // Renaming onto the same name is a no-op that still returns the current view.
    if ref_name == new_ref {
        let mut meta = ctx.meta()?;
        let (repo, ws, mut db) = ctx.workspace_mut_and_db_mut_with_perm(perm)?;
        repo.find_reference(ref_name.as_ref())
            .with_context(|| format!("Branch '{}' does not exist", ref_name.shorten()))?;
        let workspace = WorkspaceState::from_workspace_with_db(

View on GitHub (pinned to 58e5313667)