gnostr_asyncgit/sync/
commit_filter.rs

1use std::sync::Arc;
2
3use bitflags::bitflags;
4use fuzzy_matcher::FuzzyMatcher;
5use git2::{Diff, Repository};
6
7use super::{commit_files::get_commit_diff, CommitId};
8use crate::error::Result;
9
10///
11pub type SharedCommitFilterFn =
12    Arc<Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>>;
13
14///
15pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
16    Arc::new(Box::new(
17        move |repo: &Repository, commit_id: &CommitId| -> Result<bool> {
18            let diff = get_commit_diff(repo, *commit_id, Some(file_path.clone()), None, None)?;
19
20            let contains_file = diff.deltas().len() > 0;
21
22            Ok(contains_file)
23        },
24    ))
25}
26
27bitflags! {
28    ///
29    #[derive(Debug, Clone, Copy)]
30    pub struct SearchFields: u32 {
31        ///
32        const MESSAGE_SUMMARY = 1 << 0;
33        ///
34        const MESSAGE_BODY = 1 << 1;
35        ///
36        const FILENAMES = 1 << 2;
37        ///
38        const AUTHORS = 1 << 3;
39        //TODO:
40        // const COMMIT_HASHES = 1 << 3;
41        // ///
42        // const DATES = 1 << 4;
43        // ///
44        // const DIFFS = 1 << 5;
45    }
46}
47
48impl Default for SearchFields {
49    fn default() -> Self {
50        Self::MESSAGE_SUMMARY
51    }
52}
53
54bitflags! {
55    ///
56    #[derive(Debug, Clone, Copy)]
57    pub struct SearchOptions: u32 {
58        ///
59        const CASE_SENSITIVE = 1 << 0;
60        ///
61        const FUZZY_SEARCH = 1 << 1;
62    }
63}
64
65impl Default for SearchOptions {
66    fn default() -> Self {
67        Self::empty()
68    }
69}
70
71///
72#[derive(Default, Debug, Clone)]
73pub struct LogFilterSearchOptions {
74    ///
75    pub search_pattern: String,
76    ///
77    pub fields: SearchFields,
78    ///
79    pub options: SearchOptions,
80}
81
82///
83#[derive(Default)]
84pub struct LogFilterSearch {
85    ///
86    pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
87    ///
88    pub options: LogFilterSearchOptions,
89}
90
91impl LogFilterSearch {
92    ///
93    pub fn new(options: LogFilterSearchOptions) -> Self {
94        let mut options = options;
95        if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
96            options.search_pattern = options.search_pattern.to_lowercase();
97        }
98        Self {
99            matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
100            options,
101        }
102    }
103
104    fn match_diff(&self, diff: &Diff<'_>) -> bool {
105        diff.deltas().any(|delta| {
106            if delta
107                .new_file()
108                .path()
109                .and_then(|file| file.as_os_str().to_str())
110                .is_some_and(|file| self.match_text(file))
111            {
112                return true;
113            }
114
115            delta
116                .old_file()
117                .path()
118                .and_then(|file| file.as_os_str().to_str())
119                .is_some_and(|file| self.match_text(file))
120        })
121    }
122
123    ///
124    pub fn match_text(&self, text: &str) -> bool {
125        if self.options.options.contains(SearchOptions::FUZZY_SEARCH) {
126            self.matcher
127                .fuzzy_match(text, self.options.search_pattern.as_str())
128                .is_some()
129        } else if self.options.options.contains(SearchOptions::CASE_SENSITIVE) {
130            text.contains(self.options.search_pattern.as_str())
131        } else {
132            text.to_lowercase()
133                .contains(self.options.search_pattern.as_str())
134        }
135    }
136}
137
138///
139pub fn filter_commit_by_search(filter: LogFilterSearch) -> SharedCommitFilterFn {
140    Arc::new(Box::new(
141        move |repo: &Repository, commit_id: &CommitId| -> Result<bool> {
142            let commit = repo.find_commit((*commit_id).into())?;
143
144            let msg_summary_match = filter
145                .options
146                .fields
147                .contains(SearchFields::MESSAGE_SUMMARY)
148                .then(|| commit.summary().map(|msg| filter.match_text(msg)))
149                .flatten()
150                .unwrap_or_default();
151
152            let msg_body_match = filter
153                .options
154                .fields
155                .contains(SearchFields::MESSAGE_BODY)
156                .then(|| commit.body().map(|msg| filter.match_text(msg)))
157                .flatten()
158                .unwrap_or_default();
159
160            let file_match = filter
161                .options
162                .fields
163                .contains(SearchFields::FILENAMES)
164                .then(|| get_commit_diff(repo, *commit_id, None, None, None).ok())
165                .flatten()
166                .is_some_and(|diff| filter.match_diff(&diff));
167
168            let authors_match = filter
169                .options
170                .fields
171                .contains(SearchFields::AUTHORS)
172                .then(|| {
173                    let name_match = commit
174                        .author()
175                        .name()
176                        .is_some_and(|name| filter.match_text(name));
177                    let mail_match = commit
178                        .author()
179                        .email()
180                        .is_some_and(|name| filter.match_text(name));
181
182                    name_match || mail_match
183                })
184                .unwrap_or_default();
185
186            Ok(msg_summary_match || msg_body_match || file_match || authors_match)
187        },
188    ))
189}