//! Auto-merge against real git repositories. //! //! The rule is measured from the diff, so it has to be tested against real //! diffs — a unit test on the classifier alone would not catch a wrong //! revision range. use cm_api::auto_merge::{self, MergePolicy}; fn git(repo: &std::path::Path, args: &[&str]) { let out = std::process::Command::new("git") .arg("-C") .arg(repo) .args(args) .env("GIT_AUTHOR_NAME", "T") .env("GIT_AUTHOR_EMAIL", "t@x.local") .env("GIT_COMMITTER_NAME", "T") .env("GIT_COMMITTER_EMAIL", "t@x.local") .output() .unwrap(); assert!( out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr) ); } /// Returns (work checkout, bare remote path). fn seed() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) { let tmp = tempfile::tempdir().unwrap(); let remote = tmp.path().join("remote.git"); let work = tmp.path().join("work"); std::process::Command::new("git") .args(["init", "--quiet", "--bare"]) .arg(&remote) .output() .unwrap(); std::fs::create_dir_all(&work).unwrap(); git(&work, &["init", "--quiet"]); git(&work, &["checkout", "-q", "-B", "main"]); std::fs::write(work.join("README.md"), "# vault\n").unwrap(); git(&work, &["add", "."]); git(&work, &["commit", "--quiet", "-m", "base"]); git(&work, &["remote", "add", "origin", remote.to_str().unwrap()]); git(&work, &["push", "--quiet", "origin", "main"]); (tmp, work, remote) } #[tokio::test] async fn a_purely_additive_branch_is_merged() { let (_tmp, work, remote) = seed(); git(&work, &["checkout", "-q", "-B", "lib/add"]); std::fs::create_dir_all(work.join("60 Papers")).unwrap(); std::fs::write(work.join("60 Papers/a.md"), "# paper\n").unwrap(); git(&work, &["add", "."]); git(&work, &["commit", "--quiet", "-m", "add paper"]); git(&work, &["push", "--quiet", "origin", "lib/add"]); let out = auto_merge::try_merge( &work, remote.to_str().unwrap(), "lib/add", "main", MergePolicy::AdditiveOnly, true, ) .await .unwrap(); assert!(out.merged, "should have merged: {}", out.reason); // The note must really be on main at the remote, not just locally. let ls = std::process::Command::new("git") .arg("-C").arg(&remote) .args(["ls-tree", "--name-only", "-r", "main"]) .output() .unwrap(); let listed = String::from_utf8_lossy(&ls.stdout); assert!(listed.contains("60 Papers/a.md"), "remote main: {listed}"); } /// The load-bearing refusal: a branch that rewrites an existing file must be /// left for a human even though its mission type is allowed to auto-merge. #[tokio::test] async fn a_branch_that_modifies_an_existing_file_is_refused() { let (_tmp, work, remote) = seed(); git(&work, &["checkout", "-q", "-B", "lib/bad"]); std::fs::create_dir_all(work.join("60 Papers")).unwrap(); std::fs::write(work.join("60 Papers/a.md"), "# paper\n").unwrap(); // …and clobbers a hand-written file. std::fs::write(work.join("README.md"), "# REWRITTEN BY A MACHINE\n").unwrap(); git(&work, &["add", "."]); git(&work, &["commit", "--quiet", "-m", "add + clobber"]); git(&work, &["push", "--quiet", "origin", "lib/bad"]); let out = auto_merge::try_merge( &work, remote.to_str().unwrap(), "lib/bad", "main", MergePolicy::AdditiveOnly, true, ) .await .unwrap(); assert!(!out.merged, "must refuse a non-additive branch"); assert!(out.reason.contains("not additive"), "reason: {}", out.reason); let show = std::process::Command::new("git") .arg("-C").arg(&remote) .args(["show", "main:README.md"]) .output() .unwrap(); assert_eq!( String::from_utf8_lossy(&show.stdout), "# vault\n", "the hand-written file must be untouched on main" ); } #[tokio::test] async fn unverified_work_is_never_merged() { let (_tmp, work, remote) = seed(); git(&work, &["checkout", "-q", "-B", "lib/unverified"]); std::fs::create_dir_all(work.join("60 Papers")).unwrap(); std::fs::write(work.join("60 Papers/a.md"), "# paper\n").unwrap(); git(&work, &["add", "."]); git(&work, &["commit", "--quiet", "-m", "add"]); git(&work, &["push", "--quiet", "origin", "lib/unverified"]); let out = auto_merge::try_merge( &work, remote.to_str().unwrap(), "lib/unverified", "main", MergePolicy::AdditiveOnly, false, ) .await .unwrap(); assert!(!out.merged); assert!(out.reason.contains("did not verify"), "reason: {}", out.reason); } #[tokio::test] async fn a_never_policy_branch_is_left_alone() { let (_tmp, work, remote) = seed(); git(&work, &["checkout", "-q", "-B", "code/change"]); std::fs::write(work.join("new.rs"), "fn main() {}\n").unwrap(); git(&work, &["add", "."]); git(&work, &["commit", "--quiet", "-m", "code"]); git(&work, &["push", "--quiet", "origin", "code/change"]); let out = auto_merge::try_merge( &work, remote.to_str().unwrap(), "code/change", "main", MergePolicy::Never, true, ) .await .unwrap(); assert!(!out.merged, "code must never auto-merge"); }