magi-code 0.63.4

Repository-aware CLI coding agent for terminal work
Documentation
use anyhow::Context;
use std::{
    collections::HashMap,
    fs,
    path::{Path, PathBuf},
    sync::Mutex,
    time::{Duration, Instant, SystemTime},
};

#[derive(Debug)]
pub(super) struct FsCache {
    ttl: Duration,
    entries: Mutex<HashMap<FsCacheKey, CacheEntry>>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct FsCacheKey {
    dir: PathBuf,
    modified: SystemTime,
}

#[derive(Debug, Clone)]
struct CacheEntry {
    inserted: Instant,
    listing: Vec<DirectoryListingEntry>,
}

#[derive(Debug, Clone)]
pub(super) struct DirectoryListingEntry {
    pub(super) name: String,
    pub(super) file_type: fs::FileType,
}

#[derive(Debug, Clone)]
pub(super) struct DirectoryListing {
    pub(super) entries: Vec<DirectoryListingEntry>,
    #[cfg(test)]
    pub(super) cache_hit: bool,
}

impl Default for FsCache {
    fn default() -> Self {
        Self::with_ttl(Duration::from_secs(2))
    }
}

impl FsCache {
    pub(super) fn with_ttl(ttl: Duration) -> Self {
        Self {
            ttl,
            entries: Mutex::new(HashMap::new()),
        }
    }

    pub(super) fn read_dir_listing(&self, dir: &Path) -> anyhow::Result<DirectoryListing> {
        let key = self.key_for_dir(dir)?;
        if let Some(entries) = self.get_valid(&key)? {
            return Ok(DirectoryListing {
                entries,
                #[cfg(test)]
                cache_hit: true,
            });
        }

        let entries = read_directory_entries(dir)?;
        self.store_keyed(key, entries.clone())?;
        Ok(DirectoryListing {
            entries,
            #[cfg(test)]
            cache_hit: false,
        })
    }

    pub(super) fn len(&self) -> anyhow::Result<usize> {
        self.entries
            .lock()
            .map(|entries| entries.len())
            .map_err(|_| anyhow::anyhow!("fs cache lock poisoned"))
    }

    fn key_for_dir(&self, dir: &Path) -> anyhow::Result<FsCacheKey> {
        let canonical = dir
            .canonicalize()
            .with_context(|| format!("failed to canonicalize directory '{}'", dir.display()))?;
        let modified = fs::metadata(&canonical)
            .with_context(|| format!("failed to read metadata for '{}'", canonical.display()))?
            .modified()
            .with_context(|| {
                format!(
                    "failed to read modified time for directory '{}'",
                    canonical.display()
                )
            })?;
        Ok(FsCacheKey {
            dir: canonical,
            modified,
        })
    }

    fn get_valid(&self, key: &FsCacheKey) -> anyhow::Result<Option<Vec<DirectoryListingEntry>>> {
        let entries = self
            .entries
            .lock()
            .map_err(|_| anyhow::anyhow!("fs cache lock poisoned"))?;
        Ok(entries.get(key).and_then(|entry| {
            (entry.inserted.elapsed() <= self.ttl).then(|| entry.listing.clone())
        }))
    }

    fn store_keyed(
        &self,
        key: FsCacheKey,
        listing: Vec<DirectoryListingEntry>,
    ) -> anyhow::Result<()> {
        let mut entries = self
            .entries
            .lock()
            .map_err(|_| anyhow::anyhow!("fs cache lock poisoned"))?;
        entries.insert(
            key,
            CacheEntry {
                inserted: Instant::now(),
                listing,
            },
        );
        Ok(())
    }
}

fn read_directory_entries(dir: &Path) -> anyhow::Result<Vec<DirectoryListingEntry>> {
    let mut entries = Vec::new();
    for entry in fs::read_dir(dir)
        .with_context(|| format!("failed to read directory '{}'", dir.display()))?
    {
        let entry = entry
            .with_context(|| format!("failed to read entry in directory '{}'", dir.display()))?;
        let path = entry.path();
        let file_type = entry
            .file_type()
            .with_context(|| format!("failed to read file type for '{}'", path.display()))?;
        entries.push(DirectoryListingEntry {
            name: entry.file_name().to_string_lossy().into_owned(),
            file_type,
        });
    }
    Ok(entries)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{thread, time::Duration};

    #[test]
    fn fs_cache_repeated_read_hits_cache() {
        let temp = tempfile::TempDir::new().unwrap();
        fs::write(temp.path().join("visible.txt"), "content").unwrap();
        let cache = FsCache::default();

        let first = cache.read_dir_listing(temp.path()).unwrap();
        let second = cache.read_dir_listing(temp.path()).unwrap();

        assert!(!first.cache_hit);
        assert!(second.cache_hit);
        assert_eq!(second.entries.len(), 1);
        assert_eq!(second.entries[0].name, "visible.txt");
        assert!(second.entries[0].file_type.is_file());
    }

    #[test]
    fn fs_cache_directory_mtime_change_misses_cache() {
        let temp = tempfile::TempDir::new().unwrap();
        let cache = FsCache::default();
        let first = cache.read_dir_listing(temp.path()).unwrap();
        assert!(!first.cache_hit);

        thread::sleep(Duration::from_millis(20));
        fs::write(temp.path().join("new.txt"), "content").unwrap();
        let second = cache.read_dir_listing(temp.path()).unwrap();

        assert!(!second.cache_hit);
        assert!(second.entries.iter().any(|entry| entry.name == "new.txt"));
    }

    #[test]
    fn fs_cache_ttl_expiry_misses_cache() {
        let temp = tempfile::TempDir::new().unwrap();
        let cache = FsCache::with_ttl(Duration::from_millis(1));
        assert!(!cache.read_dir_listing(temp.path()).unwrap().cache_hit);

        thread::sleep(Duration::from_millis(5));
        assert!(!cache.read_dir_listing(temp.path()).unwrap().cache_hit);
    }
}