use super::{commit_files::get_commit_diff, CommitId};
use crate::error::Result;
use bitflags::bitflags;
use fuzzy_matcher::FuzzyMatcher;
use git2::{Diff, Repository};
use std::sync::Arc;
pub type SharedCommitFilterFn = Arc<
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
>;
pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
repo,
*commit_id,
Some(file_path.clone()),
None,
None,
)?;
let contains_file = diff.deltas().len() > 0;
Ok(contains_file)
},
))
}
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct SearchFields: u32 {
const MESSAGE_SUMMARY = 1 << 0;
const MESSAGE_BODY = 1 << 1;
const FILENAMES = 1 << 2;
const AUTHORS = 1 << 3;
}
}
impl Default for SearchFields {
fn default() -> Self {
Self::MESSAGE_SUMMARY
}
}
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct SearchOptions: u32 {
const CASE_SENSITIVE = 1 << 0;
const FUZZY_SEARCH = 1 << 1;
}
}
impl Default for SearchOptions {
fn default() -> Self {
Self::empty()
}
}
#[derive(Default, Debug, Clone)]
pub struct LogFilterSearchOptions {
pub search_pattern: String,
pub fields: SearchFields,
pub options: SearchOptions,
}
#[derive(Default)]
pub struct LogFilterSearch {
pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
pub options: LogFilterSearchOptions,
}
impl LogFilterSearch {
pub fn new(options: LogFilterSearchOptions) -> Self {
let mut options = options;
if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
options.search_pattern =
options.search_pattern.to_lowercase();
}
Self {
matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
options,
}
}
fn match_diff(&self, diff: &Diff<'_>) -> bool {
diff.deltas().any(|delta| {
if delta
.new_file()
.path()
.and_then(|file| file.as_os_str().to_str())
.is_some_and(|file| self.match_text(file))
{
return true;
}
delta
.old_file()
.path()
.and_then(|file| file.as_os_str().to_str())
.is_some_and(|file| self.match_text(file))
})
}
pub fn match_text(&self, text: &str) -> bool {
if self.options.options.contains(SearchOptions::FUZZY_SEARCH)
{
self.matcher
.fuzzy_match(
text,
self.options.search_pattern.as_str(),
)
.is_some()
} else if self
.options
.options
.contains(SearchOptions::CASE_SENSITIVE)
{
text.contains(self.options.search_pattern.as_str())
} else {
text.to_lowercase()
.contains(self.options.search_pattern.as_str())
}
}
}
pub fn filter_commit_by_search(
filter: LogFilterSearch,
) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let commit = repo.find_commit((*commit_id).into())?;
let msg_summary_match = filter
.options
.fields
.contains(SearchFields::MESSAGE_SUMMARY)
.then(|| {
commit.summary().map(|msg| filter.match_text(msg))
})
.flatten()
.unwrap_or_default();
let msg_body_match = filter
.options
.fields
.contains(SearchFields::MESSAGE_BODY)
.then(|| {
commit.body().map(|msg| filter.match_text(msg))
})
.flatten()
.unwrap_or_default();
let file_match = filter
.options
.fields
.contains(SearchFields::FILENAMES)
.then(|| {
get_commit_diff(
repo, *commit_id, None, None, None,
)
.ok()
})
.flatten()
.is_some_and(|diff| filter.match_diff(&diff));
let authors_match = filter
.options
.fields
.contains(SearchFields::AUTHORS)
.then(|| {
let name_match = commit
.author()
.name()
.is_some_and(|name| filter.match_text(name));
let mail_match = commit
.author()
.email()
.is_some_and(|name| filter.match_text(name));
name_match || mail_match
})
.unwrap_or_default();
Ok(msg_summary_match
|| msg_body_match
|| file_match
|| authors_match)
},
))
}