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

  1. Treat as a bug: capture the exact 'but discard ...' invocation and report it upstream
  2. Audit the classifier to guarantee the Branches variant always carries at least one branch
  3. Maintainer: replace the expect with an internal-error result so users get a report instead of a panic
  4. 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

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


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