gitbutlerapp/gitbutler · error

Expected a local branch, but

Error message

Expected a local branch, but '{}' is not under refs/heads/

What it means

After finding the reference, resolve_local_branch verifies it is under refs/heads/. Any reference that is neither a local nor remote branch (e.g. a tag, stash ref, or notes ref) is rejected because `but branch update` supports only local branches.

Solutions

  1. Pass a local branch name that lives under refs/heads/ (check with `git branch --list <name>`).
  2. If you meant to operate on a tag, use the appropriate git/tag command instead of `but branch update`.
  3. Create a local branch if the target should be one: `git switch -c <name>`.

Example fix

// before
but branch update v1.0 --to abc123
// after
but branch update release-1.0 --to abc123
Defensive patterns

Strategy: validation

Validate before calling

const isBranch = execSync(`git show-ref --verify --quiet refs/heads/${name} && echo yes || echo no`).toString().trim() === 'yes';
if (!isBranch) throw new Error(`'${name}' is not a local branch under refs/heads/`);

Prevention

When it happens

Trigger: Passing something like a tag name (`v1.0`) or other non-branch ref to `but branch update`; try_find_reference matches the tag, whose category is not LocalBranch or RemoteBranch, hitting the fallthrough bail.

Common situations: Confusing tags with branches; passing full refs of other categories (refs/stash, refs/notes/*) copied from tooling 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/e72aac67e4780177. Report an issue: GitHub.

Appendix: source

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

    }
}

fn resolve_local_branch(ctx: &but_ctx::Context, branch: &str) -> anyhow::Result<FullName> {
    let candidate = normalize_local_branch_ref(branch)?;
    let repo = ctx.repo.get()?;
    let Some(reference) = repo.try_find_reference(candidate.as_ref())? else {
        bail!("Local branch '{}' not found", candidate.shorten());
    };
    let reference = reference.detach();
    match reference.name.category() {
        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(

View on GitHub (pinned to 58e5313667)