dumpfs 0.1.0

A tool for dumping codebase information for LLMs efficiently and effectively
Documentation
use crate::ensure;
use crate::error::Result;
use crate::fs::{FsScanOutput, FsScanner, FsScannerOpts};
use crate::git::{RemoteInfo, Repo};
use std::path::{Path, PathBuf};

#[derive(Debug, Default, Clone)]
pub struct SubjectOpts {
    pub input: String,
    pub reporter: Option<indicatif::ProgressBar>,
}
#[derive(Debug, Default)]
pub struct Subject {
    pub root: PathBuf,
    pub remote: Option<RemoteInfo>,
    pub file: Option<String>,
    pub repo: Option<Repo>,
    reporter: Option<indicatif::ProgressBar>,
}

impl Subject {
    /// Create new subject based on input
    pub fn new(opts: SubjectOpts) -> Result<Self> {
        ensure!(
            !opts.input.is_empty(),
            InvalidArgument,
            "Input cannot be empty string!"
        );

        match RemoteInfo::is_remote_input(&opts.input) {
            true => Self::new_remote(opts),
            false => Self::new_local(opts),
        }
    }

    pub fn scan(&self, mut opts: FsScannerOpts) -> Result<FsScanOutput> {
        opts.ignore_patterns
            .extend_from_slice(crate::fs::misc::common_extra_ignore());

        let scanner = FsScanner::new(&self.root, opts, self.reporter.clone())?;
        let output = scanner.scan()?;

        Ok(output)
    }

    /// Create a new Subject from an local path
    fn new_local(opts: SubjectOpts) -> Result<Self> {
        let path = PathBuf::from(&opts.input);

        ensure!(path.exists(), PathNotFound, "{}", path.display());

        Ok(Subject {
            root: get_root(&path),
            file: get_file(path),
            repo: None,
            ..Default::default()
        })
    }

    /// Create a new Subject from URL
    fn new_remote(opts: SubjectOpts) -> Result<Self> {
        let remote_info = RemoteInfo::new(opts.input)?;
        let mut repo = Repo::new(remote_info.clone());

        repo.ensure_cloned(opts.reporter.clone())?;

        if let Some(ref tree) = remote_info.tree {
            repo.root = repo.root.join(tree)
        }

        ensure!(repo.root.exists(), PathNotFound, "{}", repo.root.display());

        Ok(Subject {
            root: repo.root.clone(),
            file: remote_info.blob.clone(),
            remote: Some(remote_info),
            repo: Some(repo),
            reporter: opts.reporter,
        })
    }
}

impl std::fmt::Display for Subject {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Subject info:")?;
        writeln!(f, "  Root: {:?}", self.root)?;
        writeln!(f, "  File: {:?}", self.file)?;

        if let Some(remote) = self.remote.as_ref() {
            writeln!(f, "  Remote URL: {}", remote.url)?;
            writeln!(f, "  Owner: {}", remote.owner)?;
            writeln!(f, "  Name: {}", remote.name)?;
            writeln!(f, "  Branch: {:?}", remote.branch)?;
            if let Some(tree) = remote.tree.as_ref() {
                writeln!(f, "  Tree: {}", tree)?;
            }
            if let Some(blob) = remote.blob.as_ref() {
                writeln!(f, "  Blob: {}", blob)?;
            }
        }
        Ok(())
    }
}

fn get_file(path: PathBuf) -> Option<String> {
    path.is_file()
        .then(|| path.file_name().map(|f| f.to_string_lossy().to_string()))
        .flatten()
}

fn get_root(path: &Path) -> PathBuf {
    path.is_dir()
        .then_some(path)
        .or_else(|| path.parent())
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cache;
    use std::fs;

    #[test]
    fn test_subject_with_remote_url() {
        // Create a temporary directory for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let cache_path = temp_dir.path();
        unsafe { std::env::set_var("XDG_CACHE_HOME", cache_path) };

        // Create a subject with a remote URL
        let subject = Subject::new(SubjectOpts {
            input: "https://github.com/dtolnay/anyhow".into(),
            ..Default::default()
        })
        .expect("Subject creation should succeed");

        // Check that the subject has the expected properties
        assert!(subject.remote.is_some());
        let remote = subject.remote.unwrap();
        assert_eq!(remote.owner.as_str(), "dtolnay");
        assert_eq!(remote.name.as_str(), "anyhow");

        // Check that the cache directory was created
        let expected_path = cache_path.join(format!("{}/dtolnay/anyhow", cache::prefix()));
        assert_eq!(subject.root, expected_path);

        // Cleanup
        temp_dir.close().unwrap();
    }

    #[test]
    fn test_subject_with_local_path() {
        // Create a temporary directory for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let file_path = temp_dir.path().join("test_file.txt");

        // Create a test file
        fs::write(&file_path, "test content").unwrap();

        // Create a subject with a local file path
        let subject = Subject::new(SubjectOpts {
            input: file_path.to_string_lossy().into(),
            ..Default::default()
        })
        .expect("Subject creation should succeed");

        // Check that the subject has the expected properties
        assert!(subject.remote.is_none());
        assert_eq!(subject.root, temp_dir.path());
        assert_eq!(subject.file, Some("test_file.txt".to_string()));

        // Cleanup
        temp_dir.close().unwrap();
    }

    #[test]
    fn test_subject_with_nonexistent_path() {
        // Try to create a subject with a path that doesn't exist
        let result = Subject::new(SubjectOpts {
            input: "/path/that/does/not/exist".into(),
            ..Default::default()
        });

        // Check that we get a PathNotFound error
        assert!(result.is_err());
        match result {
            Err(crate::error::Error::PathNotFound(_)) => {}
            _ => panic!("Expected PathNotFound error"),
        }
    }

    #[test]
    fn test_subject_with_malformed_url() {
        // Create a temporary directory for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let cache_path = temp_dir.path();
        unsafe { std::env::set_var("XDG_CACHE_HOME", cache_path) };

        // Try to create a subject with a malformed URL (missing owner)
        let result = Subject::new(SubjectOpts {
            input: "https://github.com/malformed".into(),
            ..Default::default()
        });

        // Check that we get an InvalidArgument error
        assert!(result.is_err());
        match result {
            Err(crate::error::Error::InvalidArgument(_)) => {}
            _ => panic!("Expected InvalidArgument error"),
        }
    }
}