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}