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
10pub struct ModifiedFile<'c> {
12 diff: RcDiff<'c>,
13 n: usize,
14 patch_cache: Mutex<CachedPatch<'c>>,
15}
16
17impl<'c> ModifiedFile<'c> {
18 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 pub fn old_path(&self) -> Option<&Path> {
36 self.delta()?.old_file().path()
37 }
38
39 pub fn new_path(&self) -> Option<&Path> {
41 self.delta()?.new_file().path()
42 }
43
44 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 pub fn status(&self) -> Option<Delta> {
60 Some(self.delta()?.status())
61 }
62
63 pub fn insertions(&self) -> Result<usize, Error> {
65 Ok(match self.patch()? {
67 Some(p) => p.line_stats()?.1,
68 None => 0,
69 })
70 }
71
72 pub fn deletions(&self) -> Result<usize, Error> {
74 Ok(match self.patch()? {
76 Some(p) => p.line_stats()?.2,
77 None => 0,
78 })
79 }
80
81 fn delta(&self) -> Option<DiffDelta<'_>> {
83 self.diff.get_delta(self.n)
84 }
85
86 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 assert_eq!(mfile.old_path().unwrap(), "file.txt");
142 });
143 }
144
145 #[test]
146 fn test_new_path() {
147 mfile_fixture(|mfile| {
148 assert_eq!(mfile.new_path().unwrap(), "file.txt");
150 });
151 }
152
153 #[test]
154 fn test_delta() {
155 mfile_fixture(|mfile| {
156 assert_eq!(mfile.new_path().unwrap(), "file.txt");
158 });
159 }
160}