use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
pub struct TestGitRepository {
#[allow(dead_code)]
temp_dir: TempDir,
repo_path: PathBuf,
}
pub struct ExistingGitRepository {
repo_path: PathBuf,
}
impl ExistingGitRepository {
pub fn new(path: &Path) -> Result<Self> {
if !path.exists() {
anyhow::bail!("Repository path does not exist: {}", path.display());
}
Ok(Self {
repo_path: path.to_path_buf(),
})
}
pub fn configure_test_user(&self) -> Result<()> {
self.run_git_command(&["config", "user.email", "test@example.com"])?;
self.run_git_command(&["config", "user.name", "Test User"])?;
Ok(())
}
pub fn stage_all(&self) -> Result<()> {
self.run_git_command(&["add", "-A"])
.context("Failed to stage files")?;
Ok(())
}
pub fn commit(&self, message: &str) -> Result<String> {
self.run_git_command(&["commit", "-m", message])
.context("Failed to create commit")?;
self.get_current_commit()
}
pub fn get_current_commit(&self) -> Result<String> {
let output = self
.run_git_command(&["rev-parse", "HEAD"])
.context("Failed to get current commit")?;
Ok(output.trim().to_string())
}
pub fn run_git_command(&self, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.current_dir(&self.repo_path)
.output()
.context("Failed to execute git command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Git command failed: {}", stderr);
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
impl TestGitRepository {
pub fn new() -> Result<Self> {
let temp_dir = TempDir::new().context("Failed to create temp dir")?;
let repo_path = temp_dir.path().to_path_buf();
Ok(Self {
temp_dir,
repo_path,
})
}
pub fn path(&self) -> &Path {
&self.repo_path
}
pub fn init(&self) -> Result<()> {
self.run_git_command(&["init"])
.context("Failed to initialize git repository")?;
self.run_git_command(&["config", "user.email", "test@example.com"])?;
self.run_git_command(&["config", "user.name", "Test User"])?;
Ok(())
}
pub fn init_with_commit(&self) -> Result<String> {
self.init()?;
self.create_file("README.md", "# Test Repository\n")?;
self.stage_all()?;
self.commit("Initial commit")
}
pub fn create_file(&self, path: &str, content: &str) -> Result<()> {
let file_path = self.repo_path.join(path);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directories for {path}"))?;
}
fs::write(&file_path, content).with_context(|| format!("Failed to write file {path}"))?;
Ok(())
}
pub fn read_file(&self, path: &str) -> Result<String> {
let file_path = self.repo_path.join(path);
fs::read_to_string(&file_path).with_context(|| format!("Failed to read file {path}"))
}
pub fn stage_all(&self) -> Result<()> {
self.run_git_command(&["add", "-A"])
.context("Failed to stage files")?;
Ok(())
}
pub fn commit(&self, message: &str) -> Result<String> {
self.run_git_command(&["commit", "-m", message])
.context("Failed to create commit")?;
self.get_current_commit()
}
pub fn get_current_commit(&self) -> Result<String> {
let output = self
.run_git_command(&["rev-parse", "HEAD"])
.context("Failed to get current commit")?;
Ok(output.trim().to_string())
}
pub fn get_head_commit(&self) -> Result<String> {
self.get_current_commit()
}
pub fn current_branch(&self) -> Result<String> {
let output = self
.run_git_command(&["branch", "--show-current"])
.context("Failed to get current branch")?;
Ok(output.trim().to_string())
}
pub fn checkout_new_branch(&self, branch_name: &str) -> Result<()> {
self.run_git_command(&["checkout", "-b", branch_name])
.context("Failed to create and checkout new branch")?;
Ok(())
}
pub fn checkout_branch(&self, branch_name: &str) -> Result<()> {
self.run_git_command(&["checkout", branch_name])
.context("Failed to checkout branch")?;
Ok(())
}
pub fn status(&self) -> Result<String> {
self.run_git_command(&["status", "--porcelain"])
.context("Failed to get git status")
}
pub fn branches(&self) -> Result<Vec<String>> {
let output = self
.run_git_command(&["branch", "--format=%(refname:short)"])
.context("Failed to list branches")?;
Ok(output
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
pub fn setup_with_uncommitted_changes(&self) -> Result<()> {
self.init_with_commit()?;
self.create_file("unstaged.txt", "This file is not staged\n")?;
self.create_file("staged.txt", "This file is staged\n")?;
self.run_git_command(&["add", "staged.txt"])?;
self.create_file("README.md", "# Test Repository\n\nModified content\n")?;
Ok(())
}
pub fn setup_with_branches(&self, branches: Vec<&str>) -> Result<()> {
self.init_with_commit()?;
let main_branch = self.current_branch()?;
for branch in branches {
self.checkout_new_branch(branch)?;
self.create_file(&format!("{branch}.txt"), &format!("Content for {branch}\n"))?;
self.stage_all()?;
self.commit(&format!("Add {branch}.txt"))?;
self.checkout_branch(&main_branch)?;
}
Ok(())
}
pub fn setup_non_git_directory(&self) -> Result<()> {
self.create_file("file.txt", "Not a git repository\n")?;
Ok(())
}
pub fn init_with_main_branch(&self) -> Result<String> {
self.init()?;
self.run_git_command(&["config", "init.defaultBranch", "main"])?;
self.run_git_command(&["checkout", "-b", "main"])?;
self.create_file("README.md", "# Test Repository\n")?;
self.stage_all()?;
self.commit("Initial commit")
}
pub fn clone_from(&self, source: &TestGitRepository) -> Result<String> {
self.init()?;
self.run_git_command(&[
"remote",
"add",
"origin",
source
.path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid source path"))?,
])?;
self.run_git_command(&["fetch", "origin"])?;
let branch = source.current_branch()?;
self.run_git_command(&["checkout", "-b", &branch, &format!("origin/{branch}")])?;
self.get_current_commit()
}
pub fn add_submodule(&self, source: &TestGitRepository, path: &str) -> Result<()> {
self.run_git_command(&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
source
.path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid source path"))?,
path,
])?;
Ok(())
}
pub fn run_git_command(&self, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.current_dir(&self.repo_path)
.output()
.context("Failed to execute git command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Git command failed: {}", stderr);
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_repository() -> Result<()> {
let repo = TestGitRepository::new()?;
repo.init()?;
assert!(repo.path().exists());
assert!(repo.path().join(".git").exists());
Ok(())
}
#[test]
fn test_init_with_commit() -> Result<()> {
let repo = TestGitRepository::new()?;
let commit_sha = repo.init_with_commit()?;
assert!(!commit_sha.is_empty());
assert_eq!(commit_sha.len(), 40);
let readme_content = repo.read_file("README.md")?;
assert_eq!(readme_content, "# Test Repository\n");
Ok(())
}
#[test]
fn test_create_and_commit_file() -> Result<()> {
let repo = TestGitRepository::new()?;
repo.init()?;
repo.create_file("test.txt", "Hello, World!")?;
repo.stage_all()?;
let commit_sha = repo.commit("Add test file")?;
assert!(!commit_sha.is_empty());
let content = repo.read_file("test.txt")?;
assert_eq!(content, "Hello, World!");
Ok(())
}
#[test]
fn test_branch_operations() -> Result<()> {
let repo = TestGitRepository::new()?;
repo.init_with_commit()?;
let initial_branch = repo.current_branch()?;
assert!(initial_branch == "main" || initial_branch == "master");
repo.checkout_new_branch("feature")?;
assert_eq!(repo.current_branch()?, "feature");
repo.checkout_branch(&initial_branch)?;
assert_eq!(repo.current_branch()?, initial_branch);
Ok(())
}
#[test]
fn test_setup_with_uncommitted_changes() -> Result<()> {
let repo = TestGitRepository::new()?;
repo.setup_with_uncommitted_changes()?;
let status = repo.status()?;
assert!(status.contains("unstaged.txt"));
assert!(status.contains("staged.txt"));
assert!(status.contains("README.md"));
Ok(())
}
#[test]
fn test_setup_with_branches() -> Result<()> {
let repo = TestGitRepository::new()?;
repo.setup_with_branches(vec!["feature", "bugfix", "develop"])?;
let branches = repo.branches()?;
assert!(branches.iter().any(|b| b == "feature"));
assert!(branches.iter().any(|b| b == "bugfix"));
assert!(branches.iter().any(|b| b == "develop"));
Ok(())
}
}