Skip to main content

stratum/domain/
mfile.rs

1use git2::{Delta, DiffDelta, Patch};
2use lru::LruCache;
3use std::{num::NonZeroUsize, path::Path, rc::Rc, sync::Mutex};
4
5use crate::Error;
6use crate::domain::{CachedPatch, RcDiff, RcPatch};
7
8const CACHE_KEY: u8 = 1;
9
10/// A file that was touched in a commit
11pub struct ModifiedFile<'c> {
12    diff: RcDiff<'c>,
13    n: usize,
14    patch_cache: Mutex<CachedPatch<'c>>,
15}
16
17impl<'c> ModifiedFile<'c> {
18    /// Instantiate the modified file object from a git diff.
19    ///
20    /// As a single diff can have > 1 modified/touched file, a single unsigned
21    /// integer is provided to specify the delta and/or patch that this file
22    /// looks to represent. Hence, the struct will normally be instantiated via
23    /// iterating over the diff deltas as they are readily avaliable.
24    pub fn new(diff: RcDiff<'c>, n: usize) -> Self {
25        let patch_cache = Mutex::new(LruCache::new(NonZeroUsize::new(1).unwrap()));
26
27        ModifiedFile {
28            diff,
29            n,
30            patch_cache,
31        }
32    }
33
34    /// Return the path of the old file in the diff
35    pub fn old_path(&self) -> Option<&Path> {
36        self.delta()?.old_file().path()
37    }
38
39    /// Return the path of the new file in the diff
40    pub fn new_path(&self) -> Option<&Path> {
41        self.delta()?.new_file().path()
42    }
43
44    /// Return the current filename of the modified file in the commit
45    ///
46    /// Returns None if neither the old or new filename are valid
47    pub fn filename(&self) -> Option<&str> {
48        let dev_null = Path::new("/dev/null");
49        let path = match self.new_path() {
50            Some(p) if p != dev_null => p,
51            _ => self.old_path()?,
52        };
53
54        path.file_name()?.to_str()
55    }
56
57    /// Return the file status of the given patch
58    //TODO: Should this return a custom type?? Probably
59    pub fn status(&self) -> Option<Delta> {
60        Some(self.delta()?.status())
61    }
62
63    /// The number of lines added in this modified file
64    pub fn insertions(&self) -> Result<usize, Error> {
65        // As per git2 docs first entry is insertions
66        Ok(match self.patch()? {
67            Some(p) => p.line_stats()?.1,
68            None => 0,
69        })
70    }
71
72    /// The number of lines removed in this modified file
73    pub fn deletions(&self) -> Result<usize, Error> {
74        // As per git2 docs second entry is deletions
75        Ok(match self.patch()? {
76            Some(p) => p.line_stats()?.2,
77            None => 0,
78        })
79    }
80
81    /// Return the delta associated with the index
82    fn delta(&self) -> Option<DiffDelta<'_>> {
83        self.diff.get_delta(self.n)
84    }
85
86    /// Return the patch given the diff, caching it within the struct
87    ///
88    /// Returns Ok(None) if the file is unchanged
89    fn patch(&self) -> Result<Option<RcPatch<'c>>, Error> {
90        let mut guard = self.patch_cache.lock().map_err(|_| Error::PoisonedCache)?;
91        let data = guard.try_get_or_insert(CACHE_KEY, || {
92            let patch = Patch::from_diff(self.diff.as_ref(), self.n)?;
93            match patch {
94                Some(p) => Ok::<Option<RcPatch>, Error>(Some(Rc::new(p))),
95                None => Ok(None),
96            }
97        })?;
98
99        Ok(data.as_ref().map(Rc::clone))
100    }
101}
102
103#[cfg(test)]
104mod test {
105    use std::rc::Rc;
106
107    use super::*;
108    use crate::common::init_repo;
109
110    fn mfile_fixture<F, R>(f: F) -> R
111    where
112        F: FnOnce(&ModifiedFile) -> R,
113    {
114        let repo = init_repo();
115
116        let c1 = repo
117            .head()
118            .expect("Failed to fetch HEAD")
119            .peel_to_commit()
120            .expect("Failed to peel HEAD to commit");
121        let c2 = c1.parent(0).expect("Couldn't get parent");
122
123        let diff = repo
124            .diff_tree_to_tree(
125                Some(&c2.tree().expect("Failed to get tree")),
126                Some(&c1.tree().expect("Failed to get tree")),
127                None,
128            )
129            .expect("Failed to make diff");
130
131        let diff = Rc::new(diff);
132        let mfile = ModifiedFile::new(diff, 0);
133
134        f(&mfile)
135    }
136
137    #[test]
138    fn test_old_path() {
139        mfile_fixture(|mfile| {
140            // use mfile here
141            assert_eq!(mfile.old_path().unwrap(), "file.txt");
142        });
143    }
144
145    #[test]
146    fn test_new_path() {
147        mfile_fixture(|mfile| {
148            // use mfile here
149            assert_eq!(mfile.new_path().unwrap(), "file.txt");
150        });
151    }
152
153    #[test]
154    fn test_delta() {
155        mfile_fixture(|mfile| {
156            // use mfile here
157            assert_eq!(mfile.new_path().unwrap(), "file.txt");
158        });
159    }
160}