use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct MockGitRepository {
root_path: PathBuf,
commits: Arc<Mutex<Vec<MockCommit>>>,
current_branch: Arc<Mutex<String>>,
branches: Arc<Mutex<HashMap<String, String>>>,
tags: Arc<Mutex<HashMap<String, String>>>,
working_changes: Arc<Mutex<Vec<PathBuf>>>,
}
#[derive(Debug, Clone)]
pub struct MockCommit {
pub hash: String,
#[allow(dead_code)]
pub short_hash: String,
pub message: String,
#[allow(dead_code)]
pub full_message: String,
pub author: String,
pub author_email: String,
#[allow(dead_code)]
pub date: DateTime<Utc>,
pub files_changed: Vec<PathBuf>,
pub lines_added: usize,
pub lines_deleted: usize,
pub parents: Vec<String>,
}
impl MockCommit {
pub fn new(hash: impl Into<String>, message: impl Into<String>, files: Vec<PathBuf>) -> Self {
let hash = hash.into();
let short_hash = hash.chars().take(7).collect();
let message = message.into();
Self {
hash,
short_hash,
message: message.clone(),
full_message: message,
author: "Test Author".to_string(),
author_email: "test@example.com".to_string(),
date: Utc::now(),
files_changed: files,
lines_added: 10,
lines_deleted: 5,
parents: vec![],
}
}
pub fn builder(hash: impl Into<String>) -> MockCommitBuilder {
MockCommitBuilder::new(hash)
}
pub fn is_merge_commit(&self) -> bool {
self.parents.len() > 1
}
}
#[derive(Debug)]
pub struct MockCommitBuilder {
hash: String,
message: Option<String>,
full_message: Option<String>,
author: Option<String>,
author_email: Option<String>,
date: Option<DateTime<Utc>>,
files_changed: Vec<PathBuf>,
lines_added: usize,
lines_deleted: usize,
parents: Vec<String>,
}
impl MockCommitBuilder {
fn new(hash: impl Into<String>) -> Self {
Self {
hash: hash.into(),
message: None,
full_message: None,
author: None,
author_email: None,
date: None,
files_changed: vec![],
lines_added: 10,
lines_deleted: 5,
parents: vec![],
}
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
#[allow(dead_code)]
pub fn full_message(mut self, message: impl Into<String>) -> Self {
self.full_message = Some(message.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn author_email(mut self, email: impl Into<String>) -> Self {
self.author_email = Some(email.into());
self
}
#[allow(dead_code)]
pub fn date(mut self, date: DateTime<Utc>) -> Self {
self.date = Some(date);
self
}
#[allow(dead_code)]
pub fn files(mut self, files: Vec<PathBuf>) -> Self {
self.files_changed = files;
self
}
pub fn add_file(mut self, file: impl Into<PathBuf>) -> Self {
self.files_changed.push(file.into());
self
}
pub fn lines_added(mut self, lines: usize) -> Self {
self.lines_added = lines;
self
}
pub fn lines_deleted(mut self, lines: usize) -> Self {
self.lines_deleted = lines;
self
}
pub fn parents(mut self, parents: Vec<String>) -> Self {
self.parents = parents;
self
}
#[allow(dead_code)]
pub fn add_parent(mut self, parent: impl Into<String>) -> Self {
self.parents.push(parent.into());
self
}
pub fn build(self) -> MockCommit {
let message = self.message.unwrap_or_else(|| "Test commit".to_string());
let hash = self.hash;
let short_hash = hash.chars().take(7).collect();
MockCommit {
hash,
short_hash,
message: message.clone(),
full_message: self.full_message.unwrap_or(message),
author: self.author.unwrap_or_else(|| "Test Author".to_string()),
author_email: self.author_email.unwrap_or_else(|| "test@example.com".to_string()),
date: self.date.unwrap_or_else(Utc::now),
files_changed: self.files_changed,
lines_added: self.lines_added,
lines_deleted: self.lines_deleted,
parents: self.parents,
}
}
}
impl MockGitRepository {
pub fn new(root_path: impl Into<PathBuf>) -> Self {
let mut branches = HashMap::new();
branches.insert("main".to_string(), "".to_string());
Self {
root_path: root_path.into(),
commits: Arc::new(Mutex::new(Vec::new())),
current_branch: Arc::new(Mutex::new("main".to_string())),
branches: Arc::new(Mutex::new(branches)),
tags: Arc::new(Mutex::new(HashMap::new())),
working_changes: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn root_path(&self) -> &Path {
&self.root_path
}
pub fn add_commit_obj(&self, commit: MockCommit) {
let mut commits = self.commits.lock().unwrap();
commits.push(commit);
}
pub fn add_commit(
&self,
hash: impl Into<String>,
message: impl Into<String>,
files: Vec<PathBuf>,
) {
let commit = MockCommit::new(hash, message, files);
self.add_commit_obj(commit);
}
pub fn get_commits(&self) -> Vec<MockCommit> {
let commits = self.commits.lock().unwrap();
commits.clone()
}
pub fn get_commits_range(&self, from: &str, to: &str) -> Vec<MockCommit> {
let commits = self.commits.lock().unwrap();
let from_idx = commits.iter().position(|c| c.hash == from);
let to_idx = commits.iter().position(|c| c.hash == to);
match (from_idx, to_idx) {
(Some(start), Some(end)) if start < end => commits[(start + 1)..=end].to_vec(),
(None, Some(end)) => commits[..=end].to_vec(),
_ => commits.clone(),
}
}
pub fn current_branch(&self) -> String {
let branch = self.current_branch.lock().unwrap();
branch.clone()
}
#[allow(dead_code)]
pub fn set_current_branch(&self, branch: impl Into<String>) {
let mut current = self.current_branch.lock().unwrap();
*current = branch.into();
}
pub fn add_branch(&self, name: impl Into<String>, commit_hash: impl Into<String>) {
let mut branches = self.branches.lock().unwrap();
branches.insert(name.into(), commit_hash.into());
}
pub fn get_branches(&self) -> HashMap<String, String> {
let branches = self.branches.lock().unwrap();
branches.clone()
}
pub fn add_tag(&self, name: impl Into<String>, commit_hash: impl Into<String>) {
let mut tags = self.tags.lock().unwrap();
tags.insert(name.into(), commit_hash.into());
}
pub fn get_tags(&self) -> HashMap<String, String> {
let tags = self.tags.lock().unwrap();
tags.clone()
}
pub fn add_working_change(&self, path: impl Into<PathBuf>) {
let mut changes = self.working_changes.lock().unwrap();
changes.push(path.into());
}
pub fn get_working_changes(&self) -> Vec<PathBuf> {
let changes = self.working_changes.lock().unwrap();
changes.clone()
}
pub fn clear_working_changes(&self) {
let mut changes = self.working_changes.lock().unwrap();
changes.clear();
}
#[allow(dead_code)]
pub fn clear_commits(&self) {
let mut commits = self.commits.lock().unwrap();
commits.clear();
}
pub fn get_commits_for_file(&self, file_path: &Path) -> Vec<MockCommit> {
let commits = self.commits.lock().unwrap();
commits.iter().filter(|c| c.files_changed.iter().any(|f| f == file_path)).cloned().collect()
}
pub fn commit_count(&self) -> usize {
let commits = self.commits.lock().unwrap();
commits.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_repository() {
let repo = MockGitRepository::new("/repo");
assert_eq!(repo.root_path(), Path::new("/repo"));
assert_eq!(repo.commit_count(), 0);
assert_eq!(repo.current_branch(), "main");
}
#[test]
fn test_add_commit() {
let repo = MockGitRepository::new("/repo");
repo.add_commit("abc123", "Initial commit", vec![PathBuf::from("/file.txt")]);
let commits = repo.get_commits();
assert_eq!(commits.len(), 1);
assert_eq!(commits[0].hash, "abc123");
assert_eq!(commits[0].message, "Initial commit");
}
#[test]
fn test_commit_builder() {
let commit = MockCommit::builder("abc123")
.message("Test commit")
.author("John Doe")
.author_email("john@example.com")
.add_file("/test.txt")
.lines_added(20)
.lines_deleted(10)
.build();
assert_eq!(commit.hash, "abc123");
assert_eq!(commit.message, "Test commit");
assert_eq!(commit.author, "John Doe");
assert_eq!(commit.author_email, "john@example.com");
assert_eq!(commit.files_changed.len(), 1);
assert_eq!(commit.lines_added, 20);
assert_eq!(commit.lines_deleted, 10);
}
#[test]
fn test_commit_range() {
let repo = MockGitRepository::new("/repo");
repo.add_commit("commit1", "First", vec![]);
repo.add_commit("commit2", "Second", vec![]);
repo.add_commit("commit3", "Third", vec![]);
let range = repo.get_commits_range("commit1", "commit3");
assert_eq!(range.len(), 2);
assert_eq!(range[0].hash, "commit2");
assert_eq!(range[1].hash, "commit3");
}
#[test]
fn test_branches() {
let repo = MockGitRepository::new("/repo");
repo.add_branch("develop", "abc123");
repo.add_branch("feature", "def456");
let branches = repo.get_branches();
assert_eq!(branches.len(), 3); assert_eq!(branches.get("develop"), Some(&"abc123".to_string()));
}
#[test]
fn test_tags() {
let repo = MockGitRepository::new("/repo");
repo.add_tag("v1.0.0", "abc123");
repo.add_tag("v1.1.0", "def456");
let tags = repo.get_tags();
assert_eq!(tags.len(), 2);
assert_eq!(tags.get("v1.0.0"), Some(&"abc123".to_string()));
}
#[test]
fn test_working_changes() {
let repo = MockGitRepository::new("/repo");
repo.add_working_change("/file1.txt");
repo.add_working_change("/file2.txt");
let changes = repo.get_working_changes();
assert_eq!(changes.len(), 2);
repo.clear_working_changes();
let changes = repo.get_working_changes();
assert_eq!(changes.len(), 0);
}
#[test]
fn test_commits_for_file() {
let repo = MockGitRepository::new("/repo");
repo.add_commit("commit1", "First", vec![PathBuf::from("/file1.txt")]);
repo.add_commit("commit2", "Second", vec![PathBuf::from("/file2.txt")]);
repo.add_commit(
"commit3",
"Third",
vec![PathBuf::from("/file1.txt"), PathBuf::from("/file3.txt")],
);
let commits = repo.get_commits_for_file(Path::new("/file1.txt"));
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].hash, "commit1");
assert_eq!(commits[1].hash, "commit3");
}
#[test]
fn test_is_merge_commit() {
let commit = MockCommit::builder("abc123")
.parents(vec!["parent1".to_string(), "parent2".to_string()])
.build();
assert!(commit.is_merge_commit());
let regular_commit = MockCommit::builder("def456").build();
assert!(!regular_commit.is_merge_commit());
}
}