gitbutlerapp/gitbutler · error
changes being non-empty means paths are non-empty
Error message
changes being non-empty means paths are non-empty
What it means
Helper-contract panic in the discard command: paths_from_changes maps every DiffSpec to its path and re-wraps the result into NonEmpty. The function documents an implicit precondition - the changes slice must be non-empty - because an empty slice maps to zero paths and trips the expect. All current callers pass validated non-empty selections, so only a new caller passing a possibly-empty slice can trigger it.
Source
Thrown at crates/but/src/command/legacy/discard.rs:545
},
Commits(NonEmpty<CommitId>),
CommittedFiles {
source: CommitId,
paths: NonEmpty<BString>,
changes: Vec<DiffSpec>,
},
Uncommitted {
paths: NonEmpty<BString>,
changes: Vec<DiffSpec>,
},
}
fn paths_from_changes(changes: &[DiffSpec]) -> NonEmpty<BString> {
let paths = changes
.iter()
.map(|change| change.path.clone())
.collect::<Vec<_>>();
NonEmpty::from_vec(paths).expect("changes being non-empty means paths are non-empty")
}
View on GitHub (pinned to 2497b8007a)
Solutions
- Maintainer: change the signature to accept NonEmpty<DiffSpec> (or &NonEmpty<DiffSpec>) so emptiness is unrepresentable
- Callers must early-return on empty selections before reaching this helper
- Alternatively return Result and bubble a proper error for empty input
Example fix
// before
fn paths_from_changes(changes: &[DiffSpec]) -> NonEmpty<BString> {
let paths = changes.iter().map(|c| c.path.clone()).collect::<Vec<_>>();
NonEmpty::from_vec(paths).expect("changes being non-empty means paths are non-empty")
}
// after - make the precondition structural
fn paths_from_changes(changes: &NonEmpty<DiffSpec>) -> NonEmpty<BString> {
let mut it = changes.iter().map(|c| c.path.clone());
let head = it.next().expect("iterator over NonEmpty yields one");
NonEmpty { head, tail: it.collect() }
} Defensive patterns
Strategy: validation
Validate before calling
// Caller-side: reject empty selections before invoking the helper
let changes = NonEmpty::from_vec(changes)
.ok_or_else(|| bad_input("select at least one change").arg_name("<CHANGES>"))?;
let paths = paths_from_changes(&changes); Prevention
- Make the helper take NonEmpty<DiffSpec> so the compiler enforces the precondition
- Early-return a user-facing error when a selection filters down to zero changes
- Never pass raw filter output into helpers documented to require non-empty input
When it happens
Trigger: A future caller invokes paths_from_changes with an empty Vec<DiffSpec>, for example after filtering hunks/files to nothing, or with an empty uncommitted-changes selection that earlier validation failed to reject.
Common situations: Refactors where empty change selections become representable (all hunks deselected, empty staged set); new discard modes added without re-validating non-emptiness.
Related errors
- classified branches are guaranteed to be non-empty
- committed files being non-empty means paths are non-empty
- BUG: Cannot possibly not have any changes here
- source branches is already checked to be non-empty
- deduping a NonEmpty will never make it empty
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/0ae134cb78e69a8f.
Report an issue: GitHub.