gitbutlerapp/gitbutler · warning
deduping a NonEmpty will never make it empty
Error message
deduping a NonEmpty will never make it empty
What it means
Near-unreachable invariant in non_empty_dedup_maintain_sort: deduplicating a NonEmpty always retains its first element, so the output Vec is never empty and from_vec always succeeds. A panic here would require a modified loop that filters out every element including the first - ordinary code changes cannot produce it, and it cannot be triggered by input.
Source
Thrown at crates/but/src/command/legacy/squash.rs:1703
.commits
.iter()
.map(|commit| commit.id)
.map(|id| CommitId::try_from_commit_id(id, repo))
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(commits_in_segment)
}
fn non_empty_dedup_maintain_sort<T>(non_empty: NonEmpty<T>) -> NonEmpty<T>
where
T: Ord,
{
let mut out = Vec::new();
for item in non_empty {
if !out.contains(&item) {
out.push(item);
}
}
NonEmpty::from_vec(out).expect("deduping a NonEmpty will never make it empty")
}
View on GitHub (pinned to 2497b8007a)
Solutions
- No user action; if observed, suspect a locally modified build
- Maintainer: construct the result as NonEmpty { head, tail } directly and delete the expect
Example fix
// before
NonEmpty::from_vec(out).expect("deduping a NonEmpty will never make it empty")
// after - keep non-emptiness structural
let mut it = non_empty.into_iter();
let head = it.next().expect("iterator over NonEmpty yields one");
let mut tail = Vec::new();
let mut seen = vec![head.clone()];
for item in it {
if !seen.contains(&item) {
seen.push(item.clone());
tail.push(item);
}
}
NonEmpty { head, tail } Defensive patterns
Strategy: validation
Prevention
- When refactoring the dedup helper, construct NonEmpty { head, tail } directly to keep the invariant structural
- Do not replace the element-by-element loop with a blanket retain/dedup that could drop the head
- Treat any hit of this expect as evidence of a locally patched build, not a data problem
When it happens
Trigger: Only a patched or future version of the loop that drops the first item (for example switching to 'retain' on the output while excluding the head); no runtime input reaches it.
Common situations: Practically none; relevant as a code-review note when touching this helper during squash-related refactors.
Related errors
- source branches is already checked to be non-empty
- classified branches are guaranteed to be non-empty
- committed files being non-empty means paths are non-empty
- changes being non-empty means paths are non-empty
- programs was just checked to be non-empty
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/3ecc3161827286d9.
Report an issue: GitHub.