dir_structure/vfs/
git_vfs.rs1use std::io;
4use std::io::BufRead;
5use std::io::Read;
6use std::path::Path;
7use std::path::PathBuf;
8use std::pin::Pin;
9
10use crate::error::Error;
11use crate::error::Result;
12use crate::error::VfsResult;
13use crate::traits::vfs;
14use crate::traits::vfs::PathType;
15use crate::traits::vfs::VfsCore;
16
17pub struct GitVfs<'r> {
19 repo: &'r git2::Repository,
20 tree: git2::Tree<'r>,
21}
22
23impl<'r> GitVfs<'r> {
24 pub fn new(repo: &'r git2::Repository, tree: git2::Tree<'r>) -> Self {
26 Self { repo, tree }
27 }
28}
29
30impl<'r> VfsCore for GitVfs<'r> {
31 type Path = Path;
32}
33
34impl<'r> vfs::Vfs<'r> for GitVfs<'r> {
35 type DirWalk<'a>
36 = GitDirWalk
37 where
38 'r: 'a,
39 Self: 'a;
40
41 type RFile = GitRFile<'r>;
42
43 fn open_read(self: Pin<&Self>, path: &Path) -> VfsResult<Self::RFile, Self> {
44 let entry = self
45 .tree
46 .get_path(path)
47 .map_err(|e| Error::Parse(path.to_path_buf(), Box::new(e)))?;
48 let blob = entry
49 .to_object(self.repo)
50 .map_err(|e| Error::Parse(path.to_path_buf(), Box::new(e)))?
51 .into_blob()
52 .map_err(|e| {
53 Error::Parse(
54 path.to_path_buf(),
55 format!("Object is not blob: {:?}", e.kind().unwrap()).into(),
56 )
57 })?;
58 Ok(GitRFile { blob, offset: 0 })
59 }
60
61 fn read(self: Pin<&Self>, path: &Path) -> VfsResult<Vec<u8>, Self> {
62 let entry = self
63 .tree
64 .get_path(path)
65 .map_err(|e| Error::Parse(path.to_path_buf(), Box::new(e)))?;
66 let blob = entry
67 .to_object(self.repo)
68 .map_err(|e| Error::Parse(path.to_path_buf(), Box::new(e)))?
69 .into_blob()
70 .map_err(|e| {
71 Error::Parse(
72 path.to_path_buf(),
73 format!("Object is not blob: {:?}", e.kind().unwrap()).into(),
74 )
75 })?;
76 Ok(blob.content().to_vec())
77 }
78
79 fn exists(self: Pin<&Self>, path: &Path) -> VfsResult<bool, Self> {
80 Ok(self.tree.get_path(path).is_ok())
81 }
82
83 fn is_dir(self: Pin<&Self>, path: &Self::Path) -> VfsResult<bool, Self> {
84 match self.tree.get_path(path) {
85 Ok(entry) => Ok(entry.kind() == Some(git2::ObjectType::Tree)),
86 Err(_) => Ok(false),
87 }
88 }
89
90 fn walk_dir<'a>(self: Pin<&'a Self>, path: &Path) -> VfsResult<Self::DirWalk<'a>, Self>
91 where
92 'r: 'a,
93 {
94 let mut collector = Vec::new();
95 self.tree
96 .walk(git2::TreeWalkMode::PreOrder, |root, entry| {
97 let root_path = PathBuf::from(root);
99 if !path.starts_with(&root_path) {
100 return git2::TreeWalkResult::Skip;
101 }
102 let entry_path = match entry.name() {
103 Some(name) => {
104 let mut ep = PathBuf::from(root);
105 ep.push(name);
106 ep
107 }
108 None => {
109 return git2::TreeWalkResult::Skip;
111 }
112 };
113 if root_path == path {
114 let name = match entry_path.file_name() {
115 Some(f) => f.to_os_string(),
116 None => {
117 return git2::TreeWalkResult::Skip;
119 }
120 };
121 let kind = match entry.kind() {
122 Some(git2::ObjectType::Blob) => vfs::DirEntryKind::File,
123 Some(git2::ObjectType::Tree) => vfs::DirEntryKind::Directory,
124 Some(_ot) => {
125 return git2::TreeWalkResult::Skip;
127 }
128 None => {
129 return git2::TreeWalkResult::Skip;
131 }
132 };
133 collector.push(vfs::DirEntryInfo {
134 name,
135 kind,
136 path: entry_path,
137 });
138 git2::TreeWalkResult::Ok
139 } else if path.starts_with(&entry_path) {
140 git2::TreeWalkResult::Ok
141 } else {
142 git2::TreeWalkResult::Skip
143 }
144 })
145 .map_err(|e| Error::Parse(path.to_path_buf(), Box::new(e)))?;
146 let mut iter = collector.into_iter();
147 Ok(GitDirWalk {
148 next: Box::new(move || iter.next()),
149 })
150 }
151}
152
153pub struct GitDirWalk {
155 next: Box<dyn FnMut() -> Option<vfs::DirEntryInfo<Path>>>,
156}
157
158impl<'r> vfs::DirWalker<'r> for GitDirWalk {
159 type P = Path;
160
161 fn next(
162 &mut self,
163 ) -> Option<Result<vfs::DirEntryInfo<Self::P>, <Self::P as PathType>::OwnedPath>> {
164 Some(Ok((self.next)()?))
165 }
166}
167
168pub struct GitRFile<'r> {
170 blob: git2::Blob<'r>,
171 offset: usize,
172}
173
174impl BufRead for GitRFile<'_> {
175 fn fill_buf(&mut self) -> io::Result<&[u8]> {
176 let data = self.blob.content();
177 Ok(&data[self.offset..])
178 }
179
180 fn consume(&mut self, amt: usize) {
181 self.offset += amt;
182 }
183}
184
185impl Read for GitRFile<'_> {
186 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
187 let data = self.blob.content();
188 let remaining = &data[self.offset..];
189 let to_read = buf.len().min(remaining.len());
190 buf[..to_read].copy_from_slice(&remaining[..to_read]);
191 self.offset += to_read;
192 Ok(to_read)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use std::io::Read;
199 use std::path::Path;
200 use std::pin::Pin;
201
202 use crate::prelude::Vfs;
203 use crate::traits::vfs;
204 use crate::traits::vfs::DirWalker;
205 use crate::vfs::git_vfs::GitVfs;
206
207 fn open_narxia_repo() -> git2::Repository {
208 git2::Repository::open_from_env().expect("Failed to open git repository")
209 }
210
211 #[test]
212 fn test_read() {
213 let repo = open_narxia_repo();
214 let tree = repo
215 .head()
216 .expect("Failed to get HEAD")
217 .peel_to_tree()
218 .expect("Failed to get tree");
219 let vfs = GitVfs { repo: &repo, tree };
220 let vfs = Pin::new(&vfs);
221
222 let content = vfs
223 .read_string(Path::new("README.md"))
224 .expect("Failed to read README.md");
225 assert_eq!(content, include_str!("../../../../../README.md"));
226 }
227
228 #[test]
229 fn test_exists() {
230 let repo = open_narxia_repo();
231 let tree = repo
232 .head()
233 .expect("Failed to get HEAD")
234 .peel_to_tree()
235 .expect("Failed to get tree");
236 let vfs = GitVfs { repo: &repo, tree };
237 let vfs = Pin::new(&vfs);
238
239 assert!(
240 vfs.exists(Path::new("README.md"))
241 .expect("Failed to check existence")
242 );
243 assert!(
244 !vfs.exists(Path::new("NON_EXISTENT_FILE"))
245 .expect("Failed to check existence")
246 );
247 }
248
249 #[test]
250 fn test_open_read() {
251 let repo = open_narxia_repo();
252 let tree = repo
253 .head()
254 .expect("Failed to get HEAD")
255 .peel_to_tree()
256 .expect("Failed to get tree");
257 let vfs = GitVfs { repo: &repo, tree };
258 let vfs = Pin::new(&vfs);
259
260 let mut file = vfs
261 .open_read(Path::new("README.md"))
262 .expect("Failed to open README.md");
263 let mut content = String::new();
264 file.read_to_string(&mut content)
265 .expect("Failed to read README.md");
266 assert_eq!(content, include_str!("../../../../../README.md"));
267 }
268
269 #[test]
270 fn test_walk_dir() {
271 let repo = open_narxia_repo();
272 let tree = repo
273 .head()
274 .expect("Failed to get HEAD")
275 .peel_to_tree()
276 .expect("Failed to get tree");
277 let vfs = GitVfs { repo: &repo, tree };
278 let vfs = Pin::new(&vfs);
279 let mut walker = vfs.walk_dir(Path::new("src")).expect("Failed to walk dir");
280 let mut entries = Vec::new();
281 while let Some(entry) = walker.next() {
282 entries.push(entry.expect("error while walking dir"));
283 }
284
285 entries.sort_by_key(|e| e.name.clone());
286
287 assert_eq!(
288 entries,
289 vec![
290 vfs::DirEntryInfo {
291 name: "compiler".into(),
292 kind: vfs::DirEntryKind::Directory,
293 path: Path::new("src/compiler").into(),
294 },
295 vfs::DirEntryInfo {
296 name: "dev".into(),
297 kind: vfs::DirEntryKind::Directory,
298 path: Path::new("src/dev").into(),
299 },
300 vfs::DirEntryInfo {
301 name: "lib".into(),
302 kind: vfs::DirEntryKind::Directory,
303 path: Path::new("src/lib").into(),
304 },
305 ]
306 );
307
308 let mut walker = vfs
309 .walk_dir(Path::new("src/dev/narxia-workspace"))
310 .expect("Failed to walk dir");
311 let mut entries = Vec::new();
312 while let Some(entry) = walker.next() {
313 entries.push(entry.expect("error while walking dir"));
314 }
315
316 entries.sort_by_key(|e| e.name.clone());
317
318 assert_eq!(
319 entries,
320 vec![
321 vfs::DirEntryInfo {
322 name: "Cargo.toml".into(),
323 kind: vfs::DirEntryKind::File,
324 path: Path::new("src/dev/narxia-workspace/Cargo.toml").into(),
325 },
326 vfs::DirEntryInfo {
327 name: "src".into(),
328 kind: vfs::DirEntryKind::Directory,
329 path: Path::new("src/dev/narxia-workspace/src").into(),
330 },
331 ]
332 );
333 }
334}