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
- 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
- If the symlink was intentional (disk relocation), remove it and use a bind mount / proper relocation instead, then retry
- 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
- Do not symlink inside the application data directory; relocate the whole directory instead
- After restoring from backup, run `find <app-data> -type l` and resolve any hits before launching the app
- Maintainers: treat is_symlink as a removable entry (fs::remove_file unlinks it) instead of an unreachable
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
- When using OpenAI in a bring your own key configuration, you
- failed to create app settings
- Invalid response: ${JSON.stringify(result)}
- broker already configured
- broker has not been configured
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/b4c0579942365269.
Report an issue: GitHub.