1use anyhow::{anyhow, Result};
6use git2::Repository;
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use tracing::{debug, info};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CommitInfo {
14 pub hash: String,
15 pub message: String,
16 pub author: String,
17 pub timestamp: i64,
18 pub files_changed: usize,
20 pub lines_added: usize,
22 pub lines_removed: usize,
24}
25
26pub struct GitAnalyzer {
29 cache_dir: PathBuf,
30}
31
32impl GitAnalyzer {
33 pub fn new<P: AsRef<Path>>(cache_dir: P) -> Self {
46 let cache_dir = cache_dir.as_ref().to_path_buf();
47 Self { cache_dir }
48 }
49
50 pub fn clone_repository(&self, repo_url: &str, name: &str) -> Result<()> {
71 let repo_path = self.cache_dir.join(name);
72
73 if repo_path.exists() {
75 debug!("Repository {} already exists at {:?}", name, repo_path);
76 return Ok(());
77 }
78
79 info!("Cloning repository {} from {}", name, repo_url);
80
81 Repository::clone(repo_url, &repo_path).map_err(|e| {
83 anyhow!(
84 "Failed to clone repository {} from {}: {}",
85 name,
86 repo_url,
87 e
88 )
89 })?;
90
91 info!("Successfully cloned {} to {:?}", name, repo_path);
92 Ok(())
93 }
94
95 pub fn analyze_commits(&self, name: &str, limit: usize) -> Result<Vec<CommitInfo>> {
117 let repo_path = self.cache_dir.join(name);
118
119 if !repo_path.exists() {
120 return Err(anyhow!(
121 "Repository {} not found at {:?}. Clone it first.",
122 name,
123 repo_path
124 ));
125 }
126
127 debug!("Opening repository at {:?}", repo_path);
128 let repo = Repository::open(&repo_path)
129 .map_err(|e| anyhow!("Failed to open repository {}: {}", name, e))?;
130
131 let mut revwalk = repo.revwalk()?;
132 revwalk.push_head()?;
133
134 let mut commits = Vec::new();
135
136 for (i, oid) in revwalk.enumerate() {
137 if i >= limit {
138 break;
139 }
140
141 let oid = oid?;
142 let commit = repo.find_commit(oid)?;
143
144 let hash = commit.id().to_string();
145 let message = commit.message().unwrap_or("").to_string();
146 let author = commit.author().email().unwrap_or("unknown").to_string();
147 let timestamp = commit.time().seconds();
148
149 let (files_changed, lines_added, lines_removed) = if commit.parent_count() > 0 {
151 let parent = commit.parent(0)?;
152 let diff =
153 repo.diff_tree_to_tree(Some(&parent.tree()?), Some(&commit.tree()?), None)?;
154 let stats = diff.stats()?;
155 (stats.files_changed(), stats.insertions(), stats.deletions())
156 } else {
157 let tree = commit.tree()?;
159 (tree.len(), 0, 0)
160 };
161
162 commits.push(CommitInfo {
163 hash,
164 message,
165 author,
166 timestamp,
167 files_changed,
168 lines_added,
169 lines_removed,
170 });
171 }
172
173 debug!("Analyzed {} commits from {}", commits.len(), name);
174 Ok(commits)
175 }
176}
177
178pub fn analyze_repository_at_path<P: AsRef<Path>>(
199 repo_path: P,
200 limit: usize,
201) -> Result<Vec<CommitInfo>> {
202 let repo_path = repo_path.as_ref();
203
204 if !repo_path.exists() {
205 return Err(anyhow!("Repository path does not exist: {:?}", repo_path));
206 }
207
208 debug!("Opening repository at {:?}", repo_path);
209 let repo = Repository::open(repo_path)
210 .map_err(|e| anyhow!("Failed to open repository at {:?}: {}", repo_path, e))?;
211
212 let mut revwalk = repo.revwalk()?;
213 revwalk.push_head()?;
214
215 let mut commits = Vec::new();
216
217 for (i, oid) in revwalk.enumerate() {
218 if i >= limit {
219 break;
220 }
221
222 let oid = oid?;
223 let commit = repo.find_commit(oid)?;
224
225 let hash = commit.id().to_string();
226 let message = commit.message().unwrap_or("").to_string();
227 let author = commit.author().email().unwrap_or("unknown").to_string();
228 let timestamp = commit.time().seconds();
229
230 let (files_changed, lines_added, lines_removed) = if commit.parent_count() > 0 {
232 let parent = commit.parent(0)?;
233 let diff =
234 repo.diff_tree_to_tree(Some(&parent.tree()?), Some(&commit.tree()?), None)?;
235 let stats = diff.stats()?;
236 (stats.files_changed(), stats.insertions(), stats.deletions())
237 } else {
238 let tree = commit.tree()?;
240 (tree.len(), 0, 0)
241 };
242
243 commits.push(CommitInfo {
244 hash,
245 message,
246 author,
247 timestamp,
248 files_changed,
249 lines_added,
250 lines_removed,
251 });
252 }
253
254 debug!("Analyzed {} commits", commits.len());
255 Ok(commits)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use tempfile::TempDir;
262
263 #[test]
264 fn test_git_analyzer_can_be_created() {
265 let temp_dir = TempDir::new().unwrap();
266 let _analyzer = GitAnalyzer::new(temp_dir.path());
267 }
268
269 #[test]
270 fn test_commit_info_structure() {
271 let commit = CommitInfo {
272 hash: "abc123".to_string(),
273 message: "fix: null pointer dereference".to_string(),
274 author: "test@example.com".to_string(),
275 timestamp: 1234567890,
276 files_changed: 3,
277 lines_added: 15,
278 lines_removed: 8,
279 };
280
281 assert_eq!(commit.hash, "abc123");
282 assert_eq!(commit.message, "fix: null pointer dereference");
283 assert_eq!(commit.author, "test@example.com");
284 assert_eq!(commit.timestamp, 1234567890);
285 assert_eq!(commit.files_changed, 3);
286 assert_eq!(commit.lines_added, 15);
287 assert_eq!(commit.lines_removed, 8);
288 }
289
290 #[test]
291 fn test_analyze_nonexistent_repo() {
292 let temp_dir = TempDir::new().unwrap();
293 let analyzer = GitAnalyzer::new(temp_dir.path());
294
295 let result = analyzer.analyze_commits("nonexistent-repo", 10);
296
297 assert!(result.is_err());
298 }
299
300 #[test]
301 fn test_commit_info_serialization() {
302 let commit = CommitInfo {
303 hash: "test123".to_string(),
304 message: "test commit".to_string(),
305 author: "test@example.com".to_string(),
306 timestamp: 1234567890,
307 files_changed: 1,
308 lines_added: 10,
309 lines_removed: 5,
310 };
311
312 let json = serde_json::to_string(&commit).unwrap();
313 let deserialized: CommitInfo = serde_json::from_str(&json).unwrap();
314
315 assert_eq!(commit.hash, deserialized.hash);
316 assert_eq!(commit.message, deserialized.message);
317 assert_eq!(commit.author, deserialized.author);
318 }
319
320 #[test]
321 fn test_analyze_local_repo_with_commits() {
322 let temp_dir = TempDir::new().unwrap();
323 let repo_path = temp_dir.path().join("test-repo");
324 std::fs::create_dir(&repo_path).unwrap();
325
326 let repo = Repository::init(&repo_path).unwrap();
328
329 let test_file = repo_path.join("test.txt");
331 std::fs::write(&test_file, "Hello, world!").unwrap();
332
333 let mut config = repo.config().unwrap();
335 config.set_str("user.name", "Test User").unwrap();
336 config.set_str("user.email", "test@example.com").unwrap();
337
338 let mut index = repo.index().unwrap();
340 index.add_path(Path::new("test.txt")).unwrap();
341 index.write().unwrap();
342
343 let tree_id = index.write_tree().unwrap();
344 let tree = repo.find_tree(tree_id).unwrap();
345 let sig = repo.signature().unwrap();
346
347 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
348 .unwrap();
349
350 let analyzer = GitAnalyzer::new(temp_dir.path());
352 let commits = analyzer.analyze_commits("test-repo", 10).unwrap();
353
354 assert_eq!(commits.len(), 1);
355 assert_eq!(commits[0].message, "Initial commit");
356 assert!(commits[0].files_changed > 0);
357 }
358
359 #[test]
360 fn test_analyze_local_repo_multiple_commits() {
361 let temp_dir = TempDir::new().unwrap();
362 let repo_path = temp_dir.path().join("multi-commit-repo");
363 std::fs::create_dir(&repo_path).unwrap();
364
365 let repo = Repository::init(&repo_path).unwrap();
366
367 let mut config = repo.config().unwrap();
368 config.set_str("user.name", "Test User").unwrap();
369 config.set_str("user.email", "test@example.com").unwrap();
370
371 let commit_file = |name: &str, content: &str, message: &str| {
373 let file_path = repo_path.join(name);
374 std::fs::write(&file_path, content).unwrap();
375
376 let mut index = repo.index().unwrap();
377 index.add_path(Path::new(name)).unwrap();
378 index.write().unwrap();
379
380 let tree_id = index.write_tree().unwrap();
381 let tree = repo.find_tree(tree_id).unwrap();
382 let sig = repo.signature().unwrap();
383
384 let parent = if let Ok(head) = repo.head() {
385 let parent_commit = head.peel_to_commit().unwrap();
386 vec![parent_commit]
387 } else {
388 vec![]
389 };
390
391 let parent_refs: Vec<_> = parent.iter().collect();
392
393 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parent_refs)
394 .unwrap();
395 };
396
397 commit_file("file1.txt", "content 1", "Add file1");
398 commit_file("file2.txt", "content 2", "Add file2");
399 commit_file("file3.txt", "content 3", "Add file3");
400
401 let analyzer = GitAnalyzer::new(temp_dir.path());
403 let commits = analyzer.analyze_commits("multi-commit-repo", 10).unwrap();
404
405 assert_eq!(commits.len(), 3);
406 assert_eq!(commits[0].message, "Add file3"); assert_eq!(commits[1].message, "Add file2");
408 assert_eq!(commits[2].message, "Add file1");
409 }
410
411 #[test]
412 fn test_analyze_respects_commit_limit() {
413 let temp_dir = TempDir::new().unwrap();
414 let repo_path = temp_dir.path().join("limit-repo");
415 std::fs::create_dir(&repo_path).unwrap();
416
417 let repo = Repository::init(&repo_path).unwrap();
418
419 let mut config = repo.config().unwrap();
420 config.set_str("user.name", "Test").unwrap();
421 config.set_str("user.email", "test@test.com").unwrap();
422
423 for i in 0..5 {
425 let file_path = repo_path.join(format!("file{}.txt", i));
426 std::fs::write(&file_path, format!("content {}", i)).unwrap();
427
428 let mut index = repo.index().unwrap();
429 index
430 .add_path(Path::new(&format!("file{}.txt", i)))
431 .unwrap();
432 index.write().unwrap();
433
434 let tree_id = index.write_tree().unwrap();
435 let tree = repo.find_tree(tree_id).unwrap();
436 let sig = repo.signature().unwrap();
437
438 let parent = if let Ok(head) = repo.head() {
439 vec![head.peel_to_commit().unwrap()]
440 } else {
441 vec![]
442 };
443 let parent_refs: Vec<_> = parent.iter().collect();
444
445 repo.commit(
446 Some("HEAD"),
447 &sig,
448 &sig,
449 &format!("Commit {}", i),
450 &tree,
451 &parent_refs,
452 )
453 .unwrap();
454 }
455
456 let analyzer = GitAnalyzer::new(temp_dir.path());
457
458 let commits = analyzer.analyze_commits("limit-repo", 2).unwrap();
460 assert_eq!(commits.len(), 2);
461
462 let commits = analyzer.analyze_commits("limit-repo", 10).unwrap();
464 assert_eq!(commits.len(), 5);
465 }
466
467 #[test]
470 #[ignore]
471 fn test_clone_small_repository() {
472 let temp_dir = TempDir::new().unwrap();
473 let analyzer = GitAnalyzer::new(temp_dir.path());
474
475 let result =
477 analyzer.clone_repository("https://github.com/rust-lang/rustlings", "rustlings");
478
479 assert!(result.is_ok());
480 }
481
482 #[test]
483 #[ignore]
484 fn test_analyze_commits_basic() {
485 let temp_dir = TempDir::new().unwrap();
486 let analyzer = GitAnalyzer::new(temp_dir.path());
487
488 analyzer
489 .clone_repository("https://github.com/rust-lang/rustlings", "rustlings")
490 .unwrap();
491
492 let commits = analyzer.analyze_commits("rustlings", 10).unwrap();
493
494 assert!(!commits.is_empty());
495 assert!(commits.len() <= 10);
496
497 let first_commit = &commits[0];
498 assert!(!first_commit.hash.is_empty());
499 assert!(!first_commit.message.is_empty());
500 }
501
502 #[test]
503 #[ignore]
504 fn test_analyze_commits_respects_limit() {
505 let temp_dir = TempDir::new().unwrap();
506 let analyzer = GitAnalyzer::new(temp_dir.path());
507
508 analyzer
509 .clone_repository("https://github.com/rust-lang/rustlings", "rustlings")
510 .unwrap();
511
512 let commits_5 = analyzer.analyze_commits("rustlings", 5).unwrap();
513 assert!(commits_5.len() <= 5);
514
515 let commits_20 = analyzer.analyze_commits("rustlings", 20).unwrap();
516 assert!(commits_20.len() <= 20);
517 }
518
519 #[test]
520 #[ignore]
521 fn test_analyzer_caches_cloned_repos() {
522 let temp_dir = TempDir::new().unwrap();
523 let analyzer = GitAnalyzer::new(temp_dir.path());
524
525 analyzer
527 .clone_repository("https://github.com/rust-lang/rustlings", "rustlings")
528 .unwrap();
529
530 let result =
532 analyzer.clone_repository("https://github.com/rust-lang/rustlings", "rustlings");
533 assert!(result.is_ok());
534
535 let commits = analyzer.analyze_commits("rustlings", 5).unwrap();
537 assert!(!commits.is_empty());
538 }
539
540 #[test]
541 fn test_clone_repository_already_exists() {
542 let temp_dir = TempDir::new().unwrap();
543 let repo_path = temp_dir.path().join("existing-repo");
544 std::fs::create_dir(&repo_path).unwrap();
545
546 Repository::init(&repo_path).unwrap();
548
549 let analyzer = GitAnalyzer::new(temp_dir.path());
550
551 let result = analyzer.clone_repository("https://example.com/repo.git", "existing-repo");
553 assert!(result.is_ok());
554 }
555
556 #[test]
557 fn test_clone_repository_invalid_url() {
558 let temp_dir = TempDir::new().unwrap();
559 let analyzer = GitAnalyzer::new(temp_dir.path());
560
561 let result = analyzer.clone_repository("not-a-valid-url", "test-repo");
563 assert!(result.is_err());
564 }
565
566 #[test]
567 fn test_analyze_commits_with_zero_limit() {
568 let temp_dir = TempDir::new().unwrap();
569 let repo_path = temp_dir.path().join("zero-limit-repo");
570 std::fs::create_dir(&repo_path).unwrap();
571
572 let repo = Repository::init(&repo_path).unwrap();
573
574 let mut config = repo.config().unwrap();
575 config.set_str("user.name", "Test").unwrap();
576 config.set_str("user.email", "test@test.com").unwrap();
577
578 let file_path = repo_path.join("file.txt");
580 std::fs::write(&file_path, "content").unwrap();
581
582 let mut index = repo.index().unwrap();
583 index.add_path(Path::new("file.txt")).unwrap();
584 index.write().unwrap();
585
586 let tree_id = index.write_tree().unwrap();
587 let tree = repo.find_tree(tree_id).unwrap();
588 let sig = repo.signature().unwrap();
589
590 repo.commit(Some("HEAD"), &sig, &sig, "Test commit", &tree, &[])
591 .unwrap();
592
593 let analyzer = GitAnalyzer::new(temp_dir.path());
594
595 let commits = analyzer.analyze_commits("zero-limit-repo", 0).unwrap();
597 assert_eq!(commits.len(), 0);
598 }
599
600 #[test]
601 fn test_analyze_commits_with_modifications() {
602 let temp_dir = TempDir::new().unwrap();
603 let repo_path = temp_dir.path().join("modify-repo");
604 std::fs::create_dir(&repo_path).unwrap();
605
606 let repo = Repository::init(&repo_path).unwrap();
607
608 let mut config = repo.config().unwrap();
609 config.set_str("user.name", "Test User").unwrap();
610 config.set_str("user.email", "test@example.com").unwrap();
611
612 let file_path = repo_path.join("test.txt");
614 std::fs::write(&file_path, "Line 1\nLine 2\nLine 3\n").unwrap();
615
616 let mut index = repo.index().unwrap();
617 index.add_path(Path::new("test.txt")).unwrap();
618 index.write().unwrap();
619
620 let tree_id = index.write_tree().unwrap();
621 let tree = repo.find_tree(tree_id).unwrap();
622 let sig = repo.signature().unwrap();
623
624 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
625 .unwrap();
626
627 std::fs::write(&file_path, "Line 1\nLine 2 modified\nLine 3\nLine 4\n").unwrap();
629
630 let mut index = repo.index().unwrap();
631 index.add_path(Path::new("test.txt")).unwrap();
632 index.write().unwrap();
633
634 let tree_id = index.write_tree().unwrap();
635 let tree = repo.find_tree(tree_id).unwrap();
636
637 let parent = repo.head().unwrap().peel_to_commit().unwrap();
638
639 repo.commit(Some("HEAD"), &sig, &sig, "Modify file", &tree, &[&parent])
640 .unwrap();
641
642 let analyzer = GitAnalyzer::new(temp_dir.path());
643 let commits = analyzer.analyze_commits("modify-repo", 10).unwrap();
644
645 assert_eq!(commits.len(), 2);
646 assert!(commits[0].lines_added > 0 || commits[0].lines_removed > 0);
648 }
649
650 #[test]
651 fn test_commit_info_clone() {
652 let original = CommitInfo {
653 hash: "abc123".to_string(),
654 message: "test".to_string(),
655 author: "test@example.com".to_string(),
656 timestamp: 1234567890,
657 files_changed: 1,
658 lines_added: 10,
659 lines_removed: 5,
660 };
661
662 let cloned = original.clone();
663
664 assert_eq!(original.hash, cloned.hash);
665 assert_eq!(original.message, cloned.message);
666 assert_eq!(original.author, cloned.author);
667 assert_eq!(original.timestamp, cloned.timestamp);
668 assert_eq!(original.files_changed, cloned.files_changed);
669 assert_eq!(original.lines_added, cloned.lines_added);
670 assert_eq!(original.lines_removed, cloned.lines_removed);
671 }
672
673 #[test]
674 fn test_commit_info_debug_format() {
675 let commit = CommitInfo {
676 hash: "abc123".to_string(),
677 message: "test".to_string(),
678 author: "test@example.com".to_string(),
679 timestamp: 1234567890,
680 files_changed: 1,
681 lines_added: 10,
682 lines_removed: 5,
683 };
684
685 let debug_str = format!("{:?}", commit);
686 assert!(debug_str.contains("abc123"));
687 assert!(debug_str.contains("test"));
688 }
689
690 #[test]
691 fn test_analyzer_with_path_ref() {
692 let temp_dir = TempDir::new().unwrap();
693 let path = temp_dir.path();
694
695 let _analyzer = GitAnalyzer::new(path);
697 let _analyzer2 = GitAnalyzer::new(path.to_str().unwrap());
698 }
699
700 #[test]
701 fn test_analyze_empty_repository() {
702 let temp_dir = TempDir::new().unwrap();
703 let repo_path = temp_dir.path().join("empty-repo");
704 std::fs::create_dir(&repo_path).unwrap();
705
706 Repository::init(&repo_path).unwrap();
708
709 let analyzer = GitAnalyzer::new(temp_dir.path());
710
711 let result = analyzer.analyze_commits("empty-repo", 10);
713 assert!(result.is_err());
714 }
715
716 #[test]
717 fn test_analyze_commits_with_large_diff() {
718 let temp_dir = TempDir::new().unwrap();
719 let repo_path = temp_dir.path().join("large-diff-repo");
720 std::fs::create_dir(&repo_path).unwrap();
721
722 let repo = Repository::init(&repo_path).unwrap();
723
724 let mut config = repo.config().unwrap();
725 config.set_str("user.name", "Test").unwrap();
726 config.set_str("user.email", "test@test.com").unwrap();
727
728 let mut content = String::new();
730 for i in 0..1000 {
731 content.push_str(&format!("Line {}\n", i));
732 }
733
734 let file_path = repo_path.join("large.txt");
735 std::fs::write(&file_path, &content).unwrap();
736
737 let mut index = repo.index().unwrap();
738 index.add_path(Path::new("large.txt")).unwrap();
739 index.write().unwrap();
740
741 let tree_id = index.write_tree().unwrap();
742 let tree = repo.find_tree(tree_id).unwrap();
743 let sig = repo.signature().unwrap();
744
745 repo.commit(Some("HEAD"), &sig, &sig, "Large commit", &tree, &[])
746 .unwrap();
747
748 let analyzer = GitAnalyzer::new(temp_dir.path());
749 let commits = analyzer.analyze_commits("large-diff-repo", 10).unwrap();
750
751 assert_eq!(commits.len(), 1);
752 assert_eq!(commits[0].files_changed, 1);
753 }
754
755 #[test]
756 fn test_commit_info_with_empty_strings() {
757 let commit = CommitInfo {
758 hash: "".to_string(),
759 message: "".to_string(),
760 author: "".to_string(),
761 timestamp: 0,
762 files_changed: 0,
763 lines_added: 0,
764 lines_removed: 0,
765 };
766
767 assert_eq!(commit.hash, "");
768 assert_eq!(commit.message, "");
769 assert_eq!(commit.author, "");
770 }
771
772 #[test]
773 fn test_multiple_files_in_single_commit() {
774 let temp_dir = TempDir::new().unwrap();
775 let repo_path = temp_dir.path().join("multi-file-repo");
776 std::fs::create_dir(&repo_path).unwrap();
777
778 let repo = Repository::init(&repo_path).unwrap();
779
780 let mut config = repo.config().unwrap();
781 config.set_str("user.name", "Test").unwrap();
782 config.set_str("user.email", "test@test.com").unwrap();
783
784 for i in 0..5 {
786 let file_path = repo_path.join(format!("file{}.txt", i));
787 std::fs::write(&file_path, format!("content {}", i)).unwrap();
788 }
789
790 let mut index = repo.index().unwrap();
791 for i in 0..5 {
792 index
793 .add_path(Path::new(&format!("file{}.txt", i)))
794 .unwrap();
795 }
796 index.write().unwrap();
797
798 let tree_id = index.write_tree().unwrap();
799 let tree = repo.find_tree(tree_id).unwrap();
800 let sig = repo.signature().unwrap();
801
802 repo.commit(Some("HEAD"), &sig, &sig, "Add 5 files", &tree, &[])
803 .unwrap();
804
805 let analyzer = GitAnalyzer::new(temp_dir.path());
806 let commits = analyzer.analyze_commits("multi-file-repo", 10).unwrap();
807
808 assert_eq!(commits.len(), 1);
809 assert_eq!(commits[0].files_changed, 5);
810 }
811
812 #[test]
813 fn test_commit_with_deletions() {
814 let temp_dir = TempDir::new().unwrap();
815 let repo_path = temp_dir.path().join("deletion-repo");
816 std::fs::create_dir(&repo_path).unwrap();
817
818 let repo = Repository::init(&repo_path).unwrap();
819
820 let mut config = repo.config().unwrap();
821 config.set_str("user.name", "Test").unwrap();
822 config.set_str("user.email", "test@test.com").unwrap();
823
824 let file_path = repo_path.join("file.txt");
826 std::fs::write(&file_path, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n").unwrap();
827
828 let mut index = repo.index().unwrap();
829 index.add_path(Path::new("file.txt")).unwrap();
830 index.write().unwrap();
831
832 let tree_id = index.write_tree().unwrap();
833 let tree = repo.find_tree(tree_id).unwrap();
834 let sig = repo.signature().unwrap();
835
836 repo.commit(Some("HEAD"), &sig, &sig, "Initial", &tree, &[])
837 .unwrap();
838
839 std::fs::write(&file_path, "Line 1\nLine 5\n").unwrap();
841
842 let mut index = repo.index().unwrap();
843 index.add_path(Path::new("file.txt")).unwrap();
844 index.write().unwrap();
845
846 let tree_id = index.write_tree().unwrap();
847 let tree = repo.find_tree(tree_id).unwrap();
848
849 let parent = repo.head().unwrap().peel_to_commit().unwrap();
850
851 repo.commit(Some("HEAD"), &sig, &sig, "Delete lines", &tree, &[&parent])
852 .unwrap();
853
854 let analyzer = GitAnalyzer::new(temp_dir.path());
855 let commits = analyzer.analyze_commits("deletion-repo", 10).unwrap();
856
857 assert_eq!(commits.len(), 2);
858 assert!(commits[0].lines_removed > 0);
860 }
861
862 #[test]
863 fn test_commit_info_deserialization() {
864 let json = r#"{
865 "hash": "abc123",
866 "message": "test message",
867 "author": "test@example.com",
868 "timestamp": 1234567890,
869 "files_changed": 3,
870 "lines_added": 15,
871 "lines_removed": 8
872 }"#;
873
874 let commit: CommitInfo = serde_json::from_str(json).unwrap();
875
876 assert_eq!(commit.hash, "abc123");
877 assert_eq!(commit.message, "test message");
878 assert_eq!(commit.author, "test@example.com");
879 assert_eq!(commit.timestamp, 1234567890);
880 assert_eq!(commit.files_changed, 3);
881 assert_eq!(commit.lines_added, 15);
882 assert_eq!(commit.lines_removed, 8);
883 }
884}