gitbutlerapp/gitbutler · error

BUG: we do not create or work with symlinks

Error message

BUG: we do not create or work with symlinks

What it means

Legacy `Storage` in but-utils removes files under the app-data dir using `symlink_metadata`; an entry that is neither a directory nor a regular file is assumed impossible because the app never creates symlinks or special files, so that branch panics. Reaching it means something (usually a symlink) was placed inside the GitButler app-data directory from outside (crates/but-utils/src/lib.rs:86).

Source

Thrown at crates/but-utils/src/lib.rs:86

        /// Delete the file or directory at `rela_path`.
        ///
        /// ### Panics
        ///
        /// If a symlink is encountered.
        pub fn delete(&self, rela_path: impl AsRef<Path>) -> std::io::Result<()> {
            let file_path = self.local_data_dir.join(rela_path);
            let md = match file_path.symlink_metadata() {
                Ok(md) => md,
                Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
                Err(err) => return Err(err),
            };

            if md.is_dir() {
                fs::remove_dir_all(file_path)?;
            } else if md.is_file() {
                fs::remove_file(file_path)?;
            } else {
                unreachable!("BUG: we do not create or work with symlinks")
            }
            Ok(())
        }
    }
}
#[cfg(feature = "legacy")]
pub use legacy::Storage;

// Returns an ordered list of relative paths for files inside a directory recursively.
pub fn list_files<P: AsRef<Path>>(
    dir_path: P,
    ignore_prefixes: &[P],
    recursive: bool,
    remove_prefix: Option<P>,
) -> Result<Vec<PathBuf>> {
    let mut files = vec![];
    let dir_path = dir_path.as_ref();
    if !dir_path.exists() {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Inspect the app-data dir for non-regular entries: `find <app-data> -type l` and remove or inline the offending symlinks, then retry the clear
  2. If the symlink was intentional (disk relocation), remove it and use a bind mount / proper relocation instead, then retry
  3. As a maintainer: handle `md.file_type().is_symlink()` with `fs::remove_file` (which unlinks the link) instead of panicking

Example fix

// before (but-utils/src/lib.rs)
if md.is_dir() {
    fs::remove_dir_all(file_path)?;
} else if md.is_file() {
    fs::remove_file(file_path)?;
} else {
    unreachable!("BUG: we do not create or work with symlinks")
}

// after - unlink symlinks instead of panicking
if md.is_dir() {
    fs::remove_dir_all(file_path)?;
} else {
    // regular files, symlinks, and other non-dirs all unlink fine
    fs::remove_file(file_path)?;
}
Defensive patterns

Strategy: validation

Validate before calling

// Scan the app-data dir for non-regular entries before clearing
fn clearable(dir: &Path) -> std::io::Result<bool> {
    for entry in std::fs::read_dir(dir)? {
        let md = entry?.metadata()?; // follows symlinks; use symlink_metadata to detect them
        let lmd = entry?.symlink_metadata()?;
        if !(lmd.is_dir() || lmd.is_file()) { return Ok(false); }
    }
    Ok(true)
}

Prevention

When it happens

Trigger: `Storage::clear()`-style removal while the app-data dir contains a symlink, FIFO, or socket - e.g. a user symlinked a cache subfolder to another disk, or a backup restore converted files into symlinks.

Common situations: Users relocating ~/.local/share/GitButler (or Library/Application Support/GitButler) subfolders via symlinks; partial restores from Time Machine/rsync with `-l` semantics; leftover mkfifo debug artifacts.

Related errors


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