use color_eyre::Result;
use git2::Repository;
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
fn create_test_repo(path: &Path) -> Result<Repository> {
let repo = Repository::init(path)?;
let sig = git2::Signature::now("Test User", "test@example.com")?;
let tree_id = {
let mut index = repo.index()?;
index.write_tree()?
};
let tree = repo.find_tree(tree_id)?;
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
drop(tree);
Ok(repo)
}
fn create_test_repo_with_content(
path: &Path,
file_name: &str,
content: &str,
) -> Result<Repository> {
let repo = Repository::init(path)?;
fs::write(path.join(file_name), content)?;
let sig = git2::Signature::now("Test User", "test@example.com")?;
let mut index = repo.index()?;
index.add_path(Path::new(file_name))?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
drop(tree);
Ok(repo)
}
#[test]
fn test_rollback_on_partial_failure() -> Result<()> {
let temp_dir = TempDir::new()?;
let main_repo_path = temp_dir.path().join("main");
fs::create_dir(&main_repo_path)?;
let main_repo = create_test_repo(&main_repo_path)?;
let repo1_path = temp_dir.path().join("repo1");
let repo2_path = temp_dir.path().join("repo2");
fs::create_dir(&repo1_path)?;
fs::create_dir(&repo2_path)?;
create_test_repo(&repo1_path)?;
create_test_repo(&repo2_path)?;
let repos_content = format!(
r#"repositories:
test/repo1:
type: git
url: file://{}
version: main
test/invalid:
type: git
url: file:///nonexistent/repo
version: main
test/repo2:
type: git
url: file://{}
version: main
"#,
repo1_path.display(),
repo2_path.display()
);
let repos_file = main_repo_path.join("test.repos");
fs::write(&repos_file, &repos_content)?;
let initial_submodules = main_repo.submodules()?.len();
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src"])
.output()?;
assert!(!output.status.success());
let final_repo = Repository::open(&main_repo_path)?;
let final_submodules = final_repo.submodules()?.len();
assert_eq!(
initial_submodules, final_submodules,
"Rollback should restore original state"
);
assert!(!main_repo_path.join("src/test/repo1").exists());
assert!(!main_repo_path.join("src/test/invalid").exists());
assert!(!main_repo_path.join("src/test/repo2").exists());
Ok(())
}
#[test]
fn test_rollback_restores_existing_submodule_commits() -> Result<()> {
let temp_dir = TempDir::new()?;
let main_repo_path = temp_dir.path().join("main");
fs::create_dir(&main_repo_path)?;
let main_repo = create_test_repo(&main_repo_path)?;
let sub_repo_path = temp_dir.path().join("sub");
fs::create_dir(&sub_repo_path)?;
let sub_repo = create_test_repo_with_content(&sub_repo_path, "file1.txt", "version 1")?;
fs::write(sub_repo_path.join("file2.txt"), "version 2")?;
let sig = git2::Signature::now("Test User", "test@example.com")?;
let mut index = sub_repo.index()?;
index.add_path(Path::new("file2.txt"))?;
let tree_id = index.write_tree()?;
let tree = sub_repo.find_tree(tree_id)?;
let parent = sub_repo.head()?.peel_to_commit()?;
let second_commit =
sub_repo.commit(Some("HEAD"), &sig, &sig, "Second commit", &tree, &[&parent])?;
let first_commit = parent.id();
let repos_content_v1 = format!(
r#"repositories:
test/sub:
type: git
url: file://{}
version: {}
"#,
sub_repo_path.display(),
first_commit
);
let repos_file = main_repo_path.join("test.repos");
fs::write(&repos_file, &repos_content_v1)?;
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src"])
.output()?;
assert!(output.status.success());
let sig = git2::Signature::now("Test User", "test@example.com")?;
let mut index = main_repo.index()?;
index.write_tree()?;
let tree_id = index.write_tree()?;
let tree = main_repo.find_tree(tree_id)?;
let parent = main_repo.head()?.peel_to_commit()?;
main_repo.commit(Some("HEAD"), &sig, &sig, "Add submodule", &tree, &[&parent])?;
let submodule = main_repo.find_submodule("src/test/sub")?;
assert_eq!(submodule.workdir_id().unwrap(), first_commit);
let repos_content_v2 = format!(
r#"repositories:
test/sub:
type: git
url: file://{}
version: {}
test/invalid:
type: git
url: file:///nonexistent/repo
version: main
"#,
sub_repo_path.display(),
second_commit
);
fs::write(&repos_file, &repos_content_v2)?;
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src"])
.output()?;
assert!(!output.status.success());
let main_repo = Repository::open(&main_repo_path)?;
let submodule = main_repo.find_submodule("src/test/sub")?;
assert_eq!(
submodule.workdir_id().unwrap(),
first_commit,
"Submodule should be restored to original commit"
);
Ok(())
}
#[test]
fn test_rollback_with_sync_selection() -> Result<()> {
let temp_dir = TempDir::new()?;
let main_repo_path = temp_dir.path().join("main");
fs::create_dir(&main_repo_path)?;
let main_repo = create_test_repo(&main_repo_path)?;
let repo1_path = temp_dir.path().join("repo1");
let repo2_path = temp_dir.path().join("repo2");
fs::create_dir(&repo1_path)?;
fs::create_dir(&repo2_path)?;
create_test_repo(&repo1_path)?;
create_test_repo(&repo2_path)?;
let repos_content = format!(
r#"repositories:
test/repo1:
type: git
url: file://{}
version: main
test/repo2:
type: git
url: file://{}
version: main
"#,
repo1_path.display(),
repo2_path.display()
);
let repos_file = main_repo_path.join("test.repos");
fs::write(&repos_file, &repos_content)?;
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src"])
.output()?;
assert!(output.status.success());
let sig = git2::Signature::now("Test User", "test@example.com")?;
let mut index = main_repo.index()?;
let tree_id = index.write_tree()?;
let tree = main_repo.find_tree(tree_id)?;
let parent = main_repo.head()?.peel_to_commit()?;
main_repo.commit(
Some("HEAD"),
&sig,
&sig,
"Add submodules",
&tree,
&[&parent],
)?;
let repos_content_invalid = format!(
r#"repositories:
test/repo1:
type: git
url: file://{}
version: main
test/invalid:
type: git
url: file:///nonexistent/repo
version: main
"#,
repo1_path.display()
);
fs::write(&repos_file, &repos_content_invalid)?;
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src", "--sync-selection"])
.output()?;
assert!(!output.status.success());
assert!(main_repo_path.join("src/test/repo1").exists());
assert!(main_repo_path.join("src/test/repo2").exists());
Ok(())
}
#[test]
fn test_dry_run_rollback_not_triggered() -> Result<()> {
let temp_dir = TempDir::new()?;
let main_repo_path = temp_dir.path().join("main");
fs::create_dir(&main_repo_path)?;
let _main_repo = create_test_repo(&main_repo_path)?;
let repos_content = r#"repositories:
test/invalid:
type: git
url: file:///nonexistent/repo
version: main
"#;
let repos_file = main_repo_path.join("test.repos");
fs::write(&repos_file, repos_content)?;
let output = Command::new(env!("CARGO_BIN_EXE_vcs2git"))
.current_dir(&main_repo_path)
.args([repos_file.to_str().unwrap(), "src", "--dry-run"])
.output()?;
assert!(output.status.success());
assert!(!main_repo_path.join(".gitmodules").exists());
Ok(())
}