1use crate::utils::types::Result;
2use crate::{args::Args, utils::types::CommitInfo};
3use colored::Colorize;
4use git2::{Repository, Sort};
5
6pub fn get_commit_history(args: &Args, print: bool) -> Result<Vec<CommitInfo>> {
7 let repo = Repository::open(args.repo_path.as_ref().unwrap())?;
8
9 let mut revwalk = repo.revwalk()?;
10 revwalk.push_head()?;
11 revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
12
13 let mut commits = Vec::new();
15 let mut commit_infos = Vec::new();
16
17 for oid_result in revwalk {
18 let oid = oid_result?;
19 let commit = repo.find_commit(oid)?;
20 let timestamp = commit.time();
21 let datetime = chrono::DateTime::from_timestamp(timestamp.seconds(), 0)
22 .unwrap_or_default()
23 .naive_utc();
24
25 let commit_info = CommitInfo {
26 oid,
27 short_hash: oid.to_string()[..8].to_string(),
28 timestamp: datetime,
29 author_name: commit.author().name().unwrap_or("Unknown").to_string(),
30 author_email: commit
31 .author()
32 .email()
33 .unwrap_or("unknown@email.com")
34 .to_string(),
35 message: commit.message().unwrap_or("(no message)").to_string(),
36 parent_count: commit.parent_count(),
37 };
38
39 if print {
40 commits.push((oid, commit));
41 }
42 commit_infos.push(commit_info);
43 }
44
45 if print {
47 let total_commits = commit_infos.len();
48
49 if total_commits > 0 {
50 let timestamps: Vec<_> = commit_infos.iter().map(|c| c.timestamp).collect();
51 let mut sorted_timestamps = timestamps.clone();
52 sorted_timestamps.sort();
53
54 let earliest_date = sorted_timestamps[0];
55 let latest_date = sorted_timestamps[sorted_timestamps.len() - 1];
56 let date_span = latest_date.signed_duration_since(earliest_date).num_days();
57
58 let unique_authors: std::collections::HashSet<String> =
59 commit_infos.iter().map(|c| c.author_name.clone()).collect();
60
61 println!("\n{}", "Updated Commit History Summary:".bold().green());
63 println!("{}", "-".repeat(60).cyan());
64 println!(
65 "{}: {}",
66 "Total Commits".bold(),
67 total_commits.to_string().yellow()
68 );
69 println!(
70 "{}: {} days",
71 "Date Span".bold(),
72 date_span.to_string().yellow()
73 );
74 println!(
75 "{}: {} to {}",
76 "Date Range".bold(),
77 earliest_date.format("%Y-%m-%d %H:%M:%S").to_string().blue(),
78 latest_date.format("%Y-%m-%d %H:%M:%S").to_string().blue()
79 );
80 println!(
81 "{}: {}",
82 "Unique Authors".bold(),
83 unique_authors.len().to_string().yellow()
84 );
85 if unique_authors.len() <= 5 {
86 println!(
87 "{}: {}",
88 "Authors".bold(),
89 unique_authors
90 .iter()
91 .cloned()
92 .collect::<Vec<_>>()
93 .join(", ")
94 .magenta()
95 );
96 }
97 println!("{}", "=".repeat(60).cyan());
98
99 println!("\n{}", "Detailed Commit History:".bold().green());
101 println!("{}", "-".repeat(60).cyan());
102
103 for commit_info in &commit_infos {
104 println!(
105 "{} {} {} {}",
106 commit_info.short_hash.yellow().bold(),
107 commit_info
108 .timestamp
109 .format("%Y-%m-%d %H:%M:%S")
110 .to_string()
111 .blue(),
112 commit_info.author_name.magenta(),
113 commit_info.message.lines().next().unwrap_or("").white()
114 );
115 }
116
117 println!("{}", "=".repeat(60).cyan());
118 }
119 }
120
121 Ok(commit_infos)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::fs;
128 use tempfile::TempDir;
129
130 fn create_test_repo_with_commits() -> (TempDir, String) {
131 let temp_dir = TempDir::new().unwrap();
132 let repo_path = temp_dir.path().to_str().unwrap().to_string();
133
134 let repo = git2::Repository::init(&repo_path).unwrap();
136
137 for i in 1..=3 {
139 let file_path = temp_dir.path().join(format!("test{i}.txt"));
140 fs::write(&file_path, format!("test content {i}")).unwrap();
141
142 let mut index = repo.index().unwrap();
143 index
144 .add_path(std::path::Path::new(&format!("test{i}.txt")))
145 .unwrap();
146 index.write().unwrap();
147
148 let tree_id = index.write_tree().unwrap();
149 let tree = repo.find_tree(tree_id).unwrap();
150
151 let sig = git2::Signature::new(
152 "Test User",
153 "test@example.com",
154 &git2::Time::new(1234567890 + i as i64 * 3600, 0),
155 )
156 .unwrap();
157
158 let parents = if i == 1 {
159 vec![]
160 } else {
161 let head = repo.head().unwrap();
162 let parent_commit = head.peel_to_commit().unwrap();
163 vec![parent_commit]
164 };
165
166 repo.commit(
167 Some("HEAD"),
168 &sig,
169 &sig,
170 &format!("Commit {i}"),
171 &tree,
172 &parents.iter().collect::<Vec<_>>(),
173 )
174 .unwrap();
175 }
176
177 (temp_dir, repo_path)
178 }
179
180 #[test]
181 fn test_get_commit_history_without_print() {
182 let (_temp_dir, repo_path) = create_test_repo_with_commits();
183 let args = Args {
184 repo_path: Some(repo_path),
185 email: None,
186 name: None,
187 start: None,
188 end: None,
189 show_history: false,
190 pick_specific_commits: false,
191 range: false,
192 simulate: false,
193 show_diff: false,
194 };
195
196 let result = get_commit_history(&args, false);
197 assert!(result.is_ok());
198
199 let commit_infos = result.unwrap();
200 assert_eq!(commit_infos.len(), 3);
201
202 assert_eq!(commit_infos[0].message, "Commit 3");
204 assert_eq!(commit_infos[1].message, "Commit 2");
205 assert_eq!(commit_infos[2].message, "Commit 1");
206 }
207
208 #[test]
209 fn test_get_commit_history_with_print() {
210 let (_temp_dir, repo_path) = create_test_repo_with_commits();
211 let args = Args {
212 repo_path: Some(repo_path),
213 email: None,
214 name: None,
215 start: None,
216 end: None,
217 show_history: true,
218 pick_specific_commits: false,
219 range: false,
220 simulate: false,
221 show_diff: false,
222 };
223
224 let result = get_commit_history(&args, true);
225 assert!(result.is_ok());
226
227 let commit_infos = result.unwrap();
228 assert_eq!(commit_infos.len(), 3);
229 }
230
231 #[test]
232 fn test_commit_info_fields() {
233 let (_temp_dir, repo_path) = create_test_repo_with_commits();
234 let args = Args {
235 repo_path: Some(repo_path),
236 email: None,
237 name: None,
238 start: None,
239 end: None,
240 show_history: false,
241 pick_specific_commits: false,
242 range: false,
243 simulate: false,
244 show_diff: false,
245 };
246
247 let result = get_commit_history(&args, false);
248 assert!(result.is_ok());
249
250 let commit_infos = result.unwrap();
251 let first_commit = &commit_infos[0];
252
253 assert!(!first_commit.short_hash.is_empty());
255 assert_eq!(first_commit.short_hash.len(), 8);
256 assert_eq!(first_commit.author_name, "Test User");
257 assert_eq!(first_commit.author_email, "test@example.com");
258 assert!(!first_commit.message.is_empty());
259 assert_eq!(first_commit.parent_count, 1); }
261
262 #[test]
263 fn test_get_commit_history_empty_repo() {
264 let temp_dir = TempDir::new().unwrap();
265 let repo_path = temp_dir.path().to_str().unwrap().to_string();
266
267 git2::Repository::init(&repo_path).unwrap();
269
270 let args = Args {
271 repo_path: Some(repo_path),
272 email: None,
273 name: None,
274 start: None,
275 end: None,
276 show_history: false,
277 pick_specific_commits: false,
278 range: false,
279 simulate: false,
280 show_diff: false,
281 };
282
283 let result = get_commit_history(&args, false);
284 assert!(result.is_err());
286 }
287
288 #[test]
289 fn test_get_commit_history_invalid_repo() {
290 let args = Args {
291 repo_path: Some("/nonexistent/path".to_string()),
292 email: None,
293 name: None,
294 start: None,
295 end: None,
296 show_history: false,
297 pick_specific_commits: false,
298 range: false,
299 simulate: false,
300 show_diff: false,
301 };
302
303 let result = get_commit_history(&args, false);
304 assert!(result.is_err());
305 }
306
307 #[test]
308 fn test_commit_info_parent_count() {
309 let (_temp_dir, repo_path) = create_test_repo_with_commits();
310 let args = Args {
311 repo_path: Some(repo_path),
312 email: None,
313 name: None,
314 start: None,
315 end: None,
316 show_history: false,
317 pick_specific_commits: false,
318 range: false,
319 simulate: false,
320 show_diff: false,
321 };
322
323 let result = get_commit_history(&args, false);
324 assert!(result.is_ok());
325
326 let commit_infos = result.unwrap();
327
328 assert_eq!(commit_infos[2].parent_count, 0);
330
331 assert_eq!(commit_infos[1].parent_count, 1);
333 assert_eq!(commit_infos[0].parent_count, 1);
334 }
335}