This commit is contained in:
extrawurst 2023-09-14 12:39:19 +02:00
parent 06dfe42f79
commit bf43a16bdf
11 changed files with 534 additions and 215 deletions

View file

@ -7,6 +7,7 @@ use crossbeam_channel::Sender;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
/// Passed to `AsyncJob::run` allowing sending intermediate progress notifications /// Passed to `AsyncJob::run` allowing sending intermediate progress notifications
#[derive(Clone)]
pub struct RunParams< pub struct RunParams<
T: Copy + Send, T: Copy + Send,
P: Clone + Send + Sync + PartialEq, P: Clone + Send + Sync + PartialEq,
@ -37,6 +38,11 @@ impl<T: Copy + Send, P: Clone + Send + Sync + PartialEq>
true true
}) })
} }
///
pub fn progress(&self) -> P {
self.progress.read().cl
}
} }
/// trait that defines an async task we can run on a threadpool /// trait that defines an async task we can run on a threadpool

View file

@ -0,0 +1,300 @@
use git2::Repository;
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::{
self,
commit_files::{
commit_contains_file, commit_detect_file_rename,
},
CommitId, CommitInfo, LogWalker, RepoPath,
SharedCommitFilterFn,
},
AsyncGitNotification,
};
use std::{
sync::{Arc, Mutex, RwLock},
time::{Duration, Instant},
};
///
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FileHistoryEntryDelta {
///
None,
///
Added,
///
Deleted,
///
Modified,
///
Renamed,
///
Copied,
///
Typechange,
}
impl From<git2::Delta> for FileHistoryEntryDelta {
fn from(value: git2::Delta) -> Self {
match value {
git2::Delta::Unmodified
| git2::Delta::Ignored
| git2::Delta::Unreadable
| git2::Delta::Conflicted
| git2::Delta::Untracked => FileHistoryEntryDelta::None,
git2::Delta::Added => FileHistoryEntryDelta::Added,
git2::Delta::Deleted => FileHistoryEntryDelta::Deleted,
git2::Delta::Modified => FileHistoryEntryDelta::Modified,
git2::Delta::Renamed => FileHistoryEntryDelta::Renamed,
git2::Delta::Copied => FileHistoryEntryDelta::Copied,
git2::Delta::Typechange => {
FileHistoryEntryDelta::Typechange
}
}
}
}
///
#[derive(Debug, Clone, PartialEq)]
pub struct FileHistoryEntry {
///
pub commit: CommitId,
///
pub delta: FileHistoryEntryDelta,
//TODO: arc and share since most will be the same over the history
///
pub file_path: String,
///
pub info: CommitInfo,
}
///
pub struct CommitFilterResult {
///
pub result: Vec<FileHistoryEntry>,
pub duration: Duration,
}
enum JobState {
Request {
file_path: String,
repo_path: RepoPath,
},
Response(Result<CommitFilterResult>),
}
#[derive(Clone, Default)]
pub struct AsyncFileHistoryResults(Arc<Mutex<Vec<FileHistoryEntry>>>);
impl PartialEq for AsyncFileHistoryResults {
fn eq(&self, other: &Self) -> bool {
if let Ok(left) = self.0.lock() {
if let Ok(right) = other.0.lock() {
return *left == *right;
}
}
false
}
}
impl AsyncFileHistoryResults {
///
pub fn extract_results(&self) -> Result<Vec<FileHistoryEntry>> {
let mut results = self.0.lock()?;
let results =
std::mem::replace(&mut *results, Vec::with_capacity(1));
Ok(results)
}
}
///
#[derive(Clone)]
pub struct AsyncFileHistoryJob {
state: Arc<Mutex<Option<JobState>>>,
results: AsyncFileHistoryResults,
}
///
impl AsyncFileHistoryJob {
///
pub fn new(repo_path: RepoPath, file_path: String) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request {
repo_path,
file_path,
}))),
results: AsyncFileHistoryResults::default(),
}
}
///
pub fn result(&self) -> Option<Result<CommitFilterResult>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request { .. } => None,
JobState::Response(result) => Some(result),
};
}
}
None
}
///
pub fn extract_results(&self) -> Result<Vec<FileHistoryEntry>> {
self.results.extract_results()
}
fn file_history_filter(
file_path: Arc<RwLock<String>>,
results: Arc<Mutex<Vec<FileHistoryEntry>>>,
params: &RunParams<
AsyncGitNotification,
AsyncFileHistoryResults,
>,
) -> SharedCommitFilterFn {
let params = params.clone();
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let file_path = file_path.clone();
let results = results.clone();
if fun_name(file_path, results, repo, commit_id)? {
params.send(AsyncGitNotification::FileHistory)?;
Ok(true)
} else {
Ok(false)
}
},
))
}
fn run_request(
&self,
repo_path: &RepoPath,
file_path: String,
params: &RunParams<
AsyncGitNotification,
AsyncFileHistoryResults,
>,
) -> Result<CommitFilterResult> {
let start = Instant::now();
let file_name = Arc::new(RwLock::new(file_path));
let result = params.
let filter = Self::file_history_filter(
file_name,
result.clone(),
params,
);
let repo = sync::repo(repo_path)?;
let mut walker =
LogWalker::new(&repo, None)?.filter(Some(filter));
walker.read(None)?;
let result =
std::mem::replace(&mut *result.lock()?, Vec::new());
let result = CommitFilterResult {
duration: start.elapsed(),
result,
};
Ok(result)
}
}
fn fun_name(
file_path: Arc<RwLock<String>>,
results: Arc<Mutex<Vec<FileHistoryEntry>>>,
repo: &Repository,
commit_id: &CommitId,
) -> Result<bool> {
let current_file_path = file_path.read()?.to_string();
if let Some(delta) = commit_contains_file(
repo,
*commit_id,
current_file_path.as_str(),
)? {
log::info!(
"[history] edit: [{}] ({:?}) - {}",
commit_id.get_short_string(),
delta,
&current_file_path
);
let commit_info =
sync::get_commit_info_repo(repo, commit_id)?;
let entry = FileHistoryEntry {
commit: *commit_id,
delta: delta.clone().into(),
info: commit_info,
file_path: current_file_path.clone(),
};
//note: only do rename test in case file looks like being added in this commit
if matches!(delta, git2::Delta::Added) {
let rename = commit_detect_file_rename(
repo,
*commit_id,
current_file_path.as_str(),
)?;
if let Some(old_name) = rename {
// log::info!(
// "rename: [{}] {:?} <- {:?}",
// commit_id.get_short_string(),
// current_file_path,
// old_name,
// );
(*file_path.write()?) = old_name;
}
}
results.lock()?.push(entry);
return Ok(true);
}
Ok(false)
}
impl AsyncJob for AsyncFileHistoryJob {
type Notification = AsyncGitNotification;
type Progress = AsyncFileHistoryResults;
fn run(
&mut self,
params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request {
file_path,
repo_path,
} => JobState::Response(
self.run_request(&repo_path, file_path, &params),
),
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::FileHistory)
}
}

View file

@ -38,6 +38,7 @@ mod commit_files;
mod diff; mod diff;
mod error; mod error;
mod fetch_job; mod fetch_job;
mod file_history;
mod filter_commits; mod filter_commits;
mod progress; mod progress;
mod pull; mod pull;
@ -58,6 +59,9 @@ pub use crate::{
diff::{AsyncDiff, DiffParams, DiffType}, diff::{AsyncDiff, DiffParams, DiffType},
error::{Error, Result}, error::{Error, Result},
fetch_job::AsyncFetchJob, fetch_job::AsyncFetchJob,
file_history::{
AsyncFileHistoryJob, FileHistoryEntry, FileHistoryEntryDelta,
},
filter_commits::{AsyncCommitFilterJob, CommitFilterResult}, filter_commits::{AsyncCommitFilterJob, CommitFilterResult},
progress::ProgressPercent, progress::ProgressPercent,
pull::{AsyncPull, FetchRequest}, pull::{AsyncPull, FetchRequest},
@ -115,6 +119,8 @@ pub enum AsyncGitNotification {
TreeFiles, TreeFiles,
/// ///
CommitFilter, CommitFilter,
///
FileHistory,
} }
/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait /// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait

View file

@ -8,6 +8,7 @@ use crate::{
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use scopetime::scope_time; use scopetime::scope_time;
use std::{ use std::{
cell::RefCell,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, Arc, Mutex,
@ -201,17 +202,17 @@ impl AsyncLog {
) -> Result<()> { ) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let mut entries = Vec::with_capacity(LIMIT_COUNT); let entries = RefCell::new(Vec::with_capacity(LIMIT_COUNT));
let r = repo(repo_path)?; let r = repo(repo_path)?;
let mut walker = let mut walker =
LogWalker::new(&r, LIMIT_COUNT)?.filter(filter); LogWalker::new(&r, Some(LIMIT_COUNT))?.filter(filter);
loop { loop {
entries.clear(); entries.borrow_mut().clear();
let read = walker.read(&mut entries)?; let read = walker.read(Some(&entries))?;
let mut current = arc_current.lock()?; let mut current = arc_current.lock()?;
current.commits.extend(entries.iter()); current.commits.extend(entries.borrow().iter());
current.duration = start_time.elapsed(); current.duration = start_time.elapsed();
if read == 0 { if read == 0 {

View file

@ -134,13 +134,14 @@ mod tests {
}; };
use commit::{amend, tag_commit}; use commit::{amend, tag_commit};
use git2::Repository; use git2::Repository;
use std::cell::RefCell;
use std::{fs::File, io::Write, path::Path}; use std::{fs::File, io::Write, path::Path};
fn count_commits(repo: &Repository, max: usize) -> usize { fn count_commits(repo: &Repository, max: usize) -> usize {
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walk = LogWalker::new(repo, max).unwrap(); let mut walk = LogWalker::new(repo, Some(max)).unwrap();
walk.read(&mut items).unwrap(); walk.read(Some(&items)).unwrap();
items.len() items.take().len()
} }
#[test] #[test]

View file

@ -1,71 +1,15 @@
use super::{ use super::{commit_files::get_commit_diff, CommitId};
commit_files::{commit_contains_file, get_commit_diff}, use crate::error::Result;
CommitId,
};
use crate::{
error::Result, sync::commit_files::commit_detect_file_rename,
};
use bitflags::bitflags; use bitflags::bitflags;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use git2::{Diff, Repository}; use git2::{Diff, Repository};
use std::sync::{Arc, RwLock}; use std::sync::Arc;
/// ///
pub type SharedCommitFilterFn = Arc< pub type SharedCommitFilterFn = Arc<
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>, Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
>; >;
///
pub fn diff_contains_file(
file_path: Arc<RwLock<String>>,
) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let current_file_path = file_path.read()?.to_string();
if let Some(delta) = commit_contains_file(
repo,
*commit_id,
current_file_path.as_str(),
)? {
//note: only do rename test in case file looks like being added in this commit
// log::info!(
// "edit: [{}] ({:?}) - {}",
// commit_id.get_short_string(),
// delta,
// &current_file_path
// );
if matches!(delta, git2::Delta::Added) {
let rename = commit_detect_file_rename(
repo,
*commit_id,
current_file_path.as_str(),
)?;
if let Some(old_name) = rename {
// log::info!(
// "rename: [{}] {:?} <- {:?}",
// commit_id.get_short_string(),
// current_file_path,
// old_name,
// );
(*file_path.write()?) = old_name;
}
}
return Ok(true);
}
Ok(false)
},
))
}
bitflags! { bitflags! {
/// ///
pub struct SearchFields: u32 { pub struct SearchFields: u32 {

View file

@ -1,6 +1,6 @@
use super::RepoPath; use super::RepoPath;
use crate::{error::Result, sync::repository::repo}; use crate::{error::Result, sync::repository::repo};
use git2::{Commit, Error, Oid}; use git2::{Commit, Error, Oid, Repository};
use scopetime::scope_time; use scopetime::scope_time;
use unicode_truncate::UnicodeTruncateStr; use unicode_truncate::UnicodeTruncateStr;
@ -65,7 +65,7 @@ impl From<Oid> for CommitId {
} }
/// ///
#[derive(Debug)] #[derive(Debug, Clone, PartialEq)]
pub struct CommitInfo { pub struct CommitInfo {
/// ///
pub message: String, pub message: String,
@ -121,6 +121,14 @@ pub fn get_commit_info(
let repo = repo(repo_path)?; let repo = repo(repo_path)?;
get_commit_info_repo(&repo, commit_id)
}
///
pub(crate) fn get_commit_info_repo(
repo: &Repository,
commit_id: &CommitId,
) -> Result<CommitInfo> {
let commit = repo.find_commit((*commit_id).into())?; let commit = repo.find_commit((*commit_id).into())?;
let author = commit.author(); let author = commit.author();

View file

@ -3,6 +3,7 @@ use super::{CommitId, SharedCommitFilterFn};
use crate::error::Result; use crate::error::Result;
use git2::{Commit, Oid, Repository}; use git2::{Commit, Oid, Repository};
use std::{ use std::{
cell::RefCell,
cmp::Ordering, cmp::Ordering,
collections::{BinaryHeap, HashSet}, collections::{BinaryHeap, HashSet},
}; };
@ -33,14 +34,17 @@ impl<'a> Ord for TimeOrderedCommit<'a> {
pub struct LogWalker<'a> { pub struct LogWalker<'a> {
commits: BinaryHeap<TimeOrderedCommit<'a>>, commits: BinaryHeap<TimeOrderedCommit<'a>>,
visited: HashSet<Oid>, visited: HashSet<Oid>,
limit: usize, limit: Option<usize>,
repo: &'a Repository, repo: &'a Repository,
filter: Option<SharedCommitFilterFn>, filter: Option<SharedCommitFilterFn>,
} }
impl<'a> LogWalker<'a> { impl<'a> LogWalker<'a> {
/// ///
pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> { pub fn new(
repo: &'a Repository,
limit: Option<usize>,
) -> Result<Self> {
let c = repo.head()?.peel_to_commit()?; let c = repo.head()?.peel_to_commit()?;
let mut commits = BinaryHeap::with_capacity(10); let mut commits = BinaryHeap::with_capacity(10);
@ -70,7 +74,10 @@ impl<'a> LogWalker<'a> {
} }
/// ///
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> { pub fn read(
&mut self,
out: Option<&RefCell<Vec<CommitId>>>,
) -> Result<usize> {
let mut count = 0_usize; let mut count = 0_usize;
while let Some(c) = self.commits.pop() { while let Some(c) = self.commits.pop() {
@ -87,11 +94,17 @@ impl<'a> LogWalker<'a> {
}; };
if commit_should_be_included { if commit_should_be_included {
out.push(id); if let Some(out) = out {
out.borrow_mut().push(id);
}
} }
count += 1; count += 1;
if count == self.limit { if self
.limit
.map(|limit| limit == count)
.unwrap_or_default()
{
break; break;
} }
} }
@ -112,6 +125,9 @@ impl<'a> LogWalker<'a> {
mod tests { mod tests {
use super::*; use super::*;
use crate::error::Result; use crate::error::Result;
use crate::sync::commit_files::{
commit_contains_file, commit_detect_file_rename,
};
use crate::sync::commit_filter::{SearchFields, SearchOptions}; use crate::sync::commit_filter::{SearchFields, SearchOptions};
use crate::sync::tests::{rename_file, write_commit_file}; use crate::sync::tests::{rename_file, write_commit_file};
use crate::sync::{ use crate::sync::{
@ -119,13 +135,47 @@ mod tests {
tests::repo_init_empty, tests::repo_init_empty,
}; };
use crate::sync::{ use crate::sync::{
diff_contains_file, filter_commit_by_search, stage_add_all, filter_commit_by_search, stage_add_all, LogFilterSearch,
LogFilterSearch, LogFilterSearchOptions, RepoPath, LogFilterSearchOptions, RepoPath,
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::{fs::File, io::Write, path::Path}; use std::{fs::File, io::Write, path::Path};
fn diff_contains_file(
file_path: Arc<RwLock<String>>,
) -> SharedCommitFilterFn {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let current_file_path = file_path.read()?.to_string();
if let Some(delta) = commit_contains_file(
repo,
*commit_id,
current_file_path.as_str(),
)? {
if matches!(delta, git2::Delta::Added) {
let rename = commit_detect_file_rename(
repo,
*commit_id,
current_file_path.as_str(),
)?;
if let Some(old_name) = rename {
(*file_path.write()?) = old_name;
}
}
return Ok(true);
}
Ok(false)
},
))
}
#[test] #[test]
fn test_limit() -> Result<()> { fn test_limit() -> Result<()> {
let file_path = Path::new("foo"); let file_path = Path::new("foo");
@ -141,9 +191,10 @@ mod tests {
stage_add_file(repo_path, file_path).unwrap(); stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap(); let oid2 = commit(repo_path, "commit2").unwrap();
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walk = LogWalker::new(&repo, 1)?; let mut walk = LogWalker::new(&repo, Some(1))?;
walk.read(&mut items).unwrap(); walk.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 1); assert_eq!(items.len(), 1);
assert_eq!(items[0], oid2); assert_eq!(items[0], oid2);
@ -166,9 +217,10 @@ mod tests {
stage_add_file(repo_path, file_path).unwrap(); stage_add_file(repo_path, file_path).unwrap();
let oid2 = commit(repo_path, "commit2").unwrap(); let oid2 = commit(repo_path, "commit2").unwrap();
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walk = LogWalker::new(&repo, 100)?; let mut walk = LogWalker::new(&repo, Some(100))?;
walk.read(&mut items).unwrap(); walk.read(Some(&items)).unwrap();
let items = items.take();
let info = get_commits_info(repo_path, &items, 50).unwrap(); let info = get_commits_info(repo_path, &items, 50).unwrap();
dbg!(&info); dbg!(&info);
@ -176,8 +228,9 @@ mod tests {
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
assert_eq!(items[0], oid2); assert_eq!(items[0], oid2);
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
walk.read(&mut items).unwrap(); walk.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 0); assert_eq!(items.len(), 0);
@ -211,26 +264,29 @@ mod tests {
let file_path = Arc::new(RwLock::new(String::from("baz"))); let file_path = Arc::new(RwLock::new(String::from("baz")));
let diff_contains_baz = diff_contains_file(file_path); let diff_contains_baz = diff_contains_file(file_path);
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walker = LogWalker::new(&repo, 100)? let mut walker = LogWalker::new(&repo, Some(100))?
.filter(Some(diff_contains_baz)); .filter(Some(diff_contains_baz));
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 1); assert_eq!(items.len(), 1);
assert_eq!(items[0], second_commit_id); assert_eq!(items[0], second_commit_id);
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 0); assert_eq!(items.len(), 0);
let file_path = Arc::new(RwLock::new(String::from("bar"))); let file_path = Arc::new(RwLock::new(String::from("bar")));
let diff_contains_bar = diff_contains_file(file_path); let diff_contains_bar = diff_contains_file(file_path);
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walker = LogWalker::new(&repo, 100)? let mut walker = LogWalker::new(&repo, Some(100))?
.filter(Some(diff_contains_bar)); .filter(Some(diff_contains_bar));
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 0); assert_eq!(items.len(), 0);
@ -258,11 +314,12 @@ mod tests {
}), }),
); );
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walker = LogWalker::new(&repo, 100) let mut walker = LogWalker::new(&repo, Some(100))
.unwrap() .unwrap()
.filter(Some(log_filter)); .filter(Some(log_filter));
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 1); assert_eq!(items.len(), 1);
assert_eq!(items[0], second_commit_id); assert_eq!(items[0], second_commit_id);
@ -275,13 +332,13 @@ mod tests {
}), }),
); );
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walker = LogWalker::new(&repo, 100) let mut walker = LogWalker::new(&repo, Some(100))
.unwrap() .unwrap()
.filter(Some(log_filter)); .filter(Some(log_filter));
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
assert_eq!(items.len(), 2); assert_eq!(items.take().len(), 2);
} }
#[test] #[test]
@ -305,11 +362,12 @@ mod tests {
Arc::new(RwLock::new(String::from("bar.txt"))); Arc::new(RwLock::new(String::from("bar.txt")));
let log_filter = diff_contains_file(file_path.clone()); let log_filter = diff_contains_file(file_path.clone());
let mut items = Vec::new(); let items = RefCell::new(Vec::new());
let mut walker = LogWalker::new(&repo, 100) let mut walker = LogWalker::new(&repo, Some(100))
.unwrap() .unwrap()
.filter(Some(log_filter)); .filter(Some(log_filter));
walker.read(&mut items).unwrap(); walker.read(Some(&items)).unwrap();
let items = items.take();
assert_eq!(items.len(), 3); assert_eq!(items.len(), 3);
assert_eq!(items[1], rename_commit); assert_eq!(items[1], rename_commit);

View file

@ -50,11 +50,11 @@ pub use commit_details::{
}; };
pub use commit_files::get_commit_files; pub use commit_files::get_commit_files;
pub use commit_filter::{ pub use commit_filter::{
diff_contains_file, filter_commit_by_search, LogFilterSearch, filter_commit_by_search, LogFilterSearch, LogFilterSearchOptions,
LogFilterSearchOptions, SearchFields, SearchOptions, SearchFields, SearchOptions, SharedCommitFilterFn,
SharedCommitFilterFn,
}; };
pub use commit_revert::{commit_revert, revert_commit, revert_head}; pub use commit_revert::{commit_revert, revert_commit, revert_head};
pub(crate) use commits_info::get_commit_info_repo;
pub use commits_info::{ pub use commits_info::{
get_commit_info, get_commits_info, CommitId, CommitInfo, get_commit_info, get_commits_info, CommitId, CommitInfo,
}; };
@ -118,7 +118,7 @@ mod tests {
}; };
use crate::error::Result; use crate::error::Result;
use git2::Repository; use git2::Repository;
use std::{path::Path, process::Command}; use std::{cell::RefCell, path::Path, process::Command};
use tempfile::TempDir; use tempfile::TempDir;
/// Calling `set_search_path` with an empty directory makes sure that there /// Calling `set_search_path` with an empty directory makes sure that there
@ -329,13 +329,13 @@ mod tests {
r: &Repository, r: &Repository,
max_count: usize, max_count: usize,
) -> Vec<CommitId> { ) -> Vec<CommitId> {
let mut commit_ids = Vec::<CommitId>::new(); let commit_ids = RefCell::new(Vec::<CommitId>::new());
LogWalker::new(r, max_count) LogWalker::new(r, Some(max_count))
.unwrap() .unwrap()
.read(&mut commit_ids) .read(Some(&commit_ids))
.unwrap(); .unwrap();
commit_ids commit_ids.take()
} }
fn debug_cmd(path: &RepoPath, cmd: &str) -> String { fn debug_cmd(path: &RepoPath, cmd: &str) -> String {

View file

@ -1,6 +1,4 @@
use std::sync::{Arc, RwLock}; use super::utils::logitems::LogEntry;
use super::utils::logitems::ItemBatch;
use super::{visibility_blocking, BlameFileOpen, InspectCommitOpen}; use super::{visibility_blocking, BlameFileOpen, InspectCommitOpen};
use crate::keys::key_match; use crate::keys::key_match;
use crate::options::SharedOptions; use crate::options::SharedOptions;
@ -16,12 +14,12 @@ use crate::{
ui::{draw_scrollbar, style::SharedTheme, Orientation}, ui::{draw_scrollbar, style::SharedTheme, Orientation},
}; };
use anyhow::Result; use anyhow::Result;
use asyncgit::asyncjob::AsyncSingleJob;
use asyncgit::{ use asyncgit::{
sync::{ sync::{CommitId, RepoPathRef},
diff_contains_file, get_commits_info, CommitId, RepoPathRef, AsyncDiff, AsyncGitNotification, DiffParams, DiffType,
},
AsyncDiff, AsyncGitNotification, AsyncLog, DiffParams, DiffType,
}; };
use asyncgit::{AsyncFileHistoryJob, FileHistoryEntry};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use crossterm::event::Event; use crossterm::event::Event;
@ -33,8 +31,6 @@ use ratatui::{
Frame, Frame,
}; };
const SLICE_SIZE: usize = 1200;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct FileRevOpen { pub struct FileRevOpen {
pub file_path: String, pub file_path: String,
@ -52,7 +48,7 @@ impl FileRevOpen {
/// ///
pub struct FileRevlogComponent { pub struct FileRevlogComponent {
git_log: Option<AsyncLog>, git_history: Option<AsyncSingleJob<AsyncFileHistoryJob>>,
git_diff: AsyncDiff, git_diff: AsyncDiff,
theme: SharedTheme, theme: SharedTheme,
queue: Queue, queue: Queue,
@ -62,7 +58,7 @@ pub struct FileRevlogComponent {
repo_path: RepoPathRef, repo_path: RepoPathRef,
open_request: Option<FileRevOpen>, open_request: Option<FileRevOpen>,
table_state: std::cell::Cell<TableState>, table_state: std::cell::Cell<TableState>,
items: ItemBatch, items: Vec<FileHistoryEntry>,
count_total: usize, count_total: usize,
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
options: SharedOptions, options: SharedOptions,
@ -92,16 +88,16 @@ impl FileRevlogComponent {
true, true,
options.clone(), options.clone(),
), ),
git_log: None,
git_diff: AsyncDiff::new( git_diff: AsyncDiff::new(
repo_path.borrow().clone(), repo_path.borrow().clone(),
sender, sender,
), ),
git_history: None,
visible: false, visible: false,
repo_path: repo_path.clone(), repo_path: repo_path.clone(),
open_request: None, open_request: None,
table_state: std::cell::Cell::new(TableState::default()), table_state: std::cell::Cell::new(TableState::default()),
items: ItemBatch::default(), items: Vec::new(),
count_total: 0, count_total: 0,
key_config, key_config,
current_width: std::cell::Cell::new(0), current_width: std::cell::Cell::new(0),
@ -116,17 +112,17 @@ impl FileRevlogComponent {
/// ///
pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> { pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> {
self.items.clear();
self.open_request = Some(open_request.clone()); self.open_request = Some(open_request.clone());
let file_name = Arc::new(RwLock::new(open_request.file_path)); let mut job = AsyncSingleJob::new(self.sender.clone());
let filter = diff_contains_file(file_name); job.spawn(AsyncFileHistoryJob::new(
self.git_log = Some(AsyncLog::new(
self.repo_path.borrow().clone(), self.repo_path.borrow().clone(),
&self.sender, open_request.file_path,
Some(filter),
)); ));
self.items.clear(); self.git_history = Some(job);
self.set_selection(open_request.selection.unwrap_or(0)); self.set_selection(open_request.selection.unwrap_or(0));
self.show()?; self.show()?;
@ -143,19 +139,15 @@ impl FileRevlogComponent {
pub fn any_work_pending(&self) -> bool { pub fn any_work_pending(&self) -> bool {
self.git_diff.is_pending() self.git_diff.is_pending()
|| self || self
.git_log .git_history
.as_ref() .as_ref()
.map_or(false, AsyncLog::is_pending) .map_or(false, AsyncSingleJob::is_pending)
} }
/// ///
//TODO: needed?
pub fn update(&mut self) -> Result<()> { pub fn update(&mut self) -> Result<()> {
if let Some(ref mut git_log) = self.git_log { self.update_list()?;
git_log.fetch()?;
self.fetch_commits_if_needed()?;
self.update_diff()?;
}
Ok(()) Ok(())
} }
@ -167,8 +159,9 @@ impl FileRevlogComponent {
) -> Result<()> { ) -> Result<()> {
if self.visible { if self.visible {
match event { match event {
AsyncGitNotification::CommitFiles AsyncGitNotification::FileHistory => {
| AsyncGitNotification::Log => self.update()?, self.update_list()?
}
AsyncGitNotification::Diff => self.update_diff()?, AsyncGitNotification::Diff => self.update_diff()?,
_ => (), _ => (),
} }
@ -214,27 +207,40 @@ impl FileRevlogComponent {
Ok(()) Ok(())
} }
fn fetch_commits( pub fn update_list(&mut self) -> Result<()> {
&mut self, let is_pending = self
new_offset: usize, .git_history
new_max_offset: usize, .as_ref()
) -> Result<()> { .map(|git| git.is_pending())
if let Some(git_log) = &mut self.git_log { .unwrap_or_default();
let amount = new_max_offset
.saturating_sub(new_offset)
.max(SLICE_SIZE);
let commits = get_commits_info( if is_pending {
&self.repo_path.borrow(), if let Some(progress) = self
&git_log.get_slice(new_offset, amount)?, .git_history
self.current_width.get(), .as_ref()
); .and_then(|job| job.progress())
{
let result = progress.extract_results()?;
if let Ok(commits) = commits { log::info!(
self.items.set_items(new_offset, commits, &None); "file history update in progress: {}",
result.len()
);
self.items.extend(result.into_iter());
} }
}
self.count_total = git_log.count()?; if let Some(job) =
self.git_history.as_ref().and_then(|job| job.take_last())
{
let result = job.extract_results()?;
log::info!("file history finished: {}", result.len());
self.items.extend(result.into_iter());
self.git_history = None;
} }
Ok(()) Ok(())
@ -246,12 +252,9 @@ impl FileRevlogComponent {
let commit_id = table_state.selected().and_then(|selected| { let commit_id = table_state.selected().and_then(|selected| {
self.items self.items
.iter() .iter()
.nth( .nth(selected)
selected
.saturating_sub(self.items.index_offset()),
)
.as_ref() .as_ref()
.map(|entry| entry.id) .map(|entry| entry.commit)
}); });
self.table_state.set(table_state); self.table_state.set(table_state);
@ -270,7 +273,7 @@ impl FileRevlogComponent {
self.table_state.set(table); self.table_state.set(table);
res res
}; };
let revisions = self.get_max_selection(); let revisions = self.items.len();
self.open_request.as_ref().map_or( self.open_request.as_ref().map_or(
"<no history available>".into(), "<no history available>".into(),
@ -290,23 +293,31 @@ impl FileRevlogComponent {
.map(|entry| { .map(|entry| {
let spans = Line::from(vec![ let spans = Line::from(vec![
Span::styled( Span::styled(
entry.hash_short.to_string(), entry.commit.get_short_string(),
self.theme.commit_hash(false), self.theme.commit_hash(false),
), ),
Span::raw(" "), Span::raw(" "),
Span::styled( Span::styled(
entry.time_to_string(now), LogEntry::time_as_string(
LogEntry::timestamp_to_datetime(
entry.info.time,
)
.unwrap_or_default(),
now,
),
self.theme.commit_time(false), self.theme.commit_time(false),
), ),
Span::raw(" "), Span::raw(" "),
Span::styled( Span::styled(
entry.author.to_string(), entry.info.author.clone(),
self.theme.commit_author(false), self.theme.commit_author(false),
), ),
]); ]);
let mut text = Text::from(spans); let mut text = Text::from(spans);
text.extend(Text::raw(entry.msg.to_string())); text.extend(Text::raw(
entry.info.message.to_string(),
));
let cells = vec![Cell::from(""), Cell::from(text)]; let cells = vec![Cell::from(""), Cell::from(text)];
@ -315,19 +326,13 @@ impl FileRevlogComponent {
.collect() .collect()
} }
fn get_max_selection(&self) -> usize {
self.git_log.as_ref().map_or(0, |log| {
log.count().unwrap_or(0).saturating_sub(1)
})
}
fn move_selection( fn move_selection(
&mut self, &mut self,
scroll_type: ScrollType, scroll_type: ScrollType,
) -> Result<()> { ) -> Result<()> {
let old_selection = let old_selection =
self.table_state.get_mut().selected().unwrap_or(0); self.table_state.get_mut().selected().unwrap_or(0);
let max_selection = self.get_max_selection(); let max_selection = self.items.len();
let height_in_items = self.current_height.get() / 2; let height_in_items = self.current_height.get() / 2;
let new_selection = match scroll_type { let new_selection = match scroll_type {
@ -351,7 +356,6 @@ impl FileRevlogComponent {
} }
self.set_selection(new_selection); self.set_selection(new_selection);
self.fetch_commits_if_needed()?;
Ok(()) Ok(())
} }
@ -370,22 +374,6 @@ impl FileRevlogComponent {
self.table_state.get_mut().select(Some(selection)); self.table_state.get_mut().select(Some(selection));
} }
fn fetch_commits_if_needed(&mut self) -> Result<()> {
let selection =
self.table_state.get_mut().selected().unwrap_or(0);
let offset = *self.table_state.get_mut().offset_mut();
let height_in_items =
(self.current_height.get().saturating_sub(2)) / 2;
let new_max_offset =
selection.saturating_add(height_in_items);
if self.items.needs_data(offset, new_max_offset) {
self.fetch_commits(offset, new_max_offset)?;
}
Ok(())
}
fn get_selection(&self) -> Option<usize> { fn get_selection(&self) -> Option<usize> {
let table_state = self.table_state.take(); let table_state = self.table_state.take();
let selection = table_state.selected(); let selection = table_state.selected();
@ -432,14 +420,10 @@ impl FileRevlogComponent {
// at index 50. Subtracting the current offset from the selected index // at index 50. Subtracting the current offset from the selected index
// yields the correct index in `self.items`, in this case 0. // yields the correct index in `self.items`, in this case 0.
let mut adjusted_table_state = TableState::default() let mut adjusted_table_state = TableState::default()
.with_selected(table_state.selected().map(|selected| { .with_selected(
selected.saturating_sub(self.items.index_offset()) table_state.selected().map(|selected| selected),
})) )
.with_offset( .with_offset(table_state.offset());
table_state
.offset()
.saturating_sub(self.items.index_offset()),
);
f.render_widget(Clear, area); f.render_widget(Clear, area);
f.render_stateful_widget( f.render_stateful_widget(

View file

@ -26,18 +26,11 @@ impl From<CommitInfo> for LogEntry {
fn from(c: CommitInfo) -> Self { fn from(c: CommitInfo) -> Self {
let hash_short = c.id.get_short_string().into(); let hash_short = c.id.get_short_string().into();
let time = { let time = Self::timestamp_to_datetime(c.time);
let date = NaiveDateTime::from_timestamp_opt(c.time, 0); if time.is_none() {
if date.is_none() { log::error!("error reading commit date: {hash_short} - timestamp: {}",c.time);
log::error!("error reading commit date: {hash_short} - timestamp: {}",c.time); }
} let time = time.unwrap_or_default();
DateTime::<Local>::from(
DateTime::<Utc>::from_naive_utc_and_offset(
date.unwrap_or_default(),
Utc,
),
)
};
let author = c.author; let author = c.author;
#[allow(unused_mut)] #[allow(unused_mut)]
@ -60,7 +53,25 @@ impl From<CommitInfo> for LogEntry {
impl LogEntry { impl LogEntry {
pub fn time_to_string(&self, now: DateTime<Local>) -> String { pub fn time_to_string(&self, now: DateTime<Local>) -> String {
let delta = now - self.time; Self::time_as_string(self.time, now)
}
pub fn timestamp_to_datetime(
time: i64,
) -> Option<DateTime<Local>> {
let date = NaiveDateTime::from_timestamp_opt(time, 0)?;
Some(DateTime::<Local>::from(
DateTime::<Utc>::from_naive_utc_and_offset(date, Utc),
))
}
///
pub fn time_as_string(
time: DateTime<Local>,
now: DateTime<Local>,
) -> String {
let delta = now - time;
if delta < Duration::minutes(30) { if delta < Duration::minutes(30) {
let delta_str = if delta < Duration::minutes(1) { let delta_str = if delta < Duration::minutes(1) {
"<1m ago".to_string() "<1m ago".to_string()
@ -68,10 +79,10 @@ impl LogEntry {
format!("{:0>2}m ago", delta.num_minutes()) format!("{:0>2}m ago", delta.num_minutes())
}; };
format!("{delta_str: <10}") format!("{delta_str: <10}")
} else if self.time.date_naive() == now.date_naive() { } else if time.date_naive() == now.date_naive() {
self.time.format("%T ").to_string() time.format("%T ").to_string()
} else { } else {
self.time.format("%Y-%m-%d").to_string() time.format("%Y-%m-%d").to_string()
} }
} }
} }