gitbutlerapp/gitbutler · error

BUG: we use 'matching' so there are no directories

Error message

BUG: we use 'matching' so there are no directories

What it means

`disk_kind_to_entry_kind` maps an untracked item's `gix::dir::entry::Kind` to a `but_core::diff::EntryKind`. `Kind::Directory` is `unreachable!` because the untracked traversal runs in `matching` mode, where directories are not collapsed into entries — every emitted path is a leaf (file, symlink, repository). Seeing a Directory therefore means the traversal was run in a collapsing mode (`collapsed` untracked handling) or an index entry carried a directory kind, both of which this code explicitly rules out.

Source

Thrown at crates/but-core/src/diff/worktree.rs:860

        .kind())
}

/// Most importantly, this function allows to skip over untrackable entries, like named pipes, sockets and character devices, just like Git.
/// `path` is needed for now while we have to stat the file again to learn about the executable bits.
// TODO: remove `path` and provide the stat information or at least executable info with `gitoxide` - it has that info.
fn disk_kind_to_entry_kind(
    disk_kind: Option<gix::dir::entry::Kind>,
    index_kind: Option<gix::dir::entry::Kind>,
    path: PathBuf,
) -> anyhow::Result<Option<EntryKind>> {
    Ok(Some(
        match disk_kind
            .or(index_kind)
            .context("Didn't have any type information for untracked item")?
        {
            entry::Kind::Repository => EntryKind::Commit,
            entry::Kind::Directory => {
                unreachable!("BUG: we use 'matching' so there are no directories")
            }
            entry::Kind::Untrackable => return Ok(None),
            entry::Kind::File => {
                let md = path.symlink_metadata()?;
                if gix::fs::is_executable(&md) {
                    EntryKind::BlobExecutable
                } else {
                    EntryKind::Blob
                }
            }
            entry::Kind::Symlink => EntryKind::Link,
        },
    ))
}

/// Unified diffs
impl TreeChange {
    /// Like [`Self::unified_patch()`], but also provides the file header for diffs like this:

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Keep the traversal feeding this function in matching mode (no directory collapse) as the caller configures today.
  2. Pin/verify the gix version after upgrades by running the worktree-diff snapshot tests.
  3. If directory entries must be supported, map them to an explicit error or recurse instead of `unreachable!`.

Example fix

// before
entry::Kind::Directory => {
    unreachable!("BUG: we use 'matching' so there are no directories")
}

// after
entry::Kind::Directory => {
    anyhow::bail!("untracked traversal emitted a directory for '{path}' — matching mode required");
}
Defensive patterns

Strategy: type-guard

Type guard

fn is_leaf_kind(kind: gix::dir::entry::Kind) -> bool {
    !matches!(kind, gix::dir::entry::Kind::Directory)
}

Prevention

When it happens

Trigger: Switching the untracked collection from `matching` to `collapsed`/directory mode; a gix version changing when `Kind::Directory` is attached to disk or index metadata; constructing items by hand in tests with directory kinds.

Common situations: Performance experiments that collapse untracked directories; gix upgrades altering traversal kinds; fixture builders that synthesize `gix::dir::Entry` values directly.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/0c2ab13594f48412. Report an issue: GitHub.