Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target/
/openssl-prebuild/*/install
/vendor
/vendor
/repo_test
15 changes: 11 additions & 4 deletions app/src/main/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -147,6 +148,15 @@ pub struct GitAuthor {
pub email: String,
}

impl<'a> From<Signature<'a>> 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 {
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/rust/src/libgit2/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down Expand Up @@ -59,15 +59,18 @@ 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(())
}

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
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/rust/src/libgit2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ mod merge;
#[cfg(test)]
mod test;

#[cfg(test)]
mod test_merge;

const REMOTE: &str = "origin";

static REPO: LazyLock<Mutex<Option<Repository>>> = LazyLock::new(|| Mutex::new(None));
Expand Down
154 changes: 154 additions & 0 deletions app/src/main/rust/src/libgit2/test_merge.rs
Original file line number Diff line number Diff line change
@@ -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<P: AsRef<Path>>(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");
}
Loading