asyncgit/sync/
commit_filter.rs1use super::{
2 commit_details::get_author_of_commit,
3 commit_files::get_commit_diff, CommitId,
4};
5use crate::error::Result;
6use bitflags::bitflags;
7use fuzzy_matcher::FuzzyMatcher;
8use git2::{Diff, Repository};
9use std::sync::Arc;
10
11pub type SharedCommitFilterFn = Arc<
13 Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
14>;
15
16pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
18 Arc::new(Box::new(
19 move |repo: &Repository,
20 commit_id: &CommitId|
21 -> Result<bool> {
22 let diff = get_commit_diff(
23 repo,
24 *commit_id,
25 Some(file_path.clone()),
26 None,
27 None,
28 )?;
29
30 let contains_file = diff.deltas().len() > 0;
31
32 Ok(contains_file)
33 },
34 ))
35}
36
37bitflags! {
38 #[derive(Debug, Clone, Copy)]
40 pub struct SearchFields: u32 {
41 const MESSAGE_SUMMARY = 1 << 0;
43 const MESSAGE_BODY = 1 << 1;
45 const FILENAMES = 1 << 2;
47 const AUTHORS = 1 << 3;
49 }
56}
57
58impl Default for SearchFields {
59 fn default() -> Self {
60 Self::MESSAGE_SUMMARY
61 }
62}
63
64bitflags! {
65 #[derive(Debug, Clone, Copy)]
67 pub struct SearchOptions: u32 {
68 const CASE_SENSITIVE = 1 << 0;
70 const FUZZY_SEARCH = 1 << 1;
72 }
73}
74
75impl Default for SearchOptions {
76 fn default() -> Self {
77 Self::empty()
78 }
79}
80
81#[derive(Default, Debug, Clone)]
83pub struct LogFilterSearchOptions {
84 pub search_pattern: String,
86 pub fields: SearchFields,
88 pub options: SearchOptions,
90}
91
92#[derive(Default)]
94pub struct LogFilterSearch {
95 pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
97 pub options: LogFilterSearchOptions,
99}
100
101impl LogFilterSearch {
102 pub fn new(options: LogFilterSearchOptions) -> Self {
104 let mut options = options;
105 if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
106 options.search_pattern =
107 options.search_pattern.to_lowercase();
108 }
109 Self {
110 matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
111 options,
112 }
113 }
114
115 fn match_diff(&self, diff: &Diff<'_>) -> bool {
116 diff.deltas().any(|delta| {
117 if delta
118 .new_file()
119 .path()
120 .and_then(|file| file.as_os_str().to_str())
121 .is_some_and(|file| self.match_text(file))
122 {
123 return true;
124 }
125
126 delta
127 .old_file()
128 .path()
129 .and_then(|file| file.as_os_str().to_str())
130 .is_some_and(|file| self.match_text(file))
131 })
132 }
133
134 pub fn match_text(&self, text: &str) -> bool {
136 if self.options.options.contains(SearchOptions::FUZZY_SEARCH)
137 {
138 self.matcher
139 .fuzzy_match(
140 text,
141 self.options.search_pattern.as_str(),
142 )
143 .is_some()
144 } else if self
145 .options
146 .options
147 .contains(SearchOptions::CASE_SENSITIVE)
148 {
149 text.contains(self.options.search_pattern.as_str())
150 } else {
151 text.to_lowercase()
152 .contains(self.options.search_pattern.as_str())
153 }
154 }
155}
156
157pub fn filter_commit_by_search(
159 filter: LogFilterSearch,
160) -> SharedCommitFilterFn {
161 Arc::new(Box::new(
162 move |repo: &Repository,
163 commit_id: &CommitId|
164 -> Result<bool> {
165 let mailmap = repo.mailmap()?;
166 let commit = repo.find_commit((*commit_id).into())?;
167
168 let msg_summary_match = filter
169 .options
170 .fields
171 .contains(SearchFields::MESSAGE_SUMMARY)
172 .then(|| {
173 commit.summary().map(|msg| filter.match_text(msg))
174 })
175 .flatten()
176 .unwrap_or_default();
177
178 let msg_body_match = filter
179 .options
180 .fields
181 .contains(SearchFields::MESSAGE_BODY)
182 .then(|| {
183 commit.body().map(|msg| filter.match_text(msg))
184 })
185 .flatten()
186 .unwrap_or_default();
187
188 let file_match = filter
189 .options
190 .fields
191 .contains(SearchFields::FILENAMES)
192 .then(|| {
193 get_commit_diff(
194 repo, *commit_id, None, None, None,
195 )
196 .ok()
197 })
198 .flatten()
199 .is_some_and(|diff| filter.match_diff(&diff));
200
201 let authors_match = if filter
202 .options
203 .fields
204 .contains(SearchFields::AUTHORS)
205 {
206 let author = get_author_of_commit(&commit, &mailmap);
207 [author.email(), author.name()].iter().any(
208 |opt_haystack| {
209 opt_haystack.is_some_and(|haystack| {
210 filter.match_text(haystack)
211 })
212 },
213 )
214 } else {
215 false
216 };
217
218 Ok(msg_summary_match
219 || msg_body_match
220 || file_match
221 || authors_match)
222 },
223 ))
224}