gitbutlerapp/gitbutler · error

Only local branches under refs/heads/ are supported

Error message

Only local branches under refs/heads/ are supported

What it means

normalize_local_branch_ref only accepts bare branch names or full refs under refs/heads/. A ref under refs/ but outside refs/heads/ (e.g. refs/remotes/..., refs/tags/...) cannot denote a local branch, so it is rejected early.

Solutions

  1. Use the bare local branch name (e.g. `main`) instead of a full ref under refs/remotes or refs/tags.
  2. If you must pass a full ref, ensure it starts with refs/heads/.
  3. Strip the remote prefix and create a local tracking branch if needed.

Example fix

// before
but branch update refs/remotes/origin/main --to abc123
// after
but branch update main --to abc123
Defensive patterns

Strategy: validation

Validate before calling

if (ref.startsWith('refs/') && !ref.startsWith('refs/heads/')) {
  throw new Error(`Only refs under refs/heads/ are accepted; got '${ref}'`);
}

Prevention

When it happens

Trigger: Calling `but branch update` with an argument starting with `refs/` but not `refs/heads/`, such as `refs/remotes/origin/main` or `refs/tags/v1.0`.

Common situations: Copy-pasting full ref names from git tooling output; scripts built around remote-tracking refs; passing tags by full ref path.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at crates/but/src/command/branch/update.rs:160

        Some(Category::LocalBranch) => Ok(reference.name),
        Some(Category::RemoteBranch) => {
            bail!(
                "Expected a local branch, but '{}' is a remote-tracking branch",
                reference.name.shorten()
            )
        }
        _ => bail!(
            "Expected a local branch, but '{}' is not under refs/heads/",
            reference.name.shorten()
        ),
    }
}

fn normalize_local_branch_ref(branch: &str) -> anyhow::Result<FullName> {
    let full = if branch.starts_with("refs/heads/") {
        branch.to_owned()
    } else if branch.starts_with("refs/") {
        bail!("Only local branches under refs/heads/ are supported");
    } else {
        format!("refs/heads/{branch}")
    };
    FullName::try_from(full).map_err(|err| anyhow::anyhow!("Invalid branch name: {err}"))
}

fn output_apply_result(
    branch_ref: &FullNameRef,
    divergence: &but_workspace::branch::IntegrationDivergenceDisplay,
    dry_run: bool,
    verbose: bool,
    result: IntegrateBranchResult,
    out: &mut OutputChannel,
) -> anyhow::Result<()> {
    if let Some(out) = out.for_human() {
        if dry_run {
            write!(
                out,

View on GitHub (pinned to 58e5313667)