gitbutlerapp/gitbutler · error · anyhow::Error
Invalid branch name: {err}
Error message
Invalid branch name: {err} What it means
normalize_local_branch_ref turns a user-supplied branch string into a gix::refs::FullName: it rejects anything under refs/ but outside refs/heads/, prefixes bare names with refs/heads/, and then FullName::try_from validates the result against Git's reference-name rules. The error wraps gix's validation failure, meaning the constructed full name contains characters or patterns Git forbids (space, ~, ^, :, ?, *, [, \, control chars, '..' sequences, leading/trailing issues, '.lock' suffix, etc.).
Source
Thrown at crates/but/src/command/branch/update.rs:164
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,
"{}",
format_dry_run(divergence, branch_ref, &result, verbose)
)?;
} else {View on GitHub (pinned to caf1f223d3)
Solutions
- Inspect the exact name in the error's '{err}' from gix; it names the first invalid character or pattern — remove it.
- Quote the argument and strip whitespace: `but branch update "$(printf '%s' "$name" | tr -d '[:space:]')"`.
- Replace forbidden characters with '-' or '_' (spaces, ~ ^ : ? * [ \ .., leading '-', trailing '.lock', trailing '/').
- If you meant a non-branch ref (tag, remote ref), note only refs/heads/* local branches are supported here — retarget the command.
Example fix
# before $ but branch update "feature x" Error: Invalid branch name: ... # after $ but branch update "feature-x"
Defensive patterns
Strategy: validation
Validate before calling
// Validate a branch name before calling update
fn is_valid_branch_name(name: &str) -> bool {
if name.starts_with("refs/") && !name.starts_with("refs/heads/") { return false; }
let full = if name.starts_with("refs/heads/") { name.to_owned() } else { format!("refs/heads/{name}") };
gix::refs::FullName::try_from(full).is_ok()
} Type guard
fn valid_full_name(full: &str) -> Option<gix::refs::FullName> {
gix::refs::FullName::try_from(full).ok()
} Prevention
- Sanitize user input: reject ' ', '~', '^', ':', '?', '*', '[', '\\', '..', trailing '/'/'.', '.lock'.
- Trim whitespace/newlines from pasted names before use.
- Quote branch arguments in shell scripts.
When it happens
Trigger: Calling `but branch update <name>` with a branch containing forbidden characters, e.g. 'feature x' (space), 'foo..bar', 'release*', 'branch.lock', a name ending in '/' or '.'; or a copy-pasted name carrying a trailing newline/whitespace; or a ref like refs/tags/v1 passed where only local branches are valid (that hits the separate 'Only local branches under refs/heads/' bail first only for refs/tags — non-refs names go straight to FullName validation).
Common situations: Names pasted from issue trackers or Slack that include spaces or unicode punctuation; shell glob characters unquoted; Windows copy-paste with trailing carriage returns; scripts interpolating empty variables producing 'refs/heads/'.
Related errors
- Could not turn {name:?} into a valid reference name
- Invalid message format
- HTTP Error ${response.statusText}: ${text}
- Invalid section for global key: {key}
- Invalid parents to disconnect: SelectorSet::None is not allo
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/69b8a7be52ee9158.
Report an issue: GitHub.