1use std::collections::HashSet;
4
5use git2::{Diff, Repository};
6use scopetime::scope_time;
7
8use super::{diff::DiffOptions, CommitId, RepoPath};
9use crate::{
10 error::Result,
11 sync::{get_stashes, repository::repo},
12 StatusItem, StatusItemType,
13};
14
15#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
17pub struct OldNew<T> {
18 pub old: T,
20 pub new: T,
22}
23
24pub fn sort_commits(repo: &Repository, commits: (CommitId, CommitId)) -> Result<OldNew<CommitId>> {
26 if repo.graph_descendant_of(commits.0.get_oid(), commits.1.get_oid())? {
27 Ok(OldNew {
28 old: commits.1,
29 new: commits.0,
30 })
31 } else {
32 Ok(OldNew {
33 old: commits.0,
34 new: commits.1,
35 })
36 }
37}
38
39pub fn get_commit_files(
41 repo_path: &RepoPath,
42 id: CommitId,
43 other: Option<CommitId>,
44) -> Result<Vec<StatusItem>> {
45 scope_time!("get_commit_files");
46
47 let repo = repo(repo_path)?;
48
49 let diff = if let Some(other) = other {
50 get_compare_commits_diff(&repo, sort_commits(&repo, (id, other))?, None, None)?
51 } else {
52 get_commit_diff(
53 &repo,
54 id,
55 None,
56 None,
57 Some(&get_stashes(repo_path)?.into_iter().collect()),
58 )?
59 };
60
61 let res = diff
62 .deltas()
63 .map(|delta| {
64 let status = StatusItemType::from(delta.status());
65
66 StatusItem {
67 path: delta
68 .new_file()
69 .path()
70 .map(|p| p.to_str().unwrap_or("").to_string())
71 .unwrap_or_default(),
72 status,
73 }
74 })
75 .collect::<Vec<_>>();
76
77 Ok(res)
78}
79
80#[allow(clippy::needless_pass_by_value)]
82pub fn get_compare_commits_diff(
83 repo: &Repository,
84 ids: OldNew<CommitId>,
85 pathspec: Option<String>,
86 options: Option<DiffOptions>,
87) -> Result<Diff<'_>> {
88 let commits = OldNew {
90 old: repo.find_commit(ids.old.into())?,
91 new: repo.find_commit(ids.new.into())?,
92 };
93
94 let trees = OldNew {
95 old: commits.old.tree()?,
96 new: commits.new.tree()?,
97 };
98
99 let mut opts = git2::DiffOptions::new();
100 if let Some(options) = options {
101 opts.context_lines(options.context);
102 opts.ignore_whitespace(options.ignore_whitespace);
103 opts.interhunk_lines(options.interhunk_lines);
104 }
105 if let Some(p) = &pathspec {
106 opts.pathspec(p.clone());
107 }
108
109 let diff: Diff<'_> =
110 repo.diff_tree_to_tree(Some(&trees.old), Some(&trees.new), Some(&mut opts))?;
111
112 Ok(diff)
113}
114
115pub(crate) fn get_commit_diff<'a>(
117 repo: &'a Repository,
118 id: CommitId,
119 pathspec: Option<String>,
120 options: Option<DiffOptions>,
121 stashes: Option<&HashSet<CommitId>>,
122) -> Result<Diff<'a>> {
123 let commit = repo.find_commit(id.into())?;
126 let commit_tree = commit.tree()?;
127
128 let parent = if commit.parent_count() > 0 {
129 repo.find_commit(commit.parent_id(0)?)
130 .ok()
131 .and_then(|c| c.tree().ok())
132 } else {
133 None
134 };
135
136 let mut opts = git2::DiffOptions::new();
137 if let Some(options) = options {
138 opts.context_lines(options.context);
139 opts.ignore_whitespace(options.ignore_whitespace);
140 opts.interhunk_lines(options.interhunk_lines);
141 }
142 if let Some(p) = &pathspec {
143 opts.pathspec(p.clone());
144 }
145 opts.show_binary(true);
146
147 let mut diff = repo.diff_tree_to_tree(parent.as_ref(), Some(&commit_tree), Some(&mut opts))?;
148
149 if stashes.is_some_and(|stashes| stashes.contains(&id)) {
150 if let Ok(untracked_commit) = commit.parent_id(2) {
151 let untracked_diff = get_commit_diff(
152 repo,
153 CommitId::new(untracked_commit),
154 pathspec,
155 options,
156 stashes,
157 )?;
158
159 diff.merge(&untracked_diff)?;
160 }
161 }
162
163 Ok(diff)
164}
165
166#[cfg(test)]
167mod tests {
168 use std::{fs::File, io::Write, path::Path};
169
170 use super::get_commit_files;
171 use crate::{
172 error::Result,
173 sync::{
174 commit, stage_add_file, stash_save,
175 tests::{get_statuses, repo_init},
176 RepoPath,
177 },
178 StatusItemType,
179 };
180
181 #[test]
182 fn test_smoke() -> Result<()> {
183 let file_path = Path::new("file1.txt");
184 let (_td, repo) = repo_init()?;
185 let root = repo.path().parent().unwrap();
186 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
187
188 File::create(root.join(file_path))?.write_all(b"test file1 content")?;
189
190 stage_add_file(repo_path, file_path)?;
191
192 let id = commit(repo_path, "commit msg")?;
193
194 let diff = get_commit_files(repo_path, id, None)?;
195
196 assert_eq!(diff.len(), 1);
197 assert_eq!(diff[0].status, StatusItemType::New);
198
199 Ok(())
200 }
201
202 #[test]
203 fn test_stashed_untracked() -> Result<()> {
204 let file_path = Path::new("file1.txt");
205 let (_td, repo) = repo_init()?;
206 let root = repo.path().parent().unwrap();
207 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
208
209 File::create(root.join(file_path))?.write_all(b"test file1 content")?;
210
211 let id = stash_save(repo_path, None, true, false)?;
212
213 let diff = get_commit_files(repo_path, id, None)?;
214
215 assert_eq!(diff.len(), 1);
216 assert_eq!(diff[0].status, StatusItemType::New);
217
218 Ok(())
219 }
220
221 #[test]
222 fn test_stashed_untracked_and_modified() -> Result<()> {
223 let file_path1 = Path::new("file1.txt");
224 let file_path2 = Path::new("file2.txt");
225 let (_td, repo) = repo_init()?;
226 let root = repo.path().parent().unwrap();
227 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
228
229 File::create(root.join(file_path1))?.write_all(b"test")?;
230 stage_add_file(repo_path, file_path1)?;
231 commit(repo_path, "c1")?;
232
233 File::create(root.join(file_path1))?.write_all(b"modified")?;
234 File::create(root.join(file_path2))?.write_all(b"new")?;
235
236 assert_eq!(get_statuses(repo_path), (2, 0));
237
238 let id = stash_save(repo_path, None, true, false)?;
239
240 let diff = get_commit_files(repo_path, id, None)?;
241
242 assert_eq!(diff.len(), 2);
243 assert_eq!(diff[0].status, StatusItemType::Modified);
244 assert_eq!(diff[1].status, StatusItemType::New);
245
246 Ok(())
247 }
248}