gitbutlerapp/gitbutler · error
committed files being non-empty means paths are non-empty
Error message
committed files being non-empty means paths are non-empty
What it means
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.
Source
Thrown at crates/but/src/command/legacy/discard.rs:365
for CommittedFileId {
commit_id,
path,
change_id: _,
} in tail
{
if commit_id != source.commit_id {
return Err(
bad_input("All committed files must come from the same commit")
.arg_name("<CHANGES>")
.hint("Discard committed files from each commit separately")
.into(),
);
}
paths.push(path);
}
let paths = paths.into_iter().unique().collect();
let paths = NonEmpty::from_vec(paths)
.expect("committed files being non-empty means paths are non-empty");
Ok(DiscardOperation::CommittedFiles { source, paths })
}
ClassifiedDiscardables::Uncommitted => {
Ok(DiscardOperation::Uncommitted(UncommittedSelection::All))
}
ClassifiedDiscardables::UncommittedChanges(changes) => Ok(DiscardOperation::Uncommitted(
UncommittedSelection::Changes(Box::new(changes)),
)),
}
}
pub fn run(
ctx: &mut Context,
meta: &mut impl RefMetadata,
perm: &mut RepoExclusive,
operation: DiscardOperation,
oplog_operation_kind: OperationKind,
) -> anyhow::Result<(DiscardOutcome, WorkspaceState)> {View on GitHub (pinned to 2497b8007a)
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
Example fix
// before
let paths = NonEmpty::from_vec(paths)
.expect("committed files being non-empty means paths are non-empty");
// after
let paths = NonEmpty::from_vec(paths)
.ok_or_else(|| anyhow::anyhow!("internal: committed-file selection lost all paths"))?; Defensive patterns
Strategy: validation
Validate before calling
// Guard the committed-files mapping inputs
debug_assert!(!committed_files.tail.is_empty() || committed_files.head.path.is_some(),
"CommittedFiles classification carries at least one path"); Prevention
- 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
When it happens
Trigger: 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.
Common situations: Refactors adding path filtering or normalization between classification and operation construction; not reachable through valid CLI input as written.
Related errors
- classified branches are guaranteed to be non-empty
- changes being non-empty means paths are non-empty
- source branches is already checked to be non-empty
- deduping a NonEmpty will never make it empty
- target OID must exist when ahead calculation is enabled
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/299aa050b2e14da5.
Report an issue: GitHub.