gnostr_asyncgit/sync/
logwalker.rs

1#![allow(dead_code)]
2use std::{
3    cmp::Ordering,
4    collections::{BinaryHeap, HashSet},
5};
6
7use git2::{Commit, Oid, Repository};
8
9use super::{CommitId, SharedCommitFilterFn};
10use crate::error::Result;
11
12struct TimeOrderedCommit<'a>(Commit<'a>);
13
14impl<'a> Eq for TimeOrderedCommit<'a> {}
15
16impl<'a> PartialEq for TimeOrderedCommit<'a> {
17    fn eq(&self, other: &Self) -> bool {
18        self.0.time().eq(&other.0.time())
19    }
20}
21
22impl<'a> PartialOrd for TimeOrderedCommit<'a> {
23    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
24        Some(self.cmp(other))
25    }
26}
27
28impl<'a> Ord for TimeOrderedCommit<'a> {
29    fn cmp(&self, other: &Self) -> Ordering {
30        self.0.time().cmp(&other.0.time())
31    }
32}
33
34///
35pub struct LogWalker<'a> {
36    commits: BinaryHeap<TimeOrderedCommit<'a>>,
37    visited: HashSet<Oid>,
38    limit: usize,
39    repo: &'a Repository,
40    filter: Option<SharedCommitFilterFn>,
41}
42
43impl<'a> LogWalker<'a> {
44    ///
45    pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {
46        let c = repo.head()?.peel_to_commit()?;
47
48        let mut commits = BinaryHeap::with_capacity(10);
49        commits.push(TimeOrderedCommit(c));
50
51        Ok(Self {
52            commits,
53            limit,
54            visited: HashSet::with_capacity(1000),
55            repo,
56            filter: None,
57        })
58    }
59
60    ///
61    pub fn visited(&self) -> usize {
62        self.visited.len()
63    }
64
65    ///
66    #[must_use]
67    pub fn filter(self, filter: Option<SharedCommitFilterFn>) -> Self {
68        Self { filter, ..self }
69    }
70
71    ///
72    pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
73        let mut count = 0_usize;
74
75        while let Some(c) = self.commits.pop() {
76            for p in c.0.parents() {
77                self.visit(p);
78            }
79
80            let id: CommitId = c.0.id().into();
81            let commit_should_be_included = if let Some(ref filter) = self.filter {
82                filter(self.repo, &id)?
83            } else {
84                true
85            };
86
87            if commit_should_be_included {
88                out.push(id);
89            }
90
91            count += 1;
92            if count == self.limit {
93                break;
94            }
95        }
96
97        Ok(count)
98    }
99
100    //
101    fn visit(&mut self, c: Commit<'a>) {
102        if !self.visited.contains(&c.id()) {
103            self.visited.insert(c.id());
104            self.commits.push(TimeOrderedCommit(c));
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use std::{fs::File, io::Write, path::Path};
112
113    use pretty_assertions::assert_eq;
114
115    use super::*;
116    use crate::{
117        error::Result,
118        sync::{
119            commit,
120            commit_filter::{SearchFields, SearchOptions},
121            diff_contains_file, filter_commit_by_search, get_commits_info, stage_add_file,
122            tests::{repo_init_empty, write_commit_file},
123            LogFilterSearch, LogFilterSearchOptions, RepoPath,
124        },
125    };
126
127    #[test]
128    fn test_limit() -> Result<()> {
129        let file_path = Path::new("foo");
130        let (_td, repo) = repo_init_empty().unwrap();
131        let root = repo.path().parent().unwrap();
132        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
133
134        File::create(root.join(file_path))?.write_all(b"a")?;
135        stage_add_file(repo_path, file_path).unwrap();
136        commit(repo_path, "commit1").unwrap();
137        File::create(root.join(file_path))?.write_all(b"a")?;
138        stage_add_file(repo_path, file_path).unwrap();
139        let oid2 = commit(repo_path, "commit2").unwrap();
140
141        let mut items = Vec::new();
142        let mut walk = LogWalker::new(&repo, 1)?;
143        walk.read(&mut items).unwrap();
144
145        assert_eq!(items.len(), 1);
146        assert_eq!(items[0], oid2);
147
148        Ok(())
149    }
150
151    #[test]
152    fn test_logwalker() -> Result<()> {
153        let file_path = Path::new("foo");
154        let (_td, repo) = repo_init_empty().unwrap();
155        let root = repo.path().parent().unwrap();
156        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
157
158        File::create(root.join(file_path))?.write_all(b"a")?;
159        stage_add_file(repo_path, file_path).unwrap();
160        commit(repo_path, "commit1").unwrap();
161        File::create(root.join(file_path))?.write_all(b"a")?;
162        stage_add_file(repo_path, file_path).unwrap();
163        let oid2 = commit(repo_path, "commit2").unwrap();
164
165        let mut items = Vec::new();
166        let mut walk = LogWalker::new(&repo, 100)?;
167        walk.read(&mut items).unwrap();
168
169        let info = get_commits_info(repo_path, &items, 50).unwrap();
170        dbg!(&info);
171
172        assert_eq!(items.len(), 2);
173        assert_eq!(items[0], oid2);
174
175        let mut items = Vec::new();
176        walk.read(&mut items).unwrap();
177
178        assert_eq!(items.len(), 0);
179
180        Ok(())
181    }
182
183    #[test]
184    fn test_logwalker_with_filter() -> Result<()> {
185        let file_path = Path::new("foo");
186        let second_file_path = Path::new("baz");
187        let (_td, repo) = repo_init_empty().unwrap();
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"a")?;
192        stage_add_file(&repo_path, file_path).unwrap();
193
194        let _first_commit_id = commit(&repo_path, "commit1").unwrap();
195
196        File::create(root.join(second_file_path))?.write_all(b"a")?;
197        stage_add_file(&repo_path, second_file_path).unwrap();
198
199        let second_commit_id = commit(&repo_path, "commit2").unwrap();
200
201        File::create(root.join(file_path))?.write_all(b"b")?;
202        stage_add_file(&repo_path, file_path).unwrap();
203
204        let _third_commit_id = commit(&repo_path, "commit3").unwrap();
205
206        let diff_contains_baz = diff_contains_file("baz".into());
207
208        let mut items = Vec::new();
209        let mut walker = LogWalker::new(&repo, 100)?.filter(Some(diff_contains_baz));
210        walker.read(&mut items).unwrap();
211
212        assert_eq!(items.len(), 1);
213        assert_eq!(items[0], second_commit_id);
214
215        let mut items = Vec::new();
216        walker.read(&mut items).unwrap();
217
218        assert_eq!(items.len(), 0);
219
220        let diff_contains_bar = diff_contains_file("bar".into());
221
222        let mut items = Vec::new();
223        let mut walker = LogWalker::new(&repo, 100)?.filter(Some(diff_contains_bar));
224        walker.read(&mut items).unwrap();
225
226        assert_eq!(items.len(), 0);
227
228        Ok(())
229    }
230
231    #[test]
232    fn test_logwalker_with_filter_search() {
233        let (_td, repo) = repo_init_empty().unwrap();
234
235        write_commit_file(&repo, "foo", "a", "commit1");
236        let second_commit_id = write_commit_file(&repo, "baz", "a", "my commit msg (#2)");
237        write_commit_file(&repo, "foo", "b", "commit3");
238
239        let log_filter = filter_commit_by_search(LogFilterSearch::new(LogFilterSearchOptions {
240            fields: SearchFields::MESSAGE_SUMMARY,
241            options: SearchOptions::FUZZY_SEARCH,
242            search_pattern: String::from("my msg"),
243        }));
244
245        let mut items = Vec::new();
246        let mut walker = LogWalker::new(&repo, 100).unwrap().filter(Some(log_filter));
247        walker.read(&mut items).unwrap();
248
249        assert_eq!(items.len(), 1);
250        assert_eq!(items[0], second_commit_id);
251
252        let log_filter = filter_commit_by_search(LogFilterSearch::new(LogFilterSearchOptions {
253            fields: SearchFields::FILENAMES,
254            options: SearchOptions::FUZZY_SEARCH,
255            search_pattern: String::from("fo"),
256        }));
257
258        let mut items = Vec::new();
259        let mut walker = LogWalker::new(&repo, 100).unwrap().filter(Some(log_filter));
260        walker.read(&mut items).unwrap();
261
262        assert_eq!(items.len(), 2);
263    }
264}