use gitcomet_core::merge::{MergeOptions, merge_file}; use gitcomet_core::merge_extraction::{ MergeExtractionOptions, discover_merge_commits, extract_merge_cases_from_repo, }; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; fn resource_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("libgit2") .join("resources") } fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); let file_type = entry.file_type()?; if file_type.is_dir() { copy_dir_recursive(&src_path, &dst_path)?; } else { fs::copy(&src_path, &dst_path)?; } } Ok(()) } struct RehydratedRepo { _tempdir: tempfile::TempDir, worktree_path: PathBuf, } fn rehydrate_worktree_repo(name: &str) -> RehydratedRepo { let src = resource_root().join(name); assert!(src.is_dir(), "missing resource repo: {}", src.display()); assert!( src.join(".gitted").is_dir(), "expected .gitted dir in {}", src.display() ); let tempdir = tempfile::tempdir().expect("create temp dir"); let worktree_path = tempdir.path().join(name); copy_dir_recursive(&src, &worktree_path).expect("copy fixture repo"); let gitted = worktree_path.join(".gitted"); let git_dir = worktree_path.join(".git"); fs::rename(&gitted, &git_dir).expect("rehydrate .gitted -> .git"); RehydratedRepo { _tempdir: tempdir, worktree_path, } } fn run_git_in_worktree(repo: &Path, args: &[&str]) -> String { let output = Command::new("git") .args(args) .current_dir(repo) .output() .unwrap_or_else(|e| panic!("failed to run git {:?}: {e}", args)); assert!( output.status.success(), "git {:?} failed: {}", args, String::from_utf8_lossy(&output.stderr) ); String::from_utf8_lossy(&output.stdout).into_owned() } fn run_git_with_git_dir(git_dir: &Path, args: &[&str]) -> String { let output = Command::new("git") .arg(format!("--git-dir={}", git_dir.display())) .args(args) .output() .unwrap_or_else(|e| panic!("failed to run git {:?}: {e}", args)); assert!( output.status.success(), "git {:?} failed: {}", args, String::from_utf8_lossy(&output.stderr) ); String::from_utf8_lossy(&output.stdout).into_owned() } #[test] fn libgit2_merge_resource_layout_matches_upstream_style() { let root = resource_root(); for name in [ "merge-recursive", "merge-resolve", "merge-whitespace", "mergedrepo", ] { let repo = root.join(name); assert!(repo.is_dir(), "missing {}", repo.display()); assert!( repo.join(".gitted").is_dir(), "missing .gitted in {}", repo.display() ); assert!( repo.join(".gitted").join("HEAD").is_file(), "missing .gitted/HEAD in {}", repo.display() ); } for name in ["redundant.git", "twowaymerge.git"] { let repo = root.join(name); assert!(repo.is_dir(), "missing {}", repo.display()); assert!( repo.join("HEAD").is_file(), "missing HEAD in {}", repo.display() ); let is_bare = run_git_with_git_dir(&repo, &["rev-parse", "--is-bare-repository"]); assert_eq!(is_bare.trim(), "true", "{} should be bare", repo.display()); } } #[test] fn merge_recursive_rehydration_extracts_real_text_cases() { let repo = rehydrate_worktree_repo("merge-recursive"); let git_dir = run_git_in_worktree(&repo.worktree_path, &["rev-parse", "--git-dir"]); assert_eq!(git_dir.trim(), ".git"); let merges = discover_merge_commits(&repo.worktree_path, 20).expect("discover merges"); assert!( !merges.is_empty(), "expected merge commits in merge-recursive" ); let cases = extract_merge_cases_from_repo( &repo.worktree_path, MergeExtractionOptions { max_merges: 20, max_files_per_merge: 20, }, ) .expect("extract merge cases"); assert!(!cases.is_empty(), "expected extracted merge cases"); assert!( cases.iter().all(|case| { !case.base.contains("anonymous blob ") && !case.contrib1.contains("anonymous blob ") && !case.contrib2.contains("anonymous blob ") }), "merge-recursive should yield real text payloads" ); assert!( cases.iter().any(|case| { case.base.contains("VEAL SOUP.") || case.contrib1.contains("VEAL SOUP.") || case.contrib2.contains("VEAL SOUP.") }), "expected known merge-recursive corpus text in extracted cases" ); for case in cases.iter().take(12) { let result = merge_file( &case.base, &case.contrib1, &case.contrib2, &MergeOptions::default(), ); assert!( !result.output.is_empty(), "merge output should not be empty for case {}:{}", case.merge_commit, case.file_path ); } } #[test] fn rehydrated_worktree_fixtures_match_libgit2_payloads() { let merge_resolve = rehydrate_worktree_repo("merge-resolve"); assert_eq!( run_git_in_worktree(&merge_resolve.worktree_path, &["rev-parse", "--git-dir"]).trim(), ".git" ); let conflicting = fs::read_to_string(merge_resolve.worktree_path.join("conflicting.txt")).expect("read"); assert!(conflicting.contains("changed in master and branch")); let merge_whitespace = rehydrate_worktree_repo("merge-whitespace"); let whitespace_text = fs::read_to_string(merge_whitespace.worktree_path.join("test.txt")).expect("read"); assert!( whitespace_text .lines() .next() .is_some_and(|line| line == "0") ); let mergedrepo = rehydrate_worktree_repo("mergedrepo"); let conflicts_one = fs::read_to_string(mergedrepo.worktree_path.join("conflicts-one.txt")).expect("read"); assert!(conflicts_one.contains("<<<<<<< HEAD")); assert!(conflicts_one.contains(">>>>>>>")); }