Skip to main content

the_code_graph_cli/adapters/
fs.rs

1use domain::error::{CodeGraphError, Result};
2use domain::ports::FileSystem;
3use sha2::{Digest, Sha256};
4use std::path::{Path, PathBuf};
5
6pub struct RealFileSystem;
7
8impl FileSystem for RealFileSystem {
9    fn list_files(&self, root: &Path, extensions: &[&str]) -> Result<Vec<PathBuf>> {
10        let mut builder = ignore::WalkBuilder::new(root);
11        builder.follow_links(false);
12        builder.add_custom_ignore_filename(".code-graphignore");
13
14        let files: Vec<PathBuf> = builder
15            .build()
16            .filter_map(|entry| entry.ok())
17            .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
18            .filter(|entry| {
19                entry
20                    .path()
21                    .extension()
22                    .and_then(|ext| ext.to_str())
23                    .is_some_and(|ext| extensions.contains(&ext))
24            })
25            .map(|entry| {
26                entry
27                    .path()
28                    .strip_prefix(root)
29                    .unwrap_or(entry.path())
30                    .to_path_buf()
31            })
32            .collect();
33
34        Ok(files)
35    }
36
37    fn read_file(&self, path: &Path) -> Result<String> {
38        std::fs::read_to_string(path).map_err(|e| CodeGraphError::FileSystem {
39            path: path.into(),
40            source: e,
41        })
42    }
43
44    fn file_hash(&self, path: &Path) -> Result<String> {
45        let content = std::fs::read(path).map_err(|e| CodeGraphError::FileSystem {
46            path: path.into(),
47            source: e,
48        })?;
49        let mut hasher = Sha256::new();
50        hasher.update(&content);
51        Ok(format!("{:x}", hasher.finalize()))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::fs;
59
60    #[test]
61    fn list_files_filters_by_extension() {
62        let tmp = tempfile::tempdir().unwrap();
63        fs::write(tmp.path().join("main.rs"), "fn main() {}").unwrap();
64        fs::write(tmp.path().join("lib.ts"), "export {}").unwrap();
65        fs::write(tmp.path().join("readme.md"), "# Hi").unwrap();
66
67        let fs_impl = RealFileSystem;
68        let files = fs_impl.list_files(tmp.path(), &["rs", "ts"]).unwrap();
69        assert_eq!(files.len(), 2);
70        assert!(files.iter().any(|f| f.to_str().unwrap().ends_with(".rs")));
71        assert!(files.iter().any(|f| f.to_str().unwrap().ends_with(".ts")));
72    }
73
74    #[test]
75    fn list_files_respects_gitignore() {
76        let tmp = tempfile::tempdir().unwrap();
77        // The `ignore` crate only honours .gitignore inside a git repo,
78        // so we initialise one in the temp directory.
79        fs::create_dir(tmp.path().join(".git")).unwrap();
80        fs::write(tmp.path().join(".gitignore"), "ignored.rs\n").unwrap();
81        fs::write(tmp.path().join("kept.rs"), "fn kept() {}").unwrap();
82        fs::write(tmp.path().join("ignored.rs"), "fn ignored() {}").unwrap();
83
84        let fs_impl = RealFileSystem;
85        let files = fs_impl.list_files(tmp.path(), &["rs"]).unwrap();
86        assert_eq!(files.len(), 1);
87        assert!(files[0].to_str().unwrap().contains("kept"));
88    }
89
90    #[test]
91    fn list_files_respects_code_graphignore() {
92        let tmp = tempfile::tempdir().unwrap();
93        fs::write(tmp.path().join(".code-graphignore"), "skip_me.rs\n").unwrap();
94        fs::write(tmp.path().join("keep.rs"), "fn keep() {}").unwrap();
95        fs::write(tmp.path().join("skip_me.rs"), "fn skip() {}").unwrap();
96
97        let fs_impl = RealFileSystem;
98        let files = fs_impl.list_files(tmp.path(), &["rs"]).unwrap();
99        assert_eq!(files.len(), 1);
100        assert!(files[0].to_str().unwrap().contains("keep"));
101    }
102
103    #[test]
104    fn read_file_returns_contents() {
105        let tmp = tempfile::tempdir().unwrap();
106        let path = tmp.path().join("test.txt");
107        fs::write(&path, "hello world").unwrap();
108
109        let fs_impl = RealFileSystem;
110        let content = fs_impl.read_file(&path).unwrap();
111        assert_eq!(content, "hello world");
112    }
113
114    #[test]
115    fn file_hash_is_deterministic_sha256() {
116        let tmp = tempfile::tempdir().unwrap();
117        let path = tmp.path().join("test.txt");
118        fs::write(&path, "hello").unwrap();
119
120        let fs_impl = RealFileSystem;
121        let hash1 = fs_impl.file_hash(&path).unwrap();
122        let hash2 = fs_impl.file_hash(&path).unwrap();
123        assert_eq!(hash1, hash2);
124        assert_eq!(hash1.len(), 64); // SHA-256 hex is 64 chars
125    }
126}