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

  1. Maintainer: change the signature to accept NonEmpty<DiffSpec> (or &NonEmpty<DiffSpec>) so emptiness is unrepresentable
  2. Callers must early-return on empty selections before reaching this helper
  3. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/0ae134cb78e69a8f. Report an issue: GitHub.