diff --git a/app/src/main/rust/.gitignore b/app/src/main/rust/.gitignore index 1d16177..a1fb3cb 100644 --- a/app/src/main/rust/.gitignore +++ b/app/src/main/rust/.gitignore @@ -1,3 +1,4 @@ target/ /openssl-prebuild/*/install -/vendor \ No newline at end of file +/vendor +/repo_test \ No newline at end of file diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index 8ea3a40..c13e79c 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -1,6 +1,7 @@ use std::fmt::{Debug, Display}; use anyhow::anyhow; +use git2::Signature; use jni::JNIEnv; use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::{jboolean, jint, jobject, jstring}; @@ -147,6 +148,15 @@ pub struct GitAuthor { pub email: String, } +impl<'a> From> for GitAuthor { + fn from(value: Signature<'a>) -> Self { + GitAuthor { + name: value.name().unwrap_or("").to_string(), + email: value.email().unwrap_or("").to_string(), + } + } +} + impl Debug for Cred { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -381,10 +391,7 @@ pub extern "C" fn Java_io_github_wiiznokes_gitnote_manager_GitManagerKt_pullLib< let cred = Cred::from_jni(&mut env, &cred).unwrap(); let name: String = env.get_string(&name).unwrap().into(); let email: String = env.get_string(&email).unwrap().into(); - let author = GitAuthor { - name, - email - }; + let author = GitAuthor { name, email }; unwrap_or_log!(libgit2::pull(cred, &author), "pull"); OK } diff --git a/app/src/main/rust/src/libgit2/merge.rs b/app/src/main/rust/src/libgit2/merge.rs index 92f5359..3d6511d 100644 --- a/app/src/main/rust/src/libgit2/merge.rs +++ b/app/src/main/rust/src/libgit2/merge.rs @@ -28,7 +28,7 @@ fn normal_merge( repo: &Repository, local: &git2::AnnotatedCommit, remote: &git2::AnnotatedCommit, - author: &GitAuthor + author: &GitAuthor, ) -> Result<(), git2::Error> { let local_tree = repo.find_commit(local.id())?.tree()?; let remote_tree = repo.find_commit(remote.id())?.tree()?; @@ -59,7 +59,10 @@ fn normal_merge( &[&local_commit, &remote_commit], )?; // Set working tree to match head. - repo.checkout_head(None)?; + let mut checkout_opts = git2::build::CheckoutBuilder::new(); + checkout_opts.force(); + repo.checkout_head(Some(&mut checkout_opts))?; + Ok(()) } @@ -67,7 +70,7 @@ pub fn do_merge<'a>( repo: &'a Repository, remote_branch: &str, fetch_commit: git2::AnnotatedCommit<'a>, - author: &GitAuthor + author: &GitAuthor, ) -> Result<(), Error> { // 1. do a merge analysis let analysis = repo diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index b5f4a98..358c202 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -18,6 +18,9 @@ mod merge; #[cfg(test)] mod test; +#[cfg(test)] +mod test_merge; + const REMOTE: &str = "origin"; static REPO: LazyLock>> = LazyLock::new(|| Mutex::new(None)); diff --git a/app/src/main/rust/src/libgit2/test_merge.rs b/app/src/main/rust/src/libgit2/test_merge.rs new file mode 100644 index 0000000..770c695 --- /dev/null +++ b/app/src/main/rust/src/libgit2/test_merge.rs @@ -0,0 +1,154 @@ +use git2::{Repository, Signature, build::CheckoutBuilder}; +use std::path::Path; +use std::{fs, io}; + +use crate::GitAuthor; +use crate::libgit2::merge::do_merge; + +fn clear_dir>(path: P) -> io::Result<()> { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + fs::remove_dir_all(path)?; + } else { + fs::remove_file(path)?; + } + } + Ok(()) +} + +fn signature() -> Signature<'static> { + Signature::now("Moi", "test@example.com").unwrap() +} + +fn switch_to_branch(repo: &Repository, branch_name: &str) { + let ref_name = format!("refs/heads/{}", branch_name); + let obj = repo + .revparse_single(&ref_name) + .unwrap() + .peel_to_commit() + .unwrap(); + + let mut opts = CheckoutBuilder::new(); + opts.force(); + + repo.checkout_tree(obj.as_object(), Some(&mut opts)) + .unwrap(); + repo.set_head(&ref_name).unwrap(); +} + +pub fn commit_current_state(repo: &Repository, message: &str) -> git2::Oid { + let sig = signature(); + let mut index = repo.index().unwrap(); + let tree_id = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_id).unwrap(); + + // Récupère le parent actuel (HEAD) + let parent = repo + .head() + .ok() + .and_then(|h| h.target()) + .and_then(|id| repo.find_commit(id).ok()); + + let parents = match &parent { + Some(c) => vec![c], + None => vec![], + }; + + repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents) + .unwrap() +} + +fn add_file(repo: &Repository, filename: &str, content: &str) { + let path = repo.workdir().unwrap().join(filename); + fs::write(path, content).unwrap(); + + let mut index = repo.index().unwrap(); + index.add_path(Path::new(filename)).unwrap(); + index.write().unwrap(); +} + +fn assert_content(repo: &Repository, path: &str, content: &str) { + let path = repo.workdir().unwrap().join(path); + + let real_content = fs::read_to_string(&path).unwrap(); + + assert_eq!(real_content, content); +} + +#[test] +fn test_clean_merge_flow() { + let path = "repo_test/clean_repo"; + let _ = clear_dir(path); + let repo = Repository::init(path).unwrap(); + + // 1. Premier commit sur Master + add_file(&repo, "file1.txt", "hello"); + let oid1 = commit_current_state(&repo, "Initial commit on master"); + + // 2. Créer et passer sur la branche 'dev' + let commit1 = repo.find_commit(oid1).unwrap(); + repo.branch("dev", &commit1, false).unwrap(); + switch_to_branch(&repo, "dev"); + + // 3. Commit sur 'dev' (file2.txt) + add_file(&repo, "file2.txt", "hello"); + commit_current_state(&repo, "Add file2 on dev"); + + // 4. Retour sur 'master' et commit (file3.txt) + switch_to_branch(&repo, "master"); + add_file(&repo, "file1.txt", "hello world"); + commit_current_state(&repo, "Modif file1 on master"); + + // 5. Merge 'dev' dans 'master' + let annotated_dev = { + let dev_ref = repo.find_reference("refs/heads/dev").unwrap(); + repo.reference_to_annotated_commit(&dev_ref).unwrap() + }; + + let author = GitAuthor::from(signature()); + do_merge(&repo, "dev", annotated_dev, &author).expect("Merge failed"); + + assert_content(&repo, "file1.txt", "hello world"); + assert_content(&repo, "file2.txt", "hello"); +} + +#[test] +fn test_clean_merge_flow2() { + let path = "repo_test/clean_repo2"; + let _ = clear_dir(path); + let repo = Repository::init(path).unwrap(); + + // 1. Premier commit sur Master + add_file(&repo, "file1.txt", "Contenu Initial"); + let oid1 = commit_current_state(&repo, "Initial commit on master"); + + // 2. Créer et passer sur la branche 'dev' + let commit1 = repo.find_commit(oid1).unwrap(); + repo.branch("dev", &commit1, false).unwrap(); + switch_to_branch(&repo, "dev"); + + // 3. Commit sur 'dev' (file2.txt) + add_file(&repo, "file2.txt", "Contenu Dev"); + commit_current_state(&repo, "Add file2 on dev"); + + // 4. Retour sur 'master' et commit (file3.txt) + switch_to_branch(&repo, "master"); + add_file(&repo, "file3.txt", "Contenu Master"); + commit_current_state(&repo, "Add file3 on master"); + + // 5. Merge 'dev' dans 'master' + let annotated_dev = { + let dev_ref = repo.find_reference("refs/heads/dev").unwrap(); + repo.reference_to_annotated_commit(&dev_ref).unwrap() + }; + + let author = GitAuthor::from(signature()); + do_merge(&repo, "dev", annotated_dev, &author).expect("Merge failed"); + + assert_content(&repo, "file1.txt", "Contenu Initial"); + assert_content(&repo, "file2.txt", "Contenu Dev"); + assert_content(&repo, "file3.txt", "Contenu Master"); +}