gitbutlerapp/gitbutler · info
programs was just checked to be non-empty
Error message
programs was just checked to be non-empty
What it means
`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.
Source
Thrown at crates/but/src/command/open.rs:154
.arg_value(program_id),
)
})?
}
None => {
let program_specs = list_program_specs_for_openable(&to_open);
match TryInto::<[ProgramSpec; 1]>::try_into(program_specs) {
Ok([program_spec]) => program_spec,
Err(mut programs) => {
if !programs.is_empty() {
if let Some(mut input) = out.prepare_for_terminal_input() {
let options = NonEmpty::from_vec(
programs
.iter()
.enumerate()
.map(|(idx, program)| (&program.id, idx))
.collect::<Vec<_>>(),
)
.expect("programs was just checked to be non-empty");
let Some(selection) = input.prompt_select(
"Could not automatically choose program. Choose one to open with",
&options,
)?.copied()
else {
return Err(bad_input("No program picked").into());
};
programs.swap_remove(selection)
} else {
let program_ids =
programs.into_iter().map(|program| program.id).join(", ");
return Err(bad_input(format!(
"Could not automatically choose program. Found {program_ids}"
))
.hint("Specify a program with `--program-id`")
.into());View on GitHub (pinned to 2497b8007a)
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
Example fix
// before — guard and conversion separated by an expect
if !programs.is_empty() {
let options = NonEmpty::from_vec(...).expect("programs was just checked to be non-empty");
...
}
// after — branch on the constructor so the type enforces the check
if let Some(mut programs_ne) = NonEmpty::from_vec(std::mem::take(&mut programs)) {
// interactive selection using programs_ne
} else {
// empty → non-interactive fallback path
} Defensive patterns
Strategy: validation
Validate before calling
// before invoking the open flow, validate the candidate set once
let programs: Vec<ProgramSpec> = collect_program_specs(...);
if programs.is_empty() {
return Err(anyhow::anyhow!("no program available to open this workspace"));
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- deduping a NonEmpty will never make it empty
- target OID must exist when ahead calculation is enabled
- target OID must exist when merge check is enabled
- classified branches are guaranteed to be non-empty
- committed files being non-empty means paths are non-empty
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/88b6047bb1714162.
Report an issue: GitHub.