Skip to main content

stratum/domain/
mfile.rs

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