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.

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.

Appendix: 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)