{"record":{"id":"74cd1d44f24bc938","repo":"gitbutlerapp/gitbutler","slug":"classified-branches-are-guaranteed-to-be-non-empty","errorCode":null,"errorMessage":"classified branches are guaranteed to be non-empty","messagePattern":"classified branches are guaranteed to be non-empty","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/legacy/discard.rs","lineNumber":331,"sourceCode":"    let classified = ClassifiedDiscardables::try_from_sources(\n        commit_sources,\n        branch_sources,\n        uncommitted_change_sources,\n        uncommitted_sources,\n        committed_file_sources,\n    )?;\n\n    match classified {\n        ClassifiedDiscardables::Branches(branches) => {\n            let branches = branches\n                .into_iter()\n                .map(|branch| branch.resolve_local_branch_name())\n                .collect::<anyhow::Result<Vec<_>>>()?\n                .into_iter()\n                .unique()\n                .collect();\n            let branches = NonEmpty::from_vec(branches)\n                .expect(\"classified branches are guaranteed to be non-empty\");\n            Ok(DiscardOperation::Branches(branches))\n        }\n        ClassifiedDiscardables::Commits(commits) => Ok(DiscardOperation::Commits(commits)),\n        ClassifiedDiscardables::CommittedFiles(committed_files) => {\n            let NonEmpty { head, tail } = committed_files;\n            let CommittedFileId {\n                commit_id,\n                path,\n                change_id,\n            } = head;\n            let source = CommitId {\n                commit_id,\n                change_id,\n            };\n            let mut paths = vec![path];\n            for CommittedFileId {\n                commit_id,\n                path,","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/command/legacy/discard.rs#L313-L349","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet branches = NonEmpty::from_vec(branches)\n    .expect(\"classified branches are guaranteed to be non-empty\");\n\n// after\nlet branches = NonEmpty::from_vec(branches)\n    .ok_or_else(|| anyhow::anyhow!(\"internal: branch classification produced no branches\"))?;","handlingStrategy":"validation","validationCode":"// Classifier contract: the Branches variant is never empty\ndebug_assert!(!branches.is_empty(), \"Branches classification must be non-empty\");\nlet classified = classify(items)?;\nif let ClassifiedDiscardables::Branches(b) = &classified {\n    assert!(!b.is_empty(), \"classification contract violated\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","panic","nonempty","invariant","discard"],"backgroundTag":"nonempty-invariant-violation","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}