dir_structure/vfs/
git_vfs.rs

1//! A virtual filesystem implementation that reads from a git repository.
2
3use 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
17/// A virtual filesystem that reads from a git repository.
18pub struct GitVfs<'r> {
19    repo: &'r git2::Repository,
20    tree: git2::Tree<'r>,
21}
22
23impl<'r> GitVfs<'r> {
24    /// Create a new `GitVfs` from a git repository and a tree.
25    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                // We don't actually care about the callback, we just want the iterator.
98                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                        // not valid UTF-8 in file name, skip
110                        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                            // no last component, skip
118                            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                            // unsupported git object type, skip
126                            return git2::TreeWalkResult::Skip;
127                        }
128                        None => {
129                            // unknown git object type, skip
130                            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
153/// A directory walker that reads from a git tree.
154pub 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
168/// A read-only file that reads from a git blob.
169pub 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}