the_code_graph_cli/adapters/
fs.rs1use 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 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); }
126}