{"record":{"id":"88b6047bb1714162","repo":"gitbutlerapp/gitbutler","slug":"programs-was-just-checked-to-be-non-empty","errorCode":null,"errorMessage":"programs was just checked to be non-empty","messagePattern":"programs was just checked to be non-empty","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"info","filePath":"crates/but/src/command/open.rs","lineNumber":154,"sourceCode":"                            .arg_value(program_id),\n                    )\n                })?\n        }\n        None => {\n            let program_specs = list_program_specs_for_openable(&to_open);\n            match TryInto::<[ProgramSpec; 1]>::try_into(program_specs) {\n                Ok([program_spec]) => program_spec,\n                Err(mut programs) => {\n                    if !programs.is_empty() {\n                        if let Some(mut input) = out.prepare_for_terminal_input() {\n                            let options = NonEmpty::from_vec(\n                                programs\n                                    .iter()\n                                    .enumerate()\n                                    .map(|(idx, program)| (&program.id, idx))\n                                    .collect::<Vec<_>>(),\n                            )\n                            .expect(\"programs was just checked to be non-empty\");\n\n                            let Some(selection) = input.prompt_select(\n                                \"Could not automatically choose program. Choose one to open with\",\n                                &options,\n                            )?.copied()\n                            else {\n                                return Err(bad_input(\"No program picked\").into());\n                            };\n\n                            programs.swap_remove(selection)\n                        } else {\n                            let program_ids =\n                                programs.into_iter().map(|program| program.id).join(\", \");\n                            return Err(bad_input(format!(\n                                \"Could not automatically choose program. Found {program_ids}\"\n                            ))\n                            .hint(\"Specify a program with `--program-id`\")\n                            .into());","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/command/open.rs#L136-L172","documentation":"`but open` may find several candidate programs that can open a repo/workspace; it then builds an interactive selector via NonEmpty::from_vec and expects success. NonEmpty::from_vec returns None only for an empty vec, and the branch is entered only when !programs.is_empty() two lines earlier, so as written the panic is unreachable — it exists to document the invariant, not to handle a runtime condition.","triggerScenarios":"Cannot fire in the current code. Would fire only if a future refactor removed entries between the is_empty check and the conversion (e.g. filtering programs inside the branch) or NonEmpty::from_vec changed semantics.","commonSituations":"None for users of shipped builds. Developers encounter it while refactoring the program-selection flow in crates/but/src/command/open.rs and accidentally breaking the guard-to-conversion adjacency.","solutions":["If you see this panic you are on a modified build — diff crates/but/src/command/open.rs against upstream","Make the invariant structural: construct the NonEmpty once and branch on Some/None so emptiness and the type agree (see exampleFix)","Alternatively return a typed error (bad_input) instead of expect if the guard is ever removed","Report upstream with a backtrace if an unmodified build panics"],"exampleFix":"// before — guard and conversion separated by an expect\nif !programs.is_empty() {\n    let options = NonEmpty::from_vec(...).expect(\"programs was just checked to be non-empty\");\n    ...\n}\n\n// after — branch on the constructor so the type enforces the check\nif let Some(mut programs_ne) = NonEmpty::from_vec(std::mem::take(&mut programs)) {\n    // interactive selection using programs_ne\n} else {\n    // empty → non-interactive fallback path\n}","handlingStrategy":"validation","validationCode":"// before invoking the open flow, validate the candidate set once\nlet programs: Vec<ProgramSpec> = collect_program_specs(...);\nif programs.is_empty() {\n    return Err(anyhow::anyhow!(\"no program available to open this workspace\"));\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Prefer types over guards: carry NonEmpty<ProgramSpec> once collected so emptiness is unrepresentable","If refactoring open.rs, keep the is_empty check adjacent to NonEmpty::from_vec or fold both into one match","This expect is unreachable in shipped builds — treat any occurrence as evidence of local modifications"],"tags":["rust","invariant","unreachable","cli","nonempty","open-command"],"backgroundTag":"internal-invariant-panic","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}