dumpfs 0.1.0

A tool for dumping codebase information for LLMs efficiently and effectively
Documentation
use std::{path::PathBuf, time::SystemTime};

/// Classifies the general type of a filesystem entry, potentially using heuristics.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FsFileType {
    TextFile,
    BinaryFile,
    Symlink,
    Directory,
    Other,
}

/// Common metadata associated with a filesystem entry.
#[derive(Debug, Clone)]
pub struct FsMetadata {
    pub size: u64,
    pub modified: SystemTime,
    pub permissions: u32,
    pub file_type: FsFileType,
}

/// Represents a directory in the scanned tree.
#[derive(Debug, Clone)]
pub struct FsDirectoryNode {
    pub name: String,
    pub path: PathBuf,
    pub metadata: FsMetadata,
    pub contents: Vec<FsNode>,
}

impl FsDirectoryNode {
    /// Recursively collects all file nodes from this directory and its subdirectories into a flat list.
    ///
    /// # Returns
    ///
    /// A vector containing all `FsFileNode` instances in the directory tree.
    pub fn get_files(&self) -> Vec<&FsFileNode> {
        let mut files = Vec::new();
        self.collect_files_recursive(&mut files);
        files
    }

    /// Helper method for the recursive file collection.
    fn collect_files_recursive<'a>(&'a self, files: &mut Vec<&'a FsFileNode>) {
        for node in &self.contents {
            match node {
                FsNode::Directory(dir) => {
                    dir.collect_files_recursive(files);
                }
                FsNode::File(file_node) => {
                    files.push(file_node);
                }
                _ => {}
            }
        }
    }
    /// Constructs a title by traversing up to git root and combining repo name with the relative path
    /// For example, for /path/to/myrepo/src/object it returns "myrepo's src/object"
    pub fn title(&self) -> String {
        let mut path = self.path.as_path();
        // Cache the .git dir name to avoid repeated string creation
        let git_dir = ".git";

        // Find git root using backward traversal
        while let Some(parent) = path.parent() {
            if parent.join(git_dir).is_dir() {
                // Get repo name efficiently
                let repo_name = parent
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("repo");

                // Get relative path from git root to current directory
                return match self.path.strip_prefix(parent) {
                    Ok(rel_path) => {
                        let rel_path_str = rel_path.to_str().unwrap_or(&self.name);
                        // Avoid unnecessary string allocations by checking emptiness first
                        if rel_path_str.is_empty() {
                            format!("{}'s {}", repo_name, self.name)
                        } else {
                            format!("{}'s {}", repo_name, rel_path_str)
                        }
                    }
                    Err(_) => format!("{}'s {}", repo_name, self.name),
                };
            }
            path = parent;
        }

        // Not in a git repo - avoid clone if possible by returning reference
        self.name.to_string()
    }
}

/// Represents a regular file identified as likely text-based.
#[derive(Debug, Clone)]
pub struct FsFileNode {
    pub name: String,
    pub path: PathBuf,
    pub metadata: FsMetadata,
    pub content: Option<String>,
    pub lines: Option<usize>,
    pub chars: Option<usize>,
    pub token_count: Option<usize>, // Added field for token count
}

impl FsFileNode {
    /// Returns the file extension, if any, without the leading dot
    pub fn extension(&self) -> &str {
        self.path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("txt")
    }
}

/// Represents a regular file identified as likely binary.
#[derive(Debug, Clone)]
pub struct FsBinaryNode {
    pub name: String,
    pub path: PathBuf,
    pub metadata: FsMetadata,
}

/// Represents a symbolic link.
#[derive(Debug, Clone)]
pub struct FsSymlinkNode {
    pub name: String,
    pub path: PathBuf,
    pub metadata: FsMetadata,
    pub target: String,
    pub target_is_dir: Option<bool>,
    pub target_exists: Option<bool>,
}

/// Enum representing any type of node in the scanned filesystem tree.
#[derive(Debug, Clone)]
pub enum FsNode {
    Directory(FsDirectoryNode),
    File(FsFileNode),
    Binary(FsBinaryNode),
    Symlink(FsSymlinkNode),
}

impl FsNode {
    /// Returns the relative path of the node.
    pub fn path(&self) -> &PathBuf {
        match self {
            FsNode::Directory(d) => &d.path,
            FsNode::File(f) => &f.path,
            FsNode::Binary(b) => &b.path,
            FsNode::Symlink(s) => &s.path,
        }
    }

    /// Returns the name (filename or directory name) of the node.
    pub fn name(&self) -> &str {
        match self {
            FsNode::Directory(d) => &d.name,
            FsNode::File(f) => &f.name,
            FsNode::Binary(b) => &b.name,
            FsNode::Symlink(s) => &s.name,
        }
    }

    /// Returns the metadata associated with the node.
    pub fn metadata(&self) -> &FsMetadata {
        match self {
            FsNode::Directory(d) => &d.metadata,
            FsNode::File(f) => &f.metadata,
            FsNode::Binary(b) => &b.metadata,
            FsNode::Symlink(s) => &s.metadata,
        }
    }
}

/// Contains details extracted from text files (line/char counts).
#[derive(Debug, Clone, Default)]
pub struct FileDetail {
    pub lines: usize,
    pub chars: usize,
}