gitbutlerapp/gitbutler · error

Cannot hash directory entries that aren't files or symlinks

Error message

Cannot hash directory entries that aren't files or symlinks

What it means

id_or_hash_from_worktree() hashes a worktree path's content to an object id. Directories cannot be hashed as blobs, so when the path is neither a regular file nor a symlink (e.g. a directory or special file), the function bails. It is used when computing effective merged changes.

Solutions

  1. Skip directory-typed paths before hashing (check metadata kind first)
  2. Handle submodule entries through their commit-id path rather than content hashing
  3. Re-check worktree state and retry if the file was concurrently replaced
  4. Ensure the expected entry is actually a file: git status / re-read the index

Example fix

// before
let id = id_or_hash_from_worktree(repo, path)?;
// after
let id = if path.is_dir() {
    eprintln!("{path:?} is a directory; skipping hash");
    None
} else {
    Some(id_or_hash_from_worktree(repo, path)?)
};
Defensive patterns

Strategy: validation

Validate before calling

fn hashable(path: &Path) -> bool {
    path.symlink_metadata().map(|m| m.is_file() || m.is_symlink()).unwrap_or(false)
}

Type guard

if path.is_dir() { skip(); } else { hash(path)?; }

Try / catch

match id_or_hash_from_worktree(repo, path) {
    Err(e) if e.to_string().contains("aren't files or symlinks") => skip_entry(),
    other => other?,
}

Prevention

When it happens

Trigger: Calling merged_change_is_effective (or the hashing helper) on a path that resolves to a directory at hash time — commonly because the file was deleted and replaced by a directory, or the path is a git submodule/fifo/socket.

Common situations: A branch swap where a formerly-tracked file path became a directory; submodule checkout state making a path a directory; race where the file is replaced between stat and hash; special files on Linux.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/b9686648cd8212a1. Report an issue: GitHub.

Appendix: source

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

                &mut stream,
                md.len(),
                &mut gix::progress::Discard,
                &gix::interrupt::IS_INTERRUPTED,
            )?,
            ToGitOutcome::Process(mut stream) => {
                let mut buf = repo.empty_reusable_buffer();
                stream.read_to_end(&mut buf)?;
                gix::objs::compute_hash(repo.object_hash(), gix::object::Kind::Blob, &buf)?
            }
            ToGitOutcome::Buffer(buf) => {
                gix::objs::compute_hash(repo.object_hash(), gix::object::Kind::Blob, buf)?
            }
        }
    } else if md.is_symlink() {
        let bytes = gix::path::os_string_into_bstring(std::fs::read_link(path)?.into())?;
        gix::objs::compute_hash(repo.object_hash(), gix::object::Kind::Blob, &bytes)?
    } else {
        bail!("Cannot hash directory entries that aren't files or symlinks");
    };
    Ok(id)
}

fn into_tree_entry_kind(mode: gix::index::entry::Mode) -> anyhow::Result<EntryKind> {
    Ok(mode
        .to_tree_entry_mode()
        .with_context(|| format!("Entry contained invalid entry mode: {mode:?}"))?
        .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,

View on GitHub (pinned to 58e5313667)