{"record":{"id":"299aa050b2e14da5","repo":"gitbutlerapp/gitbutler","slug":"committed-files-being-non-empty-means-paths-are-no","errorCode":null,"errorMessage":"committed files being non-empty means paths are non-empty","messagePattern":"committed files being non-empty means paths are non-empty","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/legacy/discard.rs","lineNumber":365,"sourceCode":"            for CommittedFileId {\n                commit_id,\n                path,\n                change_id: _,\n            } in tail\n            {\n                if commit_id != source.commit_id {\n                    return Err(\n                        bad_input(\"All committed files must come from the same commit\")\n                            .arg_name(\"<CHANGES>\")\n                            .hint(\"Discard committed files from each commit separately\")\n                            .into(),\n                    );\n                }\n                paths.push(path);\n            }\n            let paths = paths.into_iter().unique().collect();\n            let paths = NonEmpty::from_vec(paths)\n                .expect(\"committed files being non-empty means paths are non-empty\");\n            Ok(DiscardOperation::CommittedFiles { source, paths })\n        }\n        ClassifiedDiscardables::Uncommitted => {\n            Ok(DiscardOperation::Uncommitted(UncommittedSelection::All))\n        }\n        ClassifiedDiscardables::UncommittedChanges(changes) => Ok(DiscardOperation::Uncommitted(\n            UncommittedSelection::Changes(Box::new(changes)),\n        )),\n    }\n}\n\npub fn run(\n    ctx: &mut Context,\n    meta: &mut impl RefMetadata,\n    perm: &mut RepoExclusive,\n    operation: DiscardOperation,\n    oplog_operation_kind: OperationKind,\n) -> anyhow::Result<(DiscardOutcome, WorkspaceState)> {","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/command/legacy/discard.rs#L347-L383","documentation":"Invariant panic while building DiscardOperation::CommittedFiles: classification guarantees at least one committed file (the NonEmpty head supplies the source commit), and the loop pushes one path per file plus the head's path, so the collected path list cannot be empty. NonEmpty::from_vec returning None therefore signals an upstream contract break - for instance a refactor that filters paths between classification and this point.","triggerScenarios":"A future edit drops paths silently (a filter, a renamed field) before the from_vec call while still selecting the CommittedFiles variant; the head supplying the commit but yielding no path.","commonSituations":"Refactors adding path filtering or normalization between classification and operation construction; not reachable through valid CLI input as written.","solutions":["Report the exact 'but discard ...' invocation - this path is not reachable via well-formed input","Maintainer: thread the classification's NonEmpty through instead of rebuilding it from a Vec","Convert the expect into an internal-error result to aid bug reports"],"exampleFix":"// before\nlet paths = NonEmpty::from_vec(paths)\n    .expect(\"committed files being non-empty means paths are non-empty\");\n\n// after\nlet paths = NonEmpty::from_vec(paths)\n    .ok_or_else(|| anyhow::anyhow!(\"internal: committed-file selection lost all paths\"))?;","handlingStrategy":"validation","validationCode":"// Guard the committed-files mapping inputs\ndebug_assert!(!committed_files.tail.is_empty() || committed_files.head.path.is_some(),\n    \"CommittedFiles classification carries at least one path\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not insert path filtering between classification and operation construction","Preserve the NonEmpty structure of committed files through the mapping instead of rebuilding from Vec","Add unit tests constructing CommittedFiles classifications and asserting the produced operation"],"tags":["rust","panic","nonempty","invariant","discard","committed-files"],"backgroundTag":"nonempty-invariant-violation","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}