asyncgit/sync/
commit_filter.rs

1use 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
11///
12pub type SharedCommitFilterFn = Arc<
13	Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
14>;
15
16///
17pub 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	///
39	#[derive(Debug, Clone, Copy)]
40	pub struct SearchFields: u32 {
41		///
42		const MESSAGE_SUMMARY = 1 << 0;
43		///
44		const MESSAGE_BODY = 1 << 1;
45		///
46		const FILENAMES = 1 << 2;
47		///
48		const AUTHORS = 1 << 3;
49		//TODO:
50		// const COMMIT_HASHES = 1 << 3;
51		// ///
52		// const DATES = 1 << 4;
53		// ///
54		// const DIFFS = 1 << 5;
55	}
56}
57
58impl Default for SearchFields {
59	fn default() -> Self {
60		Self::MESSAGE_SUMMARY
61	}
62}
63
64bitflags! {
65	///
66	#[derive(Debug, Clone, Copy)]
67	pub struct SearchOptions: u32 {
68		///
69		const CASE_SENSITIVE = 1 << 0;
70		///
71		const FUZZY_SEARCH = 1 << 1;
72	}
73}
74
75impl Default for SearchOptions {
76	fn default() -> Self {
77		Self::empty()
78	}
79}
80
81///
82#[derive(Default, Debug, Clone)]
83pub struct LogFilterSearchOptions {
84	///
85	pub search_pattern: String,
86	///
87	pub fields: SearchFields,
88	///
89	pub options: SearchOptions,
90}
91
92///
93#[derive(Default)]
94pub struct LogFilterSearch {
95	///
96	pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
97	///
98	pub options: LogFilterSearchOptions,
99}
100
101impl LogFilterSearch {
102	///
103	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	///
135	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
157///
158pub 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}