Skip to main content

gitgraph_core/
search.rs

1use regex::RegexBuilder;
2
3use crate::error::{GitLgError, Result};
4use crate::models::{CommitSearchQuery, GraphRow};
5
6pub fn filter_commits(rows: &[GraphRow], query: &CommitSearchQuery) -> Result<Vec<GraphRow>> {
7    let needle = query.text.trim();
8    if needle.is_empty() {
9        return Ok(rows.to_vec());
10    }
11
12    if query.use_regex {
13        let regex = RegexBuilder::new(needle)
14            .case_insensitive(!query.case_sensitive)
15            .build()
16            .map_err(|e| GitLgError::Parse(format!("invalid regex {:?}: {}", needle, e)))?;
17        return Ok(rows
18            .iter()
19            .filter(|row| search_parts(row, query, |part| regex.is_match(part)))
20            .cloned()
21            .collect());
22    }
23
24    if query.case_sensitive {
25        return Ok(rows
26            .iter()
27            .filter(|row| search_parts(row, query, |part| part.contains(needle)))
28            .cloned()
29            .collect());
30    }
31
32    let normalized_needle = needle.to_lowercase();
33    Ok(rows
34        .iter()
35        .filter(|row| {
36            search_parts(row, query, |part| {
37                part.to_lowercase().contains(&normalized_needle)
38            })
39        })
40        .cloned()
41        .collect())
42}
43
44fn search_parts(
45    row: &GraphRow,
46    query: &CommitSearchQuery,
47    mut matches: impl FnMut(&str) -> bool,
48) -> bool {
49    if query.include_hash && (matches(row.hash.as_str()) || matches(row.short_hash.as_str())) {
50        return true;
51    }
52    if query.include_subject && matches(row.subject.as_str()) {
53        return true;
54    }
55    if query.include_body && matches(row.body.as_str()) {
56        return true;
57    }
58    if query.include_author && matches(row.author_name.as_str()) {
59        return true;
60    }
61    if query.include_email && matches(row.author_email.as_str()) {
62        return true;
63    }
64    if query.include_refs {
65        for git_ref in &row.refs {
66            if matches(git_ref.name.as_str()) {
67                return true;
68            }
69            if let Some(target) = git_ref.target.as_deref()
70                && matches(target)
71            {
72                return true;
73            }
74        }
75    }
76    false
77}
78
79#[cfg(test)]
80mod tests {
81    use crate::models::{CommitSearchQuery, GitRef, GitRefKind, GraphRow};
82
83    use super::filter_commits;
84
85    fn sample_rows() -> Vec<GraphRow> {
86        vec![
87            GraphRow {
88                hash: "aaaaaaaa".to_string(),
89                short_hash: "aaaaaaa".to_string(),
90                parents: vec!["bbbbbbbb".to_string()],
91                author_name: "Alice".to_string(),
92                author_email: "alice@example.com".to_string(),
93                authored_unix: 10,
94                committed_unix: 10,
95                subject: "Add parser".to_string(),
96                body: "Adds commit parser".to_string(),
97                refs: vec![GitRef {
98                    kind: GitRefKind::LocalBranch,
99                    name: "main".to_string(),
100                    target: None,
101                }],
102                lane: 0,
103                active_lane_count: 1,
104                edges: vec![],
105            },
106            GraphRow {
107                hash: "cccccccc".to_string(),
108                short_hash: "ccccccc".to_string(),
109                parents: vec!["bbbbbbbb".to_string()],
110                author_name: "Bob".to_string(),
111                author_email: "bob@example.com".to_string(),
112                authored_unix: 11,
113                committed_unix: 11,
114                subject: "Fix ui".to_string(),
115                body: "Nothing about parser".to_string(),
116                refs: vec![],
117                lane: 0,
118                active_lane_count: 1,
119                edges: vec![],
120            },
121        ]
122    }
123
124    #[test]
125    fn filters_substring_case_insensitive() {
126        let mut q = CommitSearchQuery::default();
127        q.text = "PARSER".to_string();
128        let filtered = filter_commits(&sample_rows(), &q).expect("search");
129        assert_eq!(filtered.len(), 2);
130    }
131
132    #[test]
133    fn filters_regex() {
134        let mut q = CommitSearchQuery::default();
135        q.use_regex = true;
136        q.text = "^Fix\\s".to_string();
137        let filtered = filter_commits(&sample_rows(), &q).expect("search");
138        assert_eq!(filtered.len(), 1);
139        assert_eq!(filtered[0].author_name, "Bob");
140    }
141}