diff --git a/CHANGELOG.md b/CHANGELOG.md index db5220cf..7d22e4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Changed + +- Improve timestamp algorithme. O(n \* m) to O(n) + +## [26.08] + ## [26.05.1] ### Fixed diff --git a/app/src/main/rust/Cargo.toml b/app/src/main/rust/Cargo.toml index e04a2ee6..0b187f5f 100644 --- a/app/src/main/rust/Cargo.toml +++ b/app/src/main/rust/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [lib] -crate-type = ["cdylib"] +crate-type = ["rlib", "cdylib"] [dependencies] git2 = { version = "0.21", features = [ diff --git a/app/src/main/rust/RUST_VERSION b/app/src/main/rust/RUST_VERSION index 6506fb3d..0c818f53 100644 --- a/app/src/main/rust/RUST_VERSION +++ b/app/src/main/rust/RUST_VERSION @@ -1 +1 @@ -1.91.1 \ No newline at end of file +1.97.1 \ 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 f4d79cce..5522d2ee 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -26,7 +26,14 @@ const OK: jint = 0; #[derive(Debug)] enum Error { - Git2 { error: git2::Error, msg: String }, + Git2 { + error: git2::Error, + msg: String, + }, + Jni { + error: jni::errors::Error, + msg: String, + }, } impl From for Error { @@ -35,6 +42,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: jni::errors::Error) -> Self { + Self::jni(value, "") + } +} + impl Error { fn git2(error: git2::Error, msg: &str) -> Self { Self::Git2 { @@ -43,12 +56,23 @@ impl Error { } } + fn jni(error: jni::errors::Error, msg: &str) -> Self { + Self::Jni { + error, + msg: msg.into(), + } + } + fn add_message(self, msg1: &str) -> Self { match self { Error::Git2 { error, msg } => Error::Git2 { error, msg: format!("{}: {}", msg1, msg), }, + Error::Jni { error, msg } => Error::Jni { + error, + msg: format!("{}: {}", msg1, msg), + }, } } } @@ -57,6 +81,7 @@ impl From for jint { fn from(value: Error) -> Self { match value { Error::Git2 { error, .. } => error.raw_code(), + Error::Jni { .. } => -1, } } } @@ -64,9 +89,8 @@ impl From for jint { impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Error::Git2 { error, msg } => { - write!(f, "{msg}: {error}") - } + Error::Git2 { error, msg } => write!(f, "{msg}: {error}"), + Error::Jni { error, msg } => write!(f, "{msg}: {error}"), } } } @@ -513,54 +537,44 @@ fn get_timestamps_lib<'local>( _class: JClass<'local>, j_map: JObject<'local>, ) -> Result { - let timestamps = unwrap_or_log!(libgit2::get_timestamps(), "get_timestamps"); - - if let Err(e) = get_timestamps_jni(env, &j_map, timestamps.iter()) { - error!("get_timestamps_jni: {e}"); - return Ok(-1); - } - - Ok(OK) -} - -fn get_timestamps_jni<'local, 'a>( - env: &mut Env<'local>, - j_map: &JObject<'local>, - timestamps: impl Iterator, -) -> Result<(), Box> { - let map_class = env.get_object_class(j_map)?; + let map_class = env.get_object_class(&j_map)?; let put_method = env.get_method_id( map_class, - jni_str!("put"), + jni_str!("putIfAbsent"), jni_sig!((JObject, JObject) -> JObject), )?; let long_class = env.find_class(jni_str!("java/lang/Long"))?; let long_ctor = env.get_method_id(&long_class, jni_str!(""), jni_sig!((jlong)))?; - for (path, timestamp) in timestamps { - let j_key: JString = env.new_string(path)?; - - unsafe { - let j_value = env.new_object_unchecked( - &long_class, - long_ctor, - &[JValue::Long(*timestamp).as_jni()], - )?; - - env.call_method_unchecked( - j_map, - put_method, - jni::signature::ReturnType::Object, - &[ - JValue::Object(&JObject::from(j_key)).as_jni(), - JValue::Object(&j_value).as_jni(), - ], - )?; - } - } + unwrap_or_log!( + libgit2::get_timestamps(|path, timestamp| { + let j_key: JString = env.new_string(path)?; + + unsafe { + let j_value = env.new_object_unchecked( + &long_class, + long_ctor, + &[JValue::Long(timestamp).as_jni()], + )?; + + env.call_method_unchecked( + &j_map, + put_method, + jni::signature::ReturnType::Object, + &[ + JValue::Object(&JObject::from(j_key)).as_jni(), + JValue::Object(&j_value).as_jni(), + ], + )?; + } - Ok(()) + Ok(()) + }), + "get_timestamps" + ); + + Ok(OK) } fn generate_ssh_keys_lib<'local>( diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index e56c97e3..3a06e1fa 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, fs, path::Path, str::FromStr, @@ -8,13 +7,12 @@ use std::{ use git2::{ CertificateCheckStatus, FetchOptions, IndexAddOption, Progress, PushOptions, RemoteCallbacks, - Repository, Signature, StatusOptions, TreeWalkMode, TreeWalkResult, + Repository, Signature, StatusOptions, }; use crate::{Cred, Error, GitAuthor, callback::ProgressCB, mime_types::is_extension_supported}; mod merge; - #[cfg(test)] mod test; #[cfg(test)] @@ -376,77 +374,52 @@ pub fn is_change() -> Result { Ok(count > 0) } -fn find_timestamp(repo: &Repository, file_path: String) -> anyhow::Result> { - // Use revwalk to find the last commit that touched this path +pub fn get_timestamps( + mut insert: impl FnMut(&str, i64) -> Result<(), jni::errors::Error>, +) -> Result<(), Error> { + let repo = REPO.lock().expect("repo lock"); + let repo = repo.as_ref().expect("repo"); + let mut revwalk = repo.revwalk()?; revwalk.push_head()?; revwalk.set_sorting(git2::Sort::TIME)?; - for oid_result in revwalk { - let oid = oid_result?; + for oid in revwalk { + let oid = oid?; let commit = repo.find_commit(oid)?; - // Check if this commit touches the file - if commit - .tree()? - .get_path(std::path::Path::new(&file_path)) - .is_ok() - { - // We want to check if this commit modified the file_path compared to its parent(s) - let parent = commit.parents().next(); - - let is_modified = match parent { - Some(parent) => { - // Compare trees between commit and its first parent - let parent_tree = parent.tree()?; - let current_tree = commit.tree()?; - - let diff = repo.diff_tree_to_tree( - Some(&parent_tree), - Some(¤t_tree), - Some(git2::DiffOptions::new().pathspec(&file_path)), - )?; - - diff.deltas().len() > 0 - } - // Initial commit, consider as modified - None => true, - }; - - if is_modified { - return Ok(Some((file_path, commit.time().seconds() * 1000))); - } - } - } - Ok(None) -} + let current_tree = commit.tree()?; -pub fn get_timestamps() -> Result, Error> { - let repo = REPO.lock().expect("repo lock"); - let repo = repo.as_ref().expect("repo"); - - // Get HEAD commit - let head = repo.head()?.peel_to_commit()?; - - let mut file_timestamps = HashMap::new(); - - // Get the list of files in the repo at HEAD - let tree = head.tree()?; + let parent_tree = if commit.parent_count() > 0 { + Some(commit.parent(0)?.tree()?) + } else { + None + }; - tree.walk(TreeWalkMode::PreOrder, |root, entry| { - if entry.kind() == Some(git2::ObjectType::Blob) - && let Ok(name) = entry.name() - && let Some(extension) = Path::new(name).extension() - && let Some(extension) = extension.to_str() - && is_extension_supported(extension) - { - let path = format!("{root}{name}"); - if let Ok(Some((path, time))) = find_timestamp(repo, path) { - file_timestamps.insert(path, time); - } + let mut opts = git2::DiffOptions::new(); + + let diff = + repo.diff_tree_to_tree(parent_tree.as_ref(), Some(¤t_tree), Some(&mut opts))?; + + for delta in diff.deltas() { + let path = delta.new_file().path().or_else(|| delta.old_file().path()); + + if let Some(path) = path + && is_extension_supported( + Path::new(&path) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""), + ) { + match path.as_os_str().to_str() { + Some(path) => insert(path, commit.time().seconds() * 1000)?, + None => { + warn!("path can't be converted to str"); + } + } + } } - TreeWalkResult::Ok - })?; + } - Ok(file_timestamps) + Ok(()) } diff --git a/app/src/main/rust/src/libgit2/test.rs b/app/src/main/rust/src/libgit2/test.rs index c280560d..578b8e50 100644 --- a/app/src/main/rust/src/libgit2/test.rs +++ b/app/src/main/rust/src/libgit2/test.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, time::Instant}; + use super::*; #[test] @@ -5,21 +7,44 @@ use super::*; fn timestamp() { open_repo("../../../../../repo_test").unwrap(); - let res = get_timestamps(); + let mut timestamps = HashMap::new(); - dbg!(&res); + let now = Instant::now(); + + get_timestamps(|path, time| { + timestamps.insert(path.to_string(), time); + Ok(()) + }) + .unwrap(); + + let elapsed = now.elapsed(); + + println!("{elapsed:?}"); } +// cargo test timestamp2 --release -- --nocapture --ignored #[test] #[ignore = "local repo"] fn timestamp2() { open_repo("../../../../../note-pv").unwrap(); - let res = get_timestamps(); + let mut timestamps = HashMap::new(); + let now = Instant::now(); - let mut res = res.unwrap().into_iter().collect::>(); + get_timestamps(|path, time| { + timestamps.entry(path.to_string()).or_insert(time); + + Ok(()) + }) + .unwrap(); + + let mut res = timestamps.into_iter().collect::>(); res.sort_by(|a, b| a.1.cmp(&b.1)); dbg!(&res); + + let elapsed = now.elapsed(); + + println!("{elapsed:?}"); }