{"record":{"id":"05a6056164ce4ced","repo":"gitbutlerapp/gitbutler","slug":"can-only-rename-local-branches-under-refs-heads-g","errorCode":null,"errorMessage":"Can only rename local branches under refs/heads, got '{}'","messagePattern":"Can only rename local branches under refs/heads, got '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-api/src/branch.rs","lineNumber":1273,"sourceCode":"    branch_rename_with_perm(ctx, ref_name, new_name, guard.write_permission())\n}\n\n/// Rename the local branch `ref_name` to `new_name` under caller-held exclusive\n/// repository access and record an oplog snapshot on success.\n///\n/// See [`branch_rename()`] for the higher-level behaviour.\npub fn branch_rename_with_perm(\n    ctx: &mut but_ctx::Context,\n    ref_name: gix::refs::FullName,\n    new_name: String,\n    perm: &mut RepoExclusive,\n) -> anyhow::Result<BranchRenameResult> {\n    // This only renames local branches. Reject anything else up front (mirroring\n    // `branch_checkout_with_perm`) so an SDK/server caller can't pass e.g.\n    // `refs/remotes/origin/foo` and have us create a local branch while deleting the\n    // remote-tracking ref.\n    if ref_name.category() != Some(gix::refs::Category::LocalBranch) {\n        bail!(\n            \"Can only rename local branches under refs/heads, got '{}'\",\n            ref_name.as_bstr()\n        );\n    }\n\n    // Normalize the requested name into a valid local branch reference. This is the non-legacy\n    // counterpart to the old `normalize_branch_name`, so any caller can pass a raw, human-entered\n    // name and get a valid ref.\n    let normalized = but_core::branch::normalize_short_name(new_name.as_str())?;\n    let new_ref = gix::refs::Category::LocalBranch.to_full_name(normalized.as_bstr())?;\n\n    // Renaming onto the same name is a no-op that still returns the current view.\n    if ref_name == new_ref {\n        let mut meta = ctx.meta()?;\n        let (repo, ws, mut db) = ctx.workspace_mut_and_db_mut_with_perm(perm)?;\n        repo.find_reference(ref_name.as_ref())\n            .with_context(|| format!(\"Branch '{}' does not exist\", ref_name.shorten()))?;\n        let workspace = WorkspaceState::from_workspace_with_db(","sourceCodeStart":1255,"sourceCodeEnd":1291,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-api/src/branch.rs#L1255-L1291","documentation":"`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.","triggerScenarios":"Calling branch rename with a remote-tracking ref (`refs/remotes/origin/x`), a tag, or any ref whose `category()` is not `LocalBranch`.","commonSituations":"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.","solutions":["Pass a `refs/heads/...` full name to the rename API","If the intent is to rename a remote branch, rename the local branch and push the new name, then delete the old remote ref","Check the ref category before invoking rename"],"exampleFix":"// before\nawait api.renameBranch(\"refs/remotes/origin/old-name\", \"new-name\");\n// after\nawait api.renameBranch(\"refs/heads/old-name\", \"new-name\");\nawait api.pushBranch(\"new-name\");\nawait api.deleteRemoteBranch(\"origin/old-name\");","handlingStrategy":"validation","validationCode":"if (!ref.startsWith(\"refs/heads/\")) throw new Error(`rename requires a local branch ref, got: ${ref}`);","typeGuard":"function isLocalBranchRef(ref: string): ref is `refs/heads/${string}` {\n  return ref.startsWith(\"refs/heads/\");\n}","tryCatchPattern":"try {\n  await api.branchRename(ref, newName);\n} catch (e) {\n  if (String(e).includes(\"Can only rename local branches under refs/heads\")) {\n    console.warn(\"rename a local branch, then push/delete on the remote\");\n  } else { throw e; }\n}","preventionTips":["Only offer rename in the UI for local branches","Map remote-tracking branches to their local counterpart before renaming","Never pass refs/remotes/* directly into rename endpoints","Document that rename implies local ref surgery, not remote updates"],"tags":["git","branch","rename","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}