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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]
crate-type = ["rlib", "cdylib"]

[dependencies]
git2 = { version = "0.21", features = [
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/rust/RUST_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.91.1
1.97.1
100 changes: 57 additions & 43 deletions app/src/main/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<git2::Error> for Error {
Expand All @@ -35,6 +42,12 @@ impl From<git2::Error> for Error {
}
}

impl From<jni::errors::Error> for Error {
fn from(value: jni::errors::Error) -> Self {
Self::jni(value, "")
}
}

impl Error {
fn git2(error: git2::Error, msg: &str) -> Self {
Self::Git2 {
Expand All @@ -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),
},
}
}
}
Expand All @@ -57,16 +81,16 @@ impl From<Error> for jint {
fn from(value: Error) -> Self {
match value {
Error::Git2 { error, .. } => error.raw_code(),
Error::Jni { .. } => -1,
}
}
}

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}"),
}
}
}
Expand Down Expand Up @@ -513,54 +537,44 @@ fn get_timestamps_lib<'local>(
_class: JClass<'local>,
j_map: JObject<'local>,
) -> Result<jint, jni::errors::Error> {
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<Item = (&'a String, &'a i64)>,
) -> Result<(), Box<dyn std::error::Error>> {
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!("<init>"), 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>(
Expand Down
105 changes: 39 additions & 66 deletions app/src/main/rust/src/libgit2/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
collections::HashMap,
fs,
path::Path,
str::FromStr,
Expand All @@ -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)]
Expand Down Expand Up @@ -376,77 +374,52 @@ pub fn is_change() -> Result<bool, Error> {
Ok(count > 0)
}

fn find_timestamp(repo: &Repository, file_path: String) -> anyhow::Result<Option<(String, i64)>> {
// 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(&current_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<HashMap<String, i64>, 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(&current_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(())
}
33 changes: 29 additions & 4 deletions app/src/main/rust/src/libgit2/test.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
use std::{collections::HashMap, time::Instant};

use super::*;

#[test]
#[ignore = "local repo"]
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::<Vec<_>>();
get_timestamps(|path, time| {
timestamps.entry(path.to_string()).or_insert(time);

Ok(())
})
.unwrap();

let mut res = timestamps.into_iter().collect::<Vec<_>>();

res.sort_by(|a, b| a.1.cmp(&b.1));

dbg!(&res);

let elapsed = now.elapsed();

println!("{elapsed:?}");
}