gitbutlerapp/gitbutler · error
classified branches are guaranteed to be non-empty
Error message
classified branches are guaranteed to be non-empty
What it means
Invariant panic while mapping classified discardables to an operation: the classifier produced ClassifiedDiscardables::Branches, which by contract carries at least one branch. Each branch then resolves a local name (failures propagate as errors via '?') and dedup keeps at least the first element, so NonEmpty::from_vec can only return None if the upstream contract is broken - not through user input.
Source
Thrown at crates/but/src/command/legacy/discard.rs:331
let classified = ClassifiedDiscardables::try_from_sources(
commit_sources,
branch_sources,
uncommitted_change_sources,
uncommitted_sources,
committed_file_sources,
)?;
match classified {
ClassifiedDiscardables::Branches(branches) => {
let branches = branches
.into_iter()
.map(|branch| branch.resolve_local_branch_name())
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.unique()
.collect();
let branches = NonEmpty::from_vec(branches)
.expect("classified branches are guaranteed to be non-empty");
Ok(DiscardOperation::Branches(branches))
}
ClassifiedDiscardables::Commits(commits) => Ok(DiscardOperation::Commits(commits)),
ClassifiedDiscardables::CommittedFiles(committed_files) => {
let NonEmpty { head, tail } = committed_files;
let CommittedFileId {
commit_id,
path,
change_id,
} = head;
let source = CommitId {
commit_id,
change_id,
};
let mut paths = vec![path];
for CommittedFileId {
commit_id,
path,View on GitHub (pinned to 2497b8007a)
Solutions
- Treat as a bug: capture the exact 'but discard ...' invocation and report it upstream
- Audit the classifier to guarantee the Branches variant always carries at least one branch
- Maintainer: replace the expect with an internal-error result so users get a report instead of a panic
- Add a test asserting classification never yields an empty Branches payload
Example fix
// before
let branches = NonEmpty::from_vec(branches)
.expect("classified branches are guaranteed to be non-empty");
// after
let branches = NonEmpty::from_vec(branches)
.ok_or_else(|| anyhow::anyhow!("internal: branch classification produced no branches"))?; Defensive patterns
Strategy: validation
Validate before calling
// Classifier contract: the Branches variant is never empty
debug_assert!(!branches.is_empty(), "Branches classification must be non-empty");
let classified = classify(items)?;
if let ClassifiedDiscardables::Branches(b) = &classified {
assert!(!b.is_empty(), "classification contract violated");
} Prevention
- Return an explicit 'nothing selected' classification instead of an empty Branches payload
- Keep NonEmpty types flowing from classification to operation construction instead of round-tripping through Vec
- Replace expects with internal-error results in discard mapping so bugs surface as reports
When it happens
Trigger: A refactor lets Branches be built from an empty selection (for example after fuzzy-match filtering removes every candidate) instead of returning a 'nothing selected' classification.
Common situations: Changes to the discard classification/selection code; new discard modes where empty branch sets become representable; not reachable via valid CLI input in the current tree.
Related errors
- committed files being non-empty means paths are non-empty
- changes being non-empty means paths are non-empty
- source branches is already checked to be non-empty
- deduping a NonEmpty will never make it empty
- target OID must exist when ahead calculation is enabled
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/74cd1d44f24bc938.
Report an issue: GitHub.