gnostr_asyncgit/sync/
commit_files.rs

1//! Functions for getting infos about files in commits
2
3use 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/// struct containing a new and an old version
16#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
17pub struct OldNew<T> {
18    /// The old version
19    pub old: T,
20    /// The new version
21    pub new: T,
22}
23
24/// Sort two commits.
25pub 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
39/// get all files that are part of a commit
40pub 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/// get diff of two arbitrary commits
81#[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    // scope_time!("get_compare_commits_diff");
89    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
115/// get diff of a commit to its first parent
116/// get diff of a commit to its first parent
117/// get diff of a commit to its first parent
118/// get diff of a commit to its first parent
119pub(crate) fn get_commit_diff<'a>(
120    repo: &'a Repository,
121    id: CommitId,
122    pathspec: Option<String>,
123    options: Option<DiffOptions>,
124    stashes: Option<&HashSet<CommitId>>,
125) -> Result<Diff<'a>> {
126    // scope_time!("get_commit_diff");
127
128    let commit = repo.find_commit(id.into())?;
129    let commit_tree = commit.tree()?;
130
131    let parent = if commit.parent_count() > 0 {
132        repo.find_commit(commit.parent_id(0)?)
133            .ok()
134            .and_then(|c| c.tree().ok())
135    } else {
136        None
137    };
138
139    let mut opts = git2::DiffOptions::new();
140    if let Some(options) = options {
141        opts.context_lines(options.context);
142        opts.ignore_whitespace(options.ignore_whitespace);
143        opts.interhunk_lines(options.interhunk_lines);
144    }
145    if let Some(p) = &pathspec {
146        opts.pathspec(p.clone());
147    }
148    opts.show_binary(true);
149
150    let mut diff = repo.diff_tree_to_tree(parent.as_ref(), Some(&commit_tree), Some(&mut opts))?;
151
152    if stashes.is_some_and(|stashes| stashes.contains(&id)) {
153        if let Ok(untracked_commit) = commit.parent_id(2) {
154            let untracked_diff = get_commit_diff(
155                repo,
156                CommitId::new(untracked_commit),
157                pathspec,
158                options,
159                stashes,
160            )?;
161
162            diff.merge(&untracked_diff)?;
163        }
164    }
165
166    Ok(diff)
167}
168
169#[cfg(test)]
170mod tests {
171    use std::{fs::File, io::Write, path::Path};
172
173    use super::get_commit_files;
174    use crate::{
175        error::Result,
176        sync::{
177            commit, stage_add_file, stash_save,
178            tests::{get_statuses, repo_init},
179            RepoPath,
180        },
181        StatusItemType,
182    };
183
184    #[test]
185    fn test_smoke() -> Result<()> {
186        let file_path = Path::new("file1.txt");
187        let (_td, repo) = repo_init()?;
188        let root = repo.path().parent().unwrap();
189        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
190
191        File::create(root.join(file_path))?.write_all(b"test file1 content")?;
192
193        stage_add_file(repo_path, file_path)?;
194
195        let id = commit(repo_path, "commit msg")?;
196
197        let diff = get_commit_files(repo_path, id, None)?;
198
199        assert_eq!(diff.len(), 1);
200        assert_eq!(diff[0].status, StatusItemType::New);
201
202        Ok(())
203    }
204
205    #[test]
206    fn test_stashed_untracked() -> Result<()> {
207        let file_path = Path::new("file1.txt");
208        let (_td, repo) = repo_init()?;
209        let root = repo.path().parent().unwrap();
210        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
211
212        File::create(root.join(file_path))?.write_all(b"test file1 content")?;
213
214        let id = stash_save(repo_path, None, true, false)?;
215
216        let diff = get_commit_files(repo_path, id, None)?;
217
218        assert_eq!(diff.len(), 1);
219        assert_eq!(diff[0].status, StatusItemType::New);
220
221        Ok(())
222    }
223
224    #[test]
225    fn test_stashed_untracked_and_modified() -> Result<()> {
226        let file_path1 = Path::new("file1.txt");
227        let file_path2 = Path::new("file2.txt");
228        let (_td, repo) = repo_init()?;
229        let root = repo.path().parent().unwrap();
230        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
231
232        File::create(root.join(file_path1))?.write_all(b"test")?;
233        stage_add_file(repo_path, file_path1)?;
234        commit(repo_path, "c1")?;
235
236        File::create(root.join(file_path1))?.write_all(b"modified")?;
237        File::create(root.join(file_path2))?.write_all(b"new")?;
238
239        assert_eq!(get_statuses(repo_path), (2, 0));
240
241        let id = stash_save(repo_path, None, true, false)?;
242
243        let diff = get_commit_files(repo_path, id, None)?;
244
245        assert_eq!(diff.len(), 2);
246        assert_eq!(diff[0].status, StatusItemType::Modified);
247        assert_eq!(diff[1].status, StatusItemType::New);
248
249        Ok(())
250    }
251}