gitkraft_core/features/log/
ops.rs1use anyhow::{Context, Result};
4use git2::Repository;
5
6use crate::features::commits::CommitInfo;
7
8pub fn get_log(
13 repo: &Repository,
14 max_count: usize,
15 filter_author: Option<&str>,
16 filter_message: Option<&str>,
17) -> Result<Vec<CommitInfo>> {
18 let mut revwalk = repo.revwalk().context("failed to create revwalk")?;
19 revwalk.push_head().context("failed to push HEAD")?;
20 revwalk
21 .set_sorting(git2::Sort::TIME | git2::Sort::TOPOLOGICAL)
22 .context("failed to set sorting")?;
23
24 let author_lower = filter_author.map(|s| s.to_lowercase());
25 let message_lower = filter_message.map(|s| s.to_lowercase());
26
27 let mut results = Vec::with_capacity(max_count.min(256));
28
29 for oid_result in revwalk {
30 if results.len() >= max_count {
31 break;
32 }
33
34 let oid = oid_result.context("revwalk iteration error")?;
35 let commit = repo
36 .find_commit(oid)
37 .context("failed to find commit during log walk")?;
38
39 if let Some(ref needle) = author_lower {
41 let author = commit.author();
42 let name = author.name().unwrap_or("").to_lowercase();
43 let email = author.email().unwrap_or("").to_lowercase();
44 if !name.contains(needle.as_str()) && !email.contains(needle.as_str()) {
45 continue;
46 }
47 }
48
49 if let Some(ref needle) = message_lower {
51 let msg = commit.message().unwrap_or("").to_lowercase();
52 if !msg.contains(needle.as_str()) {
53 continue;
54 }
55 }
56
57 results.push(CommitInfo::from_git2_commit(&commit));
58 }
59
60 Ok(results)
61}
62
63pub fn search_commits(repo: &Repository, query: &str, max_count: usize) -> Result<Vec<CommitInfo>> {
69 let needle = query.to_lowercase();
70
71 let mut revwalk = repo.revwalk().context("failed to create revwalk")?;
72 revwalk.push_head().context("failed to push HEAD")?;
73 revwalk
74 .set_sorting(git2::Sort::TIME | git2::Sort::TOPOLOGICAL)
75 .context("failed to set sorting")?;
76
77 let mut results = Vec::with_capacity(max_count.min(256));
78
79 for oid_result in revwalk {
80 if results.len() >= max_count {
81 break;
82 }
83
84 let oid = oid_result.context("revwalk iteration error")?;
85 let commit = repo
86 .find_commit(oid)
87 .context("failed to find commit during search")?;
88
89 let summary = commit.summary().unwrap_or("").to_lowercase();
90 let message = commit.message().unwrap_or("").to_lowercase();
91 let author = commit.author();
92 let author_name = author.name().unwrap_or("").to_lowercase();
93 let author_email = author.email().unwrap_or("").to_lowercase();
94 let oid_str = oid.to_string();
95
96 if summary.contains(&needle)
97 || message.contains(&needle)
98 || author_name.contains(&needle)
99 || author_email.contains(&needle)
100 || oid_str.contains(&needle)
101 {
102 results.push(CommitInfo::from_git2_commit(&commit));
103 }
104 }
105
106 Ok(results)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::path::Path;
113
114 fn setup_repo_with_commits() -> (tempfile::TempDir, Repository) {
115 let dir = tempfile::tempdir().unwrap();
116 let repo = Repository::init(dir.path()).unwrap();
117
118 {
119 let mut config = repo.config().unwrap();
120 config.set_str("user.name", "Alice Test").unwrap();
121 config.set_str("user.email", "alice@example.com").unwrap();
122 }
123
124 let c1 = {
126 std::fs::write(dir.path().join("a.txt"), "aaa\n").unwrap();
127 let mut index = repo.index().unwrap();
128 index.add_path(Path::new("a.txt")).unwrap();
129 index.write().unwrap();
130 let tree_oid = index.write_tree().unwrap();
131 let tree = repo.find_tree(tree_oid).unwrap();
132 let sig = repo.signature().unwrap();
133 repo.commit(Some("HEAD"), &sig, &sig, "first commit", &tree, &[])
134 .unwrap()
135 };
136
137 {
139 let commit1 = repo.find_commit(c1).unwrap();
140 std::fs::write(dir.path().join("b.txt"), "bbb\n").unwrap();
141 let mut index2 = repo.index().unwrap();
142 index2.add_path(Path::new("b.txt")).unwrap();
143 index2.write().unwrap();
144 let tree_oid2 = index2.write_tree().unwrap();
145 let tree2 = repo.find_tree(tree_oid2).unwrap();
146 let sig2 = git2::Signature::now("Bob Builder", "bob@example.com").unwrap();
147 repo.commit(
148 Some("HEAD"),
149 &sig2,
150 &sig2,
151 "second commit by Bob",
152 &tree2,
153 &[&commit1],
154 )
155 .unwrap();
156 }
157
158 (dir, repo)
159 }
160
161 #[test]
162 fn get_log_no_filters() {
163 let (_dir, repo) = setup_repo_with_commits();
164 let log = get_log(&repo, 100, None, None).unwrap();
165 assert_eq!(log.len(), 2);
166 assert_eq!(log[0].summary, "second commit by Bob");
168 assert_eq!(log[1].summary, "first commit");
169 }
170
171 #[test]
172 fn get_log_filter_author() {
173 let (_dir, repo) = setup_repo_with_commits();
174 let log = get_log(&repo, 100, Some("bob"), None).unwrap();
175 assert_eq!(log.len(), 1);
176 assert_eq!(log[0].summary, "second commit by Bob");
177 }
178
179 #[test]
180 fn get_log_filter_message() {
181 let (_dir, repo) = setup_repo_with_commits();
182 let log = get_log(&repo, 100, None, Some("first")).unwrap();
183 assert_eq!(log.len(), 1);
184 assert_eq!(log[0].summary, "first commit");
185 }
186
187 #[test]
188 fn get_log_respects_max_count() {
189 let (_dir, repo) = setup_repo_with_commits();
190 let log = get_log(&repo, 1, None, None).unwrap();
191 assert_eq!(log.len(), 1);
192 }
193
194 #[test]
195 fn search_commits_by_author() {
196 let (_dir, repo) = setup_repo_with_commits();
197 let results = search_commits(&repo, "alice", 100).unwrap();
198 assert_eq!(results.len(), 1);
199 assert_eq!(results[0].summary, "first commit");
200 }
201
202 #[test]
203 fn search_commits_by_message() {
204 let (_dir, repo) = setup_repo_with_commits();
205 let results = search_commits(&repo, "second", 100).unwrap();
206 assert_eq!(results.len(), 1);
207 assert_eq!(results[0].author_name, "Bob Builder");
208 }
209
210 #[test]
211 fn search_commits_case_insensitive() {
212 let (_dir, repo) = setup_repo_with_commits();
213 let results = search_commits(&repo, "BOB", 100).unwrap();
214 assert_eq!(results.len(), 1);
215 }
216}