#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use super::super::*;
use crate::test_utils::TestGit;
use crate::utils::normalize_path_for_storage;
use anyhow::Result;
use tempfile::TempDir;
#[test]
fn test_is_git_installed() -> Result<()> {
assert!(is_git_installed());
Ok(())
}
#[test]
fn test_parse_git_url() -> Result<()> {
let cases = vec![
("https://github.com/user/repo.git", ("user", "repo")),
("git@github.com:user/repo.git", ("user", "repo")),
("https://gitlab.com/user/repo", ("user", "repo")),
("https://bitbucket.org/user/repo.git", ("user", "repo")),
("ssh://git@github.com/user/repo.git", ("user", "repo")),
("https://github.com/rust-lang/cargo.git", ("rust-lang", "cargo")),
("git@gitlab.com:group/project.git", ("group", "project")),
("ssh://git@bitbucket.org/team/repo", ("team", "repo")),
("https://github.com/user-name/repo-name", ("user-name", "repo-name")),
];
for (url, (expected_owner, expected_repo)) in cases {
let result = parse_git_url(url)?;
assert_eq!(result.0, expected_owner, "Owner mismatch for URL: {url}");
assert_eq!(result.1, expected_repo, "Repo mismatch for URL: {url}");
}
assert!(parse_git_url("not-a-url").is_err());
Ok(())
}
#[test]
fn test_parse_git_url_edge_cases() -> Result<()> {
let invalid_urls = vec!["not-a-url", "https://example.com/something", ""];
for url in invalid_urls {
assert!(parse_git_url(url).is_err(), "Expected error for invalid URL: {url}");
}
for path in ["/local/path/to/repo", "./relative/path", "../parent/path"] {
parse_git_url(path)?;
}
Ok(())
}
#[test]
fn test_parse_git_url_file_urls() -> Result<()> {
let test_cases = vec![
("file:///home/user/repos/myrepo", ("local", "myrepo")),
("file:///home/user/repos/myrepo.git", ("local", "myrepo")),
("file:///tmp/test", ("local", "test")),
("file:///var/folders/sources/official", ("local", "official")),
("ssh://git@github.com:22/user/repo.git", ("user", "repo")),
("https://gitlab.com/group/subgroup/project.git", ("subgroup", "project")),
("https://github.com/user/repo", ("user", "repo")),
];
for (url, (expected_owner, expected_repo)) in test_cases {
let result = parse_git_url(url)?;
assert_eq!(result.0, expected_owner, "Owner mismatch for {url}");
assert_eq!(result.1, expected_repo, "Repo mismatch for {url}");
}
Ok(())
}
#[test]
fn test_is_git_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo = GitRepo::new(temp_dir.path());
assert!(!repo.is_git_repo());
let git = TestGit::new(temp_dir.path());
git.init()?;
assert!(repo.is_git_repo());
Ok(())
}
#[test]
fn test_git_repo_path() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo = GitRepo::new(temp_dir.path());
assert_eq!(repo.path(), temp_dir.path());
Ok(())
}
#[tokio::test]
async fn test_clone_local_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let target_path = temp_dir.path().join("target");
std::fs::create_dir(&source_path)?;
let git = TestGit::new(&source_path);
git.init_bare()?;
let cloned_repo = GitRepo::clone(source_path.to_str().unwrap(), &target_path).await?;
assert!(cloned_repo.is_git_repo());
Ok(())
}
#[tokio::test]
async fn test_clone_with_progress() -> Result<()> {
let temp_dir = TempDir::new()?;
let bare_path = temp_dir.path().join("bare");
let clone_path = temp_dir.path().join("clone");
std::fs::create_dir(&bare_path).unwrap();
let git = TestGit::new(&bare_path);
git.init_bare().unwrap();
let repo = GitRepo::clone(bare_path.to_str().unwrap(), &clone_path).await?;
assert!(repo.is_git_repo());
assert!(clone_path.exists());
Ok(())
}
#[tokio::test]
async fn test_clone_invalid_url() -> Result<()> {
let target_dir = TempDir::new().unwrap();
let target_path = target_dir.path().join("cloned");
let invalid_urls = vec![
"/non/existent/path",
"http://invalid-git-url.test",
"not-a-url",
"",
"https://invalid.host.that.does.not.exist.9999/repo.git",
];
for url in invalid_urls {
let result = GitRepo::clone(url, &target_path).await;
assert!(result.is_err(), "Expected error for URL: {url}");
}
assert!(!target_path.exists());
Ok(())
}
#[tokio::test]
async fn test_fetch_simple() -> Result<()> {
let temp_dir = TempDir::new()?;
let bare_path = temp_dir.path().join("bare");
let clone_path = temp_dir.path().join("clone");
std::fs::create_dir(&bare_path).unwrap();
let git = TestGit::new(&bare_path);
git.init_bare().unwrap();
let repo = GitRepo::clone(bare_path.to_str().unwrap(), &clone_path).await?;
repo.fetch(None).await?;
Ok(())
}
#[tokio::test]
async fn test_fetch_with_no_network() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.remote_add("origin", "https://non-existent-host-9999.test/repo.git")?;
let repo = GitRepo::new(repo_path);
let result = repo.fetch(None).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Git operation failed: fetch"));
Ok(())
}
#[tokio::test]
async fn test_checkout() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("README.md"), "Test").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
git.tag("v1.0.0").unwrap();
std::fs::write(repo_path.join("file2.txt"), "Test2").unwrap();
git.add_all().unwrap();
git.commit("Second commit")?;
let repo = GitRepo::new(repo_path);
repo.checkout("v1.0.0").await?;
assert!(!repo_path.join("file2.txt").exists());
Ok(())
}
#[tokio::test]
async fn test_checkout_branch() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("initial.txt"), "Initial commit").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let default_branch = git.get_default_branch()?;
git.ensure_branch("feature")?;
std::fs::write(repo_path.join("feature.txt"), "Feature branch").unwrap();
git.add_all().unwrap();
git.commit("Feature commit")?;
let repo = GitRepo::new(repo_path);
assert_eq!(repo.get_current_branch().await?, "feature");
assert!(repo_path.join("feature.txt").exists());
repo.checkout(&default_branch).await?;
assert!(!repo_path.join("feature.txt").exists());
assert!(repo_path.join("initial.txt").exists());
repo.checkout("feature").await?;
assert!(repo_path.join("feature.txt").exists());
Ok(())
}
#[tokio::test]
async fn test_checkout_commit_hash() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("file1.txt"), "content1")?;
git.add_all()?;
git.commit("First commit")?;
let first_commit = git.rev_parse_head()?;
std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
git.add_all().unwrap();
git.commit("Second commit")?;
let repo = GitRepo::new(repo_path);
repo.checkout(&first_commit).await?;
assert!(repo_path.join("file1.txt").exists());
assert!(!repo_path.join("file2.txt").exists());
Ok(())
}
#[tokio::test]
async fn test_checkout_invalid_ref() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let repo = GitRepo::new(repo_path);
let result = repo.checkout("non-existent-branch").await;
assert!(result.is_err());
let error_message = format!("{:?}", result.unwrap_err());
assert!(error_message.contains("Failed to checkout"));
Ok(())
}
#[tokio::test]
async fn test_list_tags() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let tags_to_add = vec!["v1.0.0", "v1.1.0", "v2.0.0-beta", "v1.2.0", "v3.0.0-alpha"];
for tag in &tags_to_add {
git.tag(tag).unwrap();
}
let repo = GitRepo::new(repo_path);
let listed_tags = repo.list_tags().await?;
assert_eq!(listed_tags.len(), 5);
for tag in tags_to_add {
assert!(listed_tags.contains(&tag.to_string()));
}
Ok(())
}
#[tokio::test]
async fn test_get_remote_url() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.remote_add("origin", "https://github.com/test/repo.git")?;
let repo = GitRepo::new(repo_path);
let url = repo.get_remote_url().await?;
assert!(
url == "https://github.com/test/repo.git"
|| url == "ssh://git@github.com/test/repo.git"
|| url == "git@github.com:test/repo.git"
);
Ok(())
}
#[tokio::test]
async fn test_get_remote_url_no_remote() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
let repo = GitRepo::new(repo_path);
let result = repo.get_remote_url().await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_get_current_branch() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("README.md"), "Test").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let repo = GitRepo::new(repo_path);
let branch = repo.get_current_branch().await?;
assert!(branch == "main" || branch == "master");
Ok(())
}
#[tokio::test]
async fn test_error_handling_non_git_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let path = temp_dir.path().to_path_buf();
let fake_repo = GitRepo {
path,
tag_cache: std::sync::Arc::new(std::sync::OnceLock::new()),
};
let result = fake_repo.fetch(None).await;
assert!(result.is_err());
let result = fake_repo.get_current_branch().await;
assert!(result.is_err());
let result = fake_repo.list_tags().await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_concurrent_operations() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
let git = TestGit::new(&repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("initial.txt"), "initial").unwrap();
git.add_all().unwrap();
git.commit("Initial")?;
let path1 = repo_path.clone();
let path2 = repo_path.clone();
let handle1 = tokio::spawn(async move {
let repo = GitRepo::new(&path1);
repo.list_tags().await
});
let handle2 = tokio::spawn(async move {
let repo = GitRepo::new(&path2);
repo.get_current_branch().await
});
let _result1 = handle1.await.unwrap()?;
let _result2 = handle2.await.unwrap()?;
Ok(())
}
#[tokio::test]
async fn test_trait_implementation() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let repo = GitRepo::new(repo_path);
assert!(repo.is_git_repo());
assert!(repo.path().exists());
let tags = repo.list_tags().await?;
assert_eq!(tags.len(), 0);
Ok(())
}
#[tokio::test]
async fn test_clone_permission_denied() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let target_path = temp_dir.path().join("target");
std::fs::create_dir(&source_path)?;
let source_git = TestGit::new(&source_path);
source_git.init_bare()?;
std::fs::create_dir(&target_path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&target_path)?.permissions();
perms.set_mode(0o444); std::fs::set_permissions(&target_path, perms)?;
}
let source_url = format!("file://{}", normalize_path_for_storage(&source_path));
let result = GitRepo::clone(&source_url, &target_path).await;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&target_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&target_path, perms)?;
}
#[cfg(unix)]
assert!(result.is_err());
#[cfg(windows)]
let _ = result; Ok(())
}
#[tokio::test]
async fn test_fetch_local_repository() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().join("repo");
let origin_path = temp_dir.path().join("origin");
std::fs::create_dir_all(&origin_path)?;
let origin_git = TestGit::new(&origin_path);
origin_git.init_bare()?;
std::fs::create_dir_all(&repo_path)?;
let git = TestGit::new(&repo_path);
git.init()?;
let origin_url = format!("file://{}", origin_path.display());
git.remote_add("origin", &origin_url)?;
let repo = GitRepo::new(&repo_path);
let result = repo.fetch(None).await;
assert!(result.is_ok(), "Fetch failed: {:?}", result.err());
Ok(())
}
#[tokio::test]
async fn test_fetch_git_protocol() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
let bare_repo = temp_dir.path().join("bare");
std::fs::create_dir(&bare_repo)?;
let bare_git = TestGit::new(&bare_repo);
bare_git.init_bare()?;
bare_git.remote_add("origin", &format!("file://{}", bare_repo.display()))?;
let repo = GitRepo::new(repo_path);
repo.fetch(None).await?;
Ok(())
}
#[tokio::test]
async fn test_fetch_with_auth_url() -> Result<()> {
let temp_dir = TempDir::new()?;
let bare_path = temp_dir.path().join("bare");
let repo_path = temp_dir.path().join("repo");
std::fs::create_dir(&bare_path)?;
let git = TestGit::new(&bare_path);
git.init_bare()?;
let repo = GitRepo::clone(bare_path.to_str().unwrap(), &repo_path).await?;
let auth_url = format!("file://{}", bare_path.display());
repo.fetch(Some(&auth_url)).await?;
Ok(())
}
#[tokio::test]
async fn test_list_tags_non_git_directory() -> Result<()> {
let temp_dir = TempDir::new()?;
let non_git_path = temp_dir.path().join("not_git");
std::fs::create_dir(&non_git_path)?;
let repo = GitRepo::new(&non_git_path);
let result = repo.list_tags().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Not a git repository"));
Ok(())
}
#[tokio::test]
async fn test_list_tags_non_existent_directory() -> Result<()> {
let temp_dir = TempDir::new()?;
let non_existent = temp_dir.path().join("does_not_exist");
let repo = GitRepo::new(&non_existent);
let result = repo.list_tags().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Repository path does not exist"));
Ok(())
}
#[tokio::test]
async fn test_verify_url_file_protocol() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().join("repo");
std::fs::create_dir(&repo_path).unwrap();
let file_url = format!("file://{}", repo_path.display());
GitRepo::verify_url(&file_url).await?;
let bad_file_url = "file:///non/existent/path";
let result = GitRepo::verify_url(bad_file_url).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Local path does not exist"));
Ok(())
}
#[tokio::test]
async fn test_verify_url_remote() -> Result<()> {
let result = GitRepo::verify_url("https://invalid-host-9999.test/repo.git").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to verify remote repository"));
Ok(())
}
#[test]
fn test_strip_auth_from_url() -> Result<()> {
let url = "https://user:pass@github.com/owner/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "https://github.com/owner/repo.git");
let url = "https://oauth2:ghp_xxxx@github.com/owner/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "https://github.com/owner/repo.git");
let url = "http://user:pass@example.com/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "http://example.com/repo.git");
let url = "https://github.com/owner/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "https://github.com/owner/repo.git");
let url = "git@github.com:owner/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "git@github.com:owner/repo.git");
let url = "https://example.com/user@domain/repo.git";
let result = strip_auth_from_url(url)?;
assert_eq!(result, "https://example.com/user@domain/repo.git");
Ok(())
}
#[test]
fn test_parse_git_url_local_paths() -> Result<()> {
let result = parse_git_url("/absolute/path/to/repo")?;
assert_eq!(result.0, "local");
assert_eq!(result.1, "repo");
let result = parse_git_url("./relative/path/repo.git")?;
assert_eq!(result.0, "local");
assert_eq!(result.1, "repo");
let result = parse_git_url("../parent/repo")?;
assert_eq!(result.0, "local");
assert_eq!(result.1, "repo");
let result = parse_git_url("repo.git");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_ensure_git_available() -> Result<()> {
ensure_git_available()?;
Ok(())
}
#[test]
fn test_ensure_valid_git_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let result = ensure_valid_git_repo(repo_path);
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("git repository") || err_str.contains("Git repository"));
let git = TestGit::new(repo_path);
git.init().unwrap();
ensure_valid_git_repo(repo_path)?;
Ok(())
}
#[test]
fn test_is_valid_git_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
assert!(!is_valid_git_repo(repo_path));
let git = TestGit::new(repo_path);
git.init()?;
assert!(is_valid_git_repo(repo_path));
Ok(())
}
#[tokio::test]
async fn test_checkout_reset_error_handling() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial")?;
git.tag("v1.0.0")?;
let repo = GitRepo::new(repo_path);
repo.checkout("v1.0.0").await?;
let result = repo.checkout("non-existent").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Failed to checkout"));
Ok(())
}
#[tokio::test]
async fn test_get_remote_url_stderr() -> Result<()> {
let temp_dir = TempDir::new()?;
let non_git_path = temp_dir.path().join("not_git");
std::fs::create_dir(&non_git_path)?;
let repo = GitRepo::new(&non_git_path);
let result = repo.get_remote_url().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Git operation failed"));
Ok(())
}
#[tokio::test]
async fn test_concurrent_git_operations_same_repo() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init().unwrap();
git.config_user()?;
for i in 0..3 {
let file_name = format!("file{i}.txt");
std::fs::write(repo_path.join(&file_name), format!("content{i}")).unwrap();
git.add_all().unwrap();
git.commit(&format!("Commit {i}"))?;
git.tag(&format!("v{i}.0.0"))?;
}
let repo1 = GitRepo::new(repo_path);
let repo2 = GitRepo::new(repo_path);
let repo3 = GitRepo::new(repo_path);
let handle1 = tokio::spawn(async move { repo1.list_tags().await });
let handle2 = tokio::spawn(async move { repo2.get_current_branch().await });
let handle3 = tokio::spawn(async move { repo3.checkout("v1.0.0").await });
let results = tokio::join!(handle1, handle2, handle3);
results.0.unwrap()?;
results.1.unwrap()?;
results.2.unwrap()?;
Ok(())
}
#[tokio::test]
async fn test_clone_bare() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
std::fs::create_dir(&source_path)?;
let git = TestGit::new(&source_path);
git.init()?;
git.config_user()?;
std::fs::write(source_path.join("README.md"), "# Test")?;
git.add_all()?;
git.commit("Initial commit")?;
let file_url = format!("file://{}", source_path.display());
let result = GitRepo::clone_bare(&file_url, &bare_path).await;
assert!(result.is_ok(), "Failed to clone bare: {:?}", result.err());
let bare_repo = result?;
assert!(bare_repo.path().exists());
let has_objects = bare_repo.path().join("objects").exists();
let has_refs = bare_repo.path().join("refs").exists();
let has_head = bare_repo.path().join("HEAD").exists();
assert!(has_objects, "Bare repo missing objects directory");
assert!(has_refs, "Bare repo missing refs directory");
assert!(has_head, "Bare repo missing HEAD file");
let is_bare = bare_repo.is_bare().await?;
assert!(is_bare);
Ok(())
}
#[tokio::test]
async fn test_clone_bare_with_context() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
std::fs::create_dir(&source_path).unwrap();
let git = TestGit::new(&source_path);
git.init().unwrap();
GitRepo::clone_bare_with_context(
source_path.to_str().unwrap(),
&bare_path,
Some("test-dependency"),
)
.await?;
Ok(())
}
#[tokio::test]
async fn test_create_worktree() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
let worktree_path = temp_dir.path().join("worktree");
std::fs::create_dir(&source_path)?;
let git = TestGit::new(&source_path);
git.init()?;
git.config_user()?;
std::fs::write(source_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
git.tag("v1.0.0")?;
let bare_repo = GitRepo::clone_bare(source_path.to_str().unwrap(), &bare_path).await?;
let worktree = bare_repo.create_worktree(&worktree_path, Some("v1.0.0")).await?;
assert!(worktree.is_git_repo());
assert!(worktree_path.join("file.txt").exists());
Ok(())
}
#[tokio::test]
async fn test_create_worktree_with_context() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
let worktree_path = temp_dir.path().join("worktree");
std::fs::create_dir(&source_path)?;
let source_git = TestGit::new(&source_path);
source_git.init_bare()?;
let bare_repo = GitRepo::clone_bare(source_path.to_str().unwrap(), &bare_path).await?;
let result = bare_repo
.create_worktree_with_context(&worktree_path, None, Some("test-dependency"))
.await;
let _ = result; Ok(())
}
#[tokio::test]
async fn test_remove_worktree() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
let worktree_path = temp_dir.path().join("worktree");
std::fs::create_dir(&source_path).unwrap();
let git = TestGit::new(&source_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(source_path.join("file.txt"), "content").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let bare_repo =
GitRepo::clone_bare(source_path.to_str().unwrap(), &bare_path).await.unwrap();
let _worktree = bare_repo.create_worktree(&worktree_path, None).await.unwrap();
assert!(worktree_path.exists());
bare_repo.remove_worktree(&worktree_path).await?;
Ok(())
}
#[tokio::test]
async fn test_list_worktrees() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
std::fs::create_dir(&source_path)?;
let git = TestGit::new(&source_path);
git.init()?;
git.config_user()?;
std::fs::write(source_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
let bare_repo = GitRepo::clone_bare(source_path.to_str().unwrap(), &bare_path).await?;
let worktrees = bare_repo.list_worktrees().await?;
assert!(worktrees.len() <= 1); Ok(())
}
#[tokio::test]
async fn test_prune_worktrees() -> Result<()> {
let temp_dir = TempDir::new()?;
let source_path = temp_dir.path().join("source");
let bare_path = temp_dir.path().join("bare.git");
std::fs::create_dir(&source_path).unwrap();
let git = TestGit::new(&source_path);
git.init().unwrap();
git.config_user()?;
std::fs::write(source_path.join("file.txt"), "content").unwrap();
git.add_all().unwrap();
git.commit("Initial commit")?;
let bare_repo =
GitRepo::clone_bare(source_path.to_str().unwrap(), &bare_path).await.unwrap();
bare_repo.prune_worktrees().await?;
Ok(())
}
#[tokio::test]
async fn test_is_bare() -> Result<()> {
let temp_dir = TempDir::new()?;
let normal_repo_path = temp_dir.path().join("normal");
let bare_repo_path = temp_dir.path().join("bare.git");
std::fs::create_dir(&normal_repo_path)?;
let normal_git = TestGit::new(&normal_repo_path);
normal_git.init()?;
std::fs::create_dir(&bare_repo_path)?;
let bare_git = TestGit::new(&bare_repo_path);
bare_git.init_bare()?;
let normal_repo = GitRepo::new(&normal_repo_path);
let bare_repo = GitRepo::new(&bare_repo_path);
let is_bare = normal_repo.is_bare().await?;
assert!(!is_bare);
let is_bare = bare_repo.is_bare().await?;
assert!(is_bare);
Ok(())
}
#[tokio::test]
async fn test_get_current_commit() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
let repo = GitRepo::new(repo_path);
let commit = repo.get_current_commit().await?;
assert_eq!(commit.len(), 40);
assert!(commit.chars().all(|c| c.is_ascii_hexdigit()));
Ok(())
}
#[tokio::test]
async fn test_get_current_commit_error() -> Result<()> {
let temp_dir = TempDir::new()?;
let non_git_path = temp_dir.path().join("not_git");
std::fs::create_dir(&non_git_path)?;
let repo = GitRepo::new(&non_git_path);
let result = repo.get_current_commit().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to get current commit"));
Ok(())
}
#[tokio::test]
async fn test_checkout_error_handling() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
let repo = GitRepo::new(repo_path);
let result = repo.checkout("definitely-does-not-exist").await;
assert!(result.is_err());
let error_str = result.unwrap_err().to_string();
assert!(
error_str.contains("Failed to checkout") || error_str.contains("GitCheckoutFailed")
);
Ok(())
}
#[tokio::test]
async fn test_resolve_to_sha() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
let expected_sha = git.rev_parse_head()?;
git.tag("v1.0.0")?;
let repo = GitRepo::new(repo_path);
let sha = repo.resolve_to_sha(None).await?;
assert_eq!(sha, expected_sha);
let sha = repo.resolve_to_sha(Some("HEAD")).await?;
assert_eq!(sha, expected_sha);
let sha = repo.resolve_to_sha(Some("v1.0.0")).await?;
assert_eq!(sha, expected_sha);
let full_sha = "a".repeat(40);
let sha = repo.resolve_to_sha(Some(&full_sha)).await?;
assert_eq!(sha, full_sha);
let default_branch = git.get_default_branch()?;
let sha = repo.resolve_to_sha(Some(&default_branch)).await?;
assert_eq!(sha, expected_sha);
let result = repo.resolve_to_sha(Some("nonexistent")).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_resolve_to_sha_with_multiple_commits() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file1.txt"), "content1")?;
git.add_all()?;
git.commit("First commit")?;
git.tag("v1.0.0")?;
let first_sha = git.rev_parse_head()?;
std::fs::write(repo_path.join("file2.txt"), "content2")?;
git.add_all()?;
git.commit("Second commit")?;
git.tag("v2.0.0")?;
let second_sha = git.rev_parse_head()?;
let repo = GitRepo::new(repo_path);
let sha_v1 = repo.resolve_to_sha(Some("v1.0.0")).await?;
assert_eq!(sha_v1, first_sha);
let sha_v2 = repo.resolve_to_sha(Some("v2.0.0")).await?;
assert_eq!(sha_v2, second_sha);
let sha_head = repo.resolve_to_sha(Some("HEAD")).await?;
assert_eq!(sha_head, second_sha);
let short_sha = &first_sha[..7];
let resolved = repo.resolve_to_sha(Some(short_sha)).await?;
assert_eq!(resolved, first_sha);
Ok(())
}
#[tokio::test]
async fn test_file_url_clone_error_reporting() -> Result<()> {
let temp_dir = TempDir::new()?;
let target_path = temp_dir.path().join("target");
let invalid_file_url = "file:///non/existent/path/that/does/not/exist";
let result = GitRepo::clone(invalid_file_url, &target_path).await;
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains(invalid_file_url),
"Error message should contain the actual file:// URL, not 'unknown'. \
Got: {}",
error_msg
);
assert!(
!error_msg.contains("unknown"),
"Error message should not contain 'unknown'. Got: {}",
error_msg
);
Ok(())
}
#[tokio::test]
async fn test_resolve_refs_batch_empty() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
let repo = GitRepo::new(repo_path);
let results = repo.resolve_refs_batch(&[]).await?;
assert!(results.is_empty());
Ok(())
}
#[tokio::test]
async fn test_resolve_refs_batch_already_shas() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
let repo = GitRepo::new(repo_path);
let full_sha = "a".repeat(40);
let refs = vec![full_sha.as_str()];
let results = repo.resolve_refs_batch(&refs).await?;
assert_eq!(results.len(), 1);
assert_eq!(results.get(&full_sha), Some(&Some(full_sha.clone())));
Ok(())
}
#[tokio::test]
async fn test_resolve_refs_batch_tags_and_branches() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file1.txt"), "content1")?;
git.add_all()?;
git.commit("Initial commit")?;
git.tag("v1.0.0")?;
let first_sha = git.rev_parse_head()?;
std::fs::write(repo_path.join("file2.txt"), "content2")?;
git.add_all()?;
git.commit("Second commit")?;
git.tag("v2.0.0")?;
let second_sha = git.rev_parse_head()?;
let repo = GitRepo::new(repo_path);
let refs = vec!["v1.0.0", "v2.0.0", "HEAD"];
let results = repo.resolve_refs_batch(&refs).await?;
assert_eq!(results.len(), 3);
assert_eq!(results.get("v1.0.0"), Some(&Some(first_sha.clone())));
assert_eq!(results.get("v2.0.0"), Some(&Some(second_sha.clone())));
assert_eq!(results.get("HEAD"), Some(&Some(second_sha)));
Ok(())
}
#[tokio::test]
async fn test_resolve_refs_batch_mixed_valid_invalid() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
git.tag("v1.0.0")?;
let sha = git.rev_parse_head()?;
let repo = GitRepo::new(repo_path);
let refs = vec!["v1.0.0", "nonexistent-ref"];
let results = repo.resolve_refs_batch(&refs).await?;
assert_eq!(results.len(), 2);
assert_eq!(results.get("v1.0.0"), Some(&Some(sha)));
assert!(
results.get("nonexistent-ref").is_some_and(|v| v.is_none())
|| !results.contains_key("nonexistent-ref")
);
Ok(())
}
#[tokio::test]
async fn test_resolve_refs_batch_performance_many_refs() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
git.config_user()?;
std::fs::write(repo_path.join("file.txt"), "content")?;
git.add_all()?;
git.commit("Initial commit")?;
for i in 0..20 {
git.tag(&format!("v1.{}.0", i))?;
}
let repo = GitRepo::new(repo_path);
let refs: Vec<String> = (0..20).map(|i| format!("v1.{}.0", i)).collect();
let ref_strs: Vec<&str> = refs.iter().map(|s| s.as_str()).collect();
let start = std::time::Instant::now();
let results = repo.resolve_refs_batch(&ref_strs).await?;
let elapsed = start.elapsed();
assert_eq!(results.len(), 20);
for ref_name in &refs {
assert!(
results.get(ref_name).is_some_and(|v| v.is_some()),
"Ref {} should be resolved",
ref_name
);
}
assert!(
elapsed.as_secs() < 5,
"Batch resolution of 20 refs took {:?}, expected < 5s",
elapsed
);
Ok(())
}
#[tokio::test]
async fn test_execute_with_stdin_basic() -> Result<()> {
use crate::git::command_builder::GitCommand;
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let git = TestGit::new(repo_path);
git.init()?;
let output = GitCommand::new()
.args(["hash-object", "--stdin"])
.current_dir(repo_path)
.execute_with_stdin("test content")
.await?;
let sha = output.stdout.trim();
assert_eq!(sha.len(), 40, "Expected 40-char SHA, got: {}", sha);
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()), "Expected hex SHA, got: {}", sha);
let output2 = GitCommand::new()
.args(["hash-object", "--stdin"])
.current_dir(repo_path)
.execute_with_stdin("test content")
.await?;
assert_eq!(output.stdout.trim(), output2.stdout.trim());
Ok(())
}
}