nikivdev/code · error

detached HEAD (checkout a branch first)

Error message

detached HEAD (checkout a branch first)

What it means

Raised by current_branch in src/push.rs when `git rev-parse --abbrev-ref HEAD` succeeds but returns empty or the literal string "HEAD", which git does when the repository is in detached HEAD state. The mirror-push flow requires a named branch to push and refuse to guess one, so it aborts asking the user to check out a branch.

Source

Thrown at src/push.rs:343

    if !output.status.success() {
        bail!("not inside a git repository");
    }
    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
    Ok(PathBuf::from(path))
}

fn current_branch(repo_root: &Path) -> Result<String> {
    let output = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .current_dir(repo_root)
        .output()
        .context("failed to read current branch")?;
    if !output.status.success() {
        bail!("git rev-parse --abbrev-ref HEAD failed");
    }
    let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if name.is_empty() || name == "HEAD" {
        bail!("detached HEAD (checkout a branch first)");
    }
    Ok(name)
}

pub(crate) fn git_capture_in(repo_root: &Path, args: &[&str]) -> Result<String> {
    let output = Command::new("git")
        .args(args)
        .current_dir(repo_root)
        .output()
        .with_context(|| format!("failed to run git {}", args.join(" ")))?;
    if !output.status.success() {
        bail!("git {} failed", args.join(" "));
    }
    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

pub(crate) fn git_run_in(repo_root: &Path, args: &[&str]) -> Result<()> {
    git_run_in_with_env(repo_root, args, &[])

View on GitHub (pinned to a747e741ae)

Solutions

  1. Run `git checkout <branch>` (e.g. `git checkout main`) to attach HEAD to a branch
  2. If you made commits while detached, create a branch first: `git switch -c my-branch` then push
  3. Finish or abort any in-progress rebase (`git rebase --continue` / `git rebase --abort`)

Example fix

// before
HEAD detached at abc1234
$ f push
detached HEAD (checkout a branch first)
// after
$ git checkout main
$ f push
Defensive patterns

Strategy: validation

Validate before calling

let name = String::from_utf8(
    std::process::Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .output()?
        .stdout,
).unwrap().trim().to_string();
if name.is_empty() || name == "HEAD" { eprintln!("detached HEAD; checkout a branch first"); }

Try / catch

match result {
    Err(e) if e.to_string().contains("detached HEAD") => {
        eprintln!("git checkout <branch> then retry");
    }
    Err(e) => return Err(e),
    Ok(v) => Ok(v),
}

Prevention

When it happens

Trigger: The repository is in detached HEAD state (checked out a commit/tag directly, or a rebase/checkout of a remote ref) and the push command is run.

Common situations: Checking out a specific commit SHA or tag to inspect history, then forgetting to return to a branch; CI checkouts that default to detached HEAD; mid-rebase states.

Related errors


AI-assisted analysis of nikivdev/code@a747e741ae (2026-09-01). Data as JSON: /api/errors/7e1f5dd97c35cb66. Report an issue: GitHub.