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
34pub 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 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 pub fn visited(&self) -> usize {
62 self.visited.len()
63 }
64
65 #[must_use]
67 pub fn filter(self, filter: Option<SharedCommitFilterFn>) -> Self {
68 Self { filter, ..self }
69 }
70
71 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 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}