1use std::iter::FusedIterator;
2use std::path::Path;
3
4use git2::{Delta, Diff, DiffDelta, DiffFile, Repository};
5
6use super::GitError;
7use super::{Added, Change, Commit, Deleted, Modified, Renamed};
8
9pub struct Changes<'repo, 'commit> {
10 commit: &'commit Commit<'repo>,
11 diff: Diff<'repo>,
12 idx_delta: usize,
13 next_change: Option<Change>,
14}
15
16impl<'repo, 'commit> Changes<'repo, 'commit> {
17 pub(crate) fn from_commit(commit: &'commit Commit<'repo>) -> Result<Self, GitError> {
18 let current_tree = commit.commit.tree()?;
19
20 let parent_tree = commit
21 .commit
22 .parent(0)
23 .ok()
24 .map(|parent| parent.tree())
25 .transpose()?;
26
27 let mut diff =
28 commit
29 .repo
30 .diff_tree_to_tree(parent_tree.as_ref(), Some(¤t_tree), None)?;
31
32 diff.find_similar(None)?;
33
34 Ok(Self {
35 commit,
36 diff,
37 idx_delta: 0,
38 next_change: None,
39 })
40 }
41}
42
43impl<'repo, 'commit> Iterator for Changes<'repo, 'commit> {
44 type Item = Result<Change, GitError>;
45
46 fn next(&mut self) -> Option<Self::Item> {
47 loop {
48 if let Some(change) = self.next_change.take() {
49 return Some(Ok(change));
50 }
51
52 let delta = match self.diff.get_delta(self.idx_delta) {
53 Some(delta) => delta,
54 None => return None,
55 };
56 self.idx_delta += 1;
57
58 match extract_changes(&self.commit.repo, delta) {
59 Ok(Some((change, next_change))) => {
60 self.next_change = next_change;
61
62 return Some(Ok(change));
63 }
64 Ok(None) => {}
65 Err(err) => return Some(Err(err)),
66 }
67 }
68 }
69}
70
71impl FusedIterator for Changes<'_, '_> {}
72
73struct ChangeFileRef<'diff> {
74 path: &'diff Path,
75 size: usize,
77}
78
79impl<'diff> ChangeFileRef<'diff> {
80 fn new(repo: &Repository, file: DiffFile<'diff>) -> Option<Self> {
81 if !file.exists() {
82 return None;
83 }
84
85 let path = file.path()?;
86
87 let oid = file.id();
88 let Ok(blob) = repo.find_blob(oid) else {
89 return None;
92 };
93
94 Some(Self {
95 path,
96 size: blob.size(),
97 })
98 }
99}
100
101fn extract_changes<'repo>(
102 repo: &Repository,
103 delta: DiffDelta<'_>,
104) -> Result<Option<(Change, Option<Change>)>, GitError> {
105 let old_file = ChangeFileRef::new(repo, delta.old_file());
106 let new_file = ChangeFileRef::new(repo, delta.new_file());
107
108 match delta.status() {
109 Delta::Added | Delta::Copied => {
110 let Some(new_file) = new_file else {
111 return Ok(None);
113 };
114
115 let change = Change::Added(Added {
116 path: new_file.path.to_path_buf(),
117 size: new_file.size,
118 });
119
120 Ok(Some((change, None)))
121 }
122 Delta::Modified => {
123 let Some(old_file) = old_file else {
124 return Ok(None);
126 };
127 let Some(new_file) = new_file else {
128 return Ok(None);
130 };
131
132 let change = Change::Modified(Modified {
133 path: new_file.path.to_path_buf(),
134 old_size: old_file.size,
135 new_size: new_file.size,
136 });
137
138 Ok(Some((change, None)))
139 }
140 Delta::Deleted => {
141 let Some(old_file) = old_file else {
142 return Ok(None);
144 };
145
146 let change = Change::Deleted(Deleted {
147 path: old_file.path.to_path_buf(),
148 size: old_file.size,
149 });
150
151 Ok(Some((change, None)))
152 }
153 Delta::Renamed => {
154 let Some(old_file) = old_file else {
155 return Ok(None);
157 };
158 let Some(new_file) = new_file else {
159 return Ok(None);
161 };
162
163 let change_modified = if old_file.size != new_file.size {
164 Some(Change::Modified(Modified {
165 path: new_file.path.to_path_buf(),
166 old_size: old_file.size,
167 new_size: new_file.size,
168 }))
169 } else {
170 None
171 };
172
173 let change_renamed = Change::Renamed(Renamed {
174 old_path: old_file.path.to_path_buf(),
175 new_path: new_file.path.to_path_buf(),
176 size: new_file.size,
177 });
178
179 let change = match change_modified {
180 Some(change_modified) => (change_modified, Some(change_renamed)),
181 None => (change_renamed, None),
182 };
183
184 Ok(Some(change))
185 }
186 Delta::Unmodified
187 | Delta::Ignored
188 | Delta::Untracked
189 | Delta::Typechange
190 | Delta::Unreadable
191 | Delta::Conflicted => {
192 return Ok(None);
193 }
194 }
195}