gitbutlerapp/gitbutler · error

Remote HEAD for {remote_name} is not symbolic

Error message

Remote HEAD for {remote_name} is not symbolic

What it means

The remote HEAD reference exists but head_ref.target().try_name() returned None, meaning refs/remotes/<remote>/HEAD is a plain ref pointing directly at an object id instead of symbolically aliasing refs/remotes/<remote>/<branch>. GitButler needs the symbolic form to derive the default target branch name, so default_target_setting_if_none bails.

Source

Thrown at crates/but-action/src/lib.rs:158

fn default_target_setting_if_none(ctx: &Context) -> anyhow::Result<()> {
    if ctx.project_meta()?.target_ref.is_some() {
        return Ok(());
    }
    // Lets do the equivalent of `git symbolic-ref refs/remotes/origin/HEAD --short` to guess the default target.

    let repo = ctx.repo.get()?;
    let remote_name = repo
        .remote_default_name(gix::remote::Direction::Push)
        .ok_or_else(|| anyhow::anyhow!("No push remote set or more than one remote"))?
        .to_string();

    let mut head_ref = repo
        .find_reference(&format!("refs/remotes/{remote_name}/HEAD"))
        .map_err(|_| anyhow::anyhow!("No HEAD reference found for remote {remote_name}"))?;
    let target_ref_name = head_ref
        .target()
        .try_name()
        .ok_or_else(|| anyhow::anyhow!("Remote HEAD for {remote_name} is not symbolic"))?
        .to_owned();

    let head_commit = head_ref.peel_to_commit()?;

    ctx.set_project_meta(ProjectMeta {
        target_ref: Some(target_ref_name),
        target_commit_id: Some(head_commit.id),
        push_remote: None,
    })?;
    Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, EnumString, Default)]
#[serde(rename_all = "camelCase")]
pub enum ActionHandler {
    #[default]
    HandleChangesSimple,
}

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Recreate it as a symref: git update-ref -d refs/remotes/<remote>/HEAD && git remote set-head <remote> --auto.
  2. Or point it at the branch directly: git remote set-head <remote> <branch>.
  3. Or configure GitButler's target branch explicitly so the guess path is skipped.
  4. Check the current form with git symbolic-ref refs/remotes/<remote>/HEAD — it errors on non-symbolic refs.

Example fix

git update-ref -d refs/remotes/origin/HEAD
git remote set-head origin --auto
git symbolic-ref refs/remotes/origin/HEAD  # -> refs/remotes/origin/main
Defensive patterns

Strategy: validation

Validate before calling

fn remote_head_is_symbolic(repo: &gix::Repository, remote: &str) -> bool {
    repo.find_reference(&format!("refs/remotes/{remote}/HEAD"))
        .ok()
        .and_then(|r| r.target().try_name().map(|_| true))
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: default_target_setting_if_none on a repo where refs/remotes/<remote>/HEAD was written as a direct ref — e.g. via `git update-ref refs/remotes/origin/HEAD <sha>`, or created by mirror/backup tooling that materializes symrefs as plain refs.

Common situations: Repos restored from bundles or mirror clones where symrefs became direct refs; scripts pinning remote HEAD with update-ref; migration tooling copying refs verbatim.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/8a5b4832d128a246. Report an issue: GitHub.