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
116pub(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    // scope_time!("get_commit_diff");
124
125    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}