use crate::abstractions::{GitOperations, RealGitOperations};
use anyhow::Result;
use once_cell::sync::Lazy;
use std::sync::Arc;
use tokio::sync::Mutex;
static GIT_OPS: Lazy<Arc<Mutex<RealGitOperations>>> =
Lazy::new(|| Arc::new(Mutex::new(RealGitOperations::new())));
pub async fn git_command(args: &[&str], description: &str) -> Result<std::process::Output> {
let ops = GIT_OPS.lock().await;
ops.git_command(args, description).await
}
pub async fn get_last_commit_message() -> Result<String> {
let ops = GIT_OPS.lock().await;
ops.get_last_commit_message().await
}
pub async fn check_git_status() -> Result<String> {
let ops = GIT_OPS.lock().await;
ops.check_git_status().await
}
pub async fn stage_all_changes() -> Result<()> {
let ops = GIT_OPS.lock().await;
ops.stage_all_changes().await
}
pub async fn create_commit(message: &str) -> Result<()> {
let ops = GIT_OPS.lock().await;
ops.create_commit(message).await
}
pub async fn is_git_repo() -> bool {
let ops = GIT_OPS.lock().await;
ops.is_git_repo().await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
use tempfile::TempDir;
use tokio::process::Command;
async fn git_in_dir(dir: &std::path::Path, args: &[&str]) -> Result<std::process::Output> {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Git command failed: {}", stderr));
}
Ok(output)
}
async fn check_git_status_in_dir(dir: &std::path::Path) -> Result<String> {
let output = git_in_dir(dir, &["status", "--porcelain"]).await?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
async fn get_last_commit_message_in_dir(dir: &std::path::Path) -> Result<String> {
let output = git_in_dir(dir, &["log", "-1", "--pretty=format:%s"]).await?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub(super) async fn stage_all_changes_in_dir(dir: &std::path::Path) -> Result<()> {
git_in_dir(dir, &["add", "."]).await?;
Ok(())
}
pub(super) async fn create_commit_in_dir(dir: &std::path::Path, message: &str) -> Result<()> {
git_in_dir(dir, &["commit", "-m", message]).await?;
Ok(())
}
#[tokio::test]
async fn test_git_mutex_prevents_races() {
let tasks: Vec<_> = (0..5)
.map(|i| {
tokio::spawn(async move {
let result = get_last_commit_message().await;
println!("Task {} completed: {:?}", i, result.is_ok());
result
})
})
.collect();
for task in tasks {
let _ = task.await;
}
}
#[tokio::test]
async fn test_is_git_repo() {
let temp_dir = TempDir::new().unwrap();
let output = Command::new("git")
.args(["rev-parse", "--git-dir"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
assert!(
!output.status.success(),
"Should not be a git repo initially"
);
let output = Command::new("git")
.args(["init"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
assert!(output.status.success(), "git init failed: {output:?}");
let output = Command::new("git")
.args(["rev-parse", "--git-dir"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
assert!(output.status.success(), "Should be a git repo after init");
}
pub(super) async fn create_temp_git_repo() -> Result<TempDir> {
let temp_dir = TempDir::new()?;
Command::new("git")
.args(["init"])
.current_dir(temp_dir.path())
.output()
.await?;
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(temp_dir.path())
.output()
.await?;
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(temp_dir.path())
.output()
.await?;
Ok(temp_dir)
}
async fn create_test_commit(repo_path: &std::path::Path, message: &str) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let filename = format!("test_{timestamp}.txt");
let file_path = repo_path.join(&filename);
std::fs::write(&file_path, "test content")?;
Command::new("git")
.args(["add", &filename])
.current_dir(repo_path)
.output()
.await?;
Command::new("git")
.args(["commit", "-m", message])
.current_dir(repo_path)
.output()
.await?;
Ok(())
}
#[tokio::test]
async fn test_get_last_commit_message_success() {
let temp_repo = create_temp_git_repo().await.unwrap();
create_test_commit(temp_repo.path(), "Initial commit")
.await
.unwrap();
create_test_commit(temp_repo.path(), "Feature: Add new functionality")
.await
.unwrap();
let result = get_last_commit_message_in_dir(temp_repo.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Feature: Add new functionality");
}
#[tokio::test]
async fn test_get_last_commit_message_no_commits() {
let temp_repo = create_temp_git_repo().await.unwrap();
let result = get_last_commit_message_in_dir(temp_repo.path()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_stage_all_changes_success() {
let temp_repo = create_temp_git_repo().await.unwrap();
create_test_commit(temp_repo.path(), "Initial commit")
.await
.unwrap();
std::fs::write(temp_repo.path().join("new_file.txt"), "content").unwrap();
let result = stage_all_changes_in_dir(temp_repo.path()).await;
assert!(result.is_ok());
let status = check_git_status_in_dir(temp_repo.path()).await.unwrap();
assert!(status.contains("new_file.txt"));
}
#[tokio::test]
async fn test_stage_all_changes_no_changes() {
let temp_repo = create_temp_git_repo().await.unwrap();
create_test_commit(temp_repo.path(), "Initial commit")
.await
.unwrap();
let result = stage_all_changes_in_dir(temp_repo.path()).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_create_commit_success() {
let temp_repo = create_temp_git_repo().await.unwrap();
create_test_commit(temp_repo.path(), "Initial commit")
.await
.unwrap();
std::fs::write(temp_repo.path().join("new_test.txt"), "new content").unwrap();
stage_all_changes_in_dir(temp_repo.path()).await.unwrap();
let result = create_commit_in_dir(temp_repo.path(), "test: Add test file").await;
assert!(result.is_ok());
let last_message = get_last_commit_message_in_dir(temp_repo.path())
.await
.unwrap();
assert_eq!(last_message, "test: Add test file");
}
#[tokio::test]
async fn test_create_commit_no_staged_changes() {
let temp_repo = create_temp_git_repo().await.unwrap();
create_test_commit(temp_repo.path(), "Initial commit")
.await
.unwrap();
let result = create_commit_in_dir(temp_repo.path(), "test: Empty commit").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_check_git_status_success() {
let temp_dir = create_temp_git_repo().await.unwrap();
let status = check_git_status_in_dir(temp_dir.path()).await.unwrap();
assert_eq!(status.trim(), "", "Expected empty status for clean repo");
}
#[tokio::test]
async fn test_check_git_status_with_changes() {
let temp_dir = create_temp_git_repo().await.unwrap();
std::fs::write(temp_dir.path().join("test.txt"), "test content").unwrap();
let status = check_git_status_in_dir(temp_dir.path()).await.unwrap();
assert!(
status.contains("?? test.txt"),
"Expected untracked file in status: {status}"
);
}
#[tokio::test]
async fn test_public_api_functions_in_real_repo() {
if !is_git_repo().await {
eprintln!("Skipping test - not in a git repository");
return;
}
let result = git_command(&["status", "--porcelain"], "Check status").await;
assert!(result.is_ok(), "git_command should succeed in MMM repo");
let status = check_git_status().await;
assert!(status.is_ok(), "Should be able to check status in MMM repo");
let message = get_last_commit_message().await;
assert!(
message.is_ok(),
"Should get last commit message in MMM repo"
);
}
#[tokio::test]
async fn test_mutex_synchronization() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let counter = Arc::new(AtomicUsize::new(0));
let tasks: Vec<_> = (0..5)
.map(|_| {
let counter = counter.clone();
tokio::spawn(async move {
let _result = check_git_status().await;
counter.fetch_add(1, Ordering::SeqCst);
})
})
.collect();
for task in tasks {
task.await.unwrap();
}
assert_eq!(
counter.load(Ordering::SeqCst),
5,
"All tasks should complete"
);
}
}
#[cfg(test)]
mod mock_tests {
use super::tests::{create_commit_in_dir, create_temp_git_repo, stage_all_changes_in_dir};
#[tokio::test]
async fn test_stage_all_changes_with_mock() {
let temp_repo = match create_temp_git_repo().await {
Ok(repo) => repo,
Err(e) => {
eprintln!("Skipping test - could not create temp git repo: {e}");
return;
}
};
std::fs::write(temp_repo.path().join("test_stage.txt"), "content").unwrap();
let result = stage_all_changes_in_dir(temp_repo.path()).await;
assert!(result.is_ok(), "stage_all_changes_in_dir should succeed");
}
#[tokio::test]
async fn test_create_commit_with_mock() {
let temp_repo = match create_temp_git_repo().await {
Ok(repo) => repo,
Err(e) => {
eprintln!("Skipping test - could not create temp git repo: {e}");
return;
}
};
std::fs::write(temp_repo.path().join("test_commit.txt"), "content").unwrap();
let stage_result = stage_all_changes_in_dir(temp_repo.path()).await;
assert!(
stage_result.is_ok(),
"stage_all_changes_in_dir should succeed"
);
let result = create_commit_in_dir(temp_repo.path(), "test: mock commit").await;
assert!(result.is_ok(), "create_commit_in_dir should succeed");
}
}