git_commits/
changes.rs

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(&current_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    /// Total size in bytes.
76    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            // Technically safe to unwrap as if `file` exists,
90            // then `find_blob()` returns `Ok`
91            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                // Technically, this is an error but it would never occur
112                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                // Technically, this is an error but it would never occur
125                return Ok(None);
126            };
127            let Some(new_file) = new_file else {
128                // Technically, this is an error but it would never occur
129                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                // Technically, this is an error but it would never occur
143                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                // Technically, this is an error but it would never occur
156                return Ok(None);
157            };
158            let Some(new_file) = new_file else {
159                // Technically, this is an error but it would never occur
160                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}