gitbutlerapp/gitbutler · error
Can only check out local branches under refs/heads, got
Error message
Can only check out local branches under refs/heads, got '{}' What it means
`Transaction::checkout` records a final branch checkout to materialize at commit time, but only local branches (refs/heads/...) can be checked out. If the FullNameRef passed has any other category (remote-tracking, tags, notes, custom refs), the ensure! fails with this message showing the ref name.
Solutions
- Pass a refs/heads/... FullNameRef, e.g. convert refs/remotes/origin/main to its local branch first
- Create or verify a local branch exists at the desired commit and check that out instead
- If a tag is the target, check out a local branch at the tag's commit rather than the tag ref
- Validate branch.category() == Some(Category::LocalBranch) before calling checkout
Example fix
// before tx.checkout(&"refs/remotes/origin/main".try_into()?)?; // after let local: FullName = "refs/heads/main".try_into()?; tx.checkout(&local)?;
Defensive patterns
Strategy: validation
Validate before calling
use gix::refs::{Category, FullNameRef};
fn can_checkout(branch: &FullNameRef) -> bool {
branch.category() == Some(Category::LocalBranch)
}
// call: assert!(can_checkout(&name)); Type guard
fn as_local_branch(name: &FullNameRef) -> Option<&FullNameRef> {
(name.category() == Some(gix::refs::Category::LocalBranch)).then_some(name)
} Try / catch
match tx.checkout(branch) {
Err(e) if format!("{e:#}").starts_with("Can only check out local branches") => {
let local = to_local_branch(branch)?; // e.g. origin/main -> refs/heads/main
tx.checkout(&local)?;
}
r => r?,
} Prevention
- Normalize remote-tracking names to local branches before checkout
- Never pass tag or note ref names to checkout APIs
- Validate ref category at the boundary where user input is parsed
When it happens
Trigger: Calling transaction.checkout() with a fully-qualified ref like refs/remotes/origin/main, refs/tags/v1.0, or refs/notes/commits instead of a name under refs/heads.
Common situations: Resolving a branch via gix from remote-tracking ref names; trying to 'checkout a tag' through the transaction API; passing a user-supplied ref name without normalizing to the local branch form.
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
- Can only check out local branches under refs/heads or…
- Branch name ' ' collides with existing branch
- Can only delete local branches under refs/heads, got
- Can only rename local branches under refs/heads, got
- Cannot add the target
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/e92a5a613d38e26a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-transaction/src/lib.rs:377
editor,
commit_mappings.map(source),
changes,
context_lines,
)?;
let new_commit = rebase.lookup_commit(commit_selector)?;
Ok((new_commit, MaterializeWithoutCheckout::No, rebase))
})
}
/// Check out `branch` when the transaction commits.
///
/// The checkout is deferred until all in-memory commits and reference changes have been
/// materialized. Consequently, operations after this call still observe the checkout from
/// before the transaction. Calling this more than once replaces the previously requested final
/// checkout.
pub fn checkout(&mut self, branch: &FullNameRef) -> anyhow::Result<()> {
anyhow::ensure!(
branch.category() == Some(gix::refs::Category::LocalBranch),
"Can only check out local branches under refs/heads, got '{}'",
branch.as_bstr()
);
resolve_checkout_target(self.repo(), branch)?;
self.request_materialization(MaterializeWithoutCheckout::No)?;
self.inner.pending_checkout = Some(branch.to_owned());
Ok(())
}
pub fn remove_reference(&mut self, ref_name: &FullNameRef) -> anyhow::Result<()> {
self.rebase(|mut editor, _| {
let ref_selector = editor.select_reference(ref_name)?;
let must_disconnect_child = 'must_disconnect: {
let Some(target_selector) = editor.target_selector() else {View on GitHub (pinned to 58e5313667)