1use git2::{Delta, Diff, DiffDelta, Patch};
2use std::{path::Path, sync::OnceLock};
3
4use crate::Error;
5
6pub struct ModifiedFile<'c> {
8 cache: OnceLock<Option<Patch<'c>>>,
9 diff: &'c Diff<'c>,
10 n: usize,
11}
12
13impl<'c> ModifiedFile<'c> {
14 pub fn new(diff: &'c Diff<'_>, n: usize) -> Self {
21 ModifiedFile {
22 cache: OnceLock::new(),
23 diff,
24 n,
25 }
26 }
27
28 pub fn old_path(&self) -> Option<&Path> {
30 self.delta()?.old_file().path()
31 }
32
33 pub fn new_path(&self) -> Option<&Path> {
35 self.delta()?.new_file().path()
36 }
37
38 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 pub fn status(&self) -> Option<Delta> {
54 Some(self.delta()?.status())
55 }
56
57 pub fn insertions(&self) -> Result<usize, Error> {
59 Ok(match self.patch()? {
61 Some(p) => p.line_stats()?.1,
62 None => 0,
63 })
64 }
65
66 pub fn deletions(&self) -> Result<usize, Error> {
68 Ok(match self.patch()? {
70 Some(p) => p.line_stats()?.2,
71 None => 0,
72 })
73 }
74
75 fn delta(&self) -> Option<DiffDelta<'_>> {
77 self.diff.get_delta(self.n)
78 }
79
80 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 assert_eq!(mfile.old_path().unwrap(), "file.txt");
126 });
127 }
128
129 #[test]
130 fn test_new_path() {
131 mfile_fixture(|_, mfile| {
132 assert_eq!(mfile.new_path().unwrap(), "file.txt");
134 });
135 }
136
137 #[test]
138 fn test_delta() {
139 mfile_fixture(|_, mfile| {
140 assert_eq!(mfile.new_path().unwrap(), "file.txt");
142 });
143 }
144}