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

  1. Report the exact 'but discard ...' invocation - this path is not reachable via well-formed input
  2. Maintainer: thread the classification's NonEmpty through instead of rebuilding it from a Vec
  3. 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

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


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