From bf43a16bdfce17ba0d5bb35ea73486d14a6f0802 Mon Sep 17 00:00:00 2001 From: extrawurst Date: Thu, 14 Sep 2023 12:39:19 +0200 Subject: [PATCH] wip --- asyncgit/src/asyncjob/mod.rs | 6 + asyncgit/src/file_history.rs | 300 +++++++++++++++++++++++++++++ asyncgit/src/lib.rs | 6 + asyncgit/src/revlog.rs | 11 +- asyncgit/src/sync/commit.rs | 9 +- asyncgit/src/sync/commit_filter.rs | 62 +----- asyncgit/src/sync/commits_info.rs | 12 +- asyncgit/src/sync/logwalker.rs | 124 ++++++++---- asyncgit/src/sync/mod.rs | 16 +- src/components/file_revlog.rs | 162 +++++++--------- src/components/utils/logitems.rs | 43 +++-- 11 files changed, 535 insertions(+), 216 deletions(-) create mode 100644 asyncgit/src/file_history.rs diff --git a/asyncgit/src/asyncjob/mod.rs b/asyncgit/src/asyncjob/mod.rs index d92be0de67..b7f0d0868e 100644 --- a/asyncgit/src/asyncjob/mod.rs +++ b/asyncgit/src/asyncjob/mod.rs @@ -7,6 +7,7 @@ use crossbeam_channel::Sender; use std::sync::{Arc, Mutex, RwLock}; /// Passed to `AsyncJob::run` allowing sending intermediate progress notifications +#[derive(Clone)] pub struct RunParams< T: Copy + Send, P: Clone + Send + Sync + PartialEq, @@ -37,6 +38,11 @@ impl true }) } + + /// + pub fn progress(&self) -> P { + self.progress.read().cl + } } /// trait that defines an async task we can run on a threadpool diff --git a/asyncgit/src/file_history.rs b/asyncgit/src/file_history.rs new file mode 100644 index 0000000000..37197ddc61 --- /dev/null +++ b/asyncgit/src/file_history.rs @@ -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 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, + pub duration: Duration, +} + +enum JobState { + Request { + file_path: String, + repo_path: RepoPath, + }, + Response(Result), +} + +#[derive(Clone, Default)] +pub struct AsyncFileHistoryResults(Arc>>); + +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> { + 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>>, + 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> { + 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> { + self.results.extract_results() + } + + fn file_history_filter( + file_path: Arc>, + results: Arc>>, + params: &RunParams< + AsyncGitNotification, + AsyncFileHistoryResults, + >, + ) -> SharedCommitFilterFn { + let params = params.clone(); + + Arc::new(Box::new( + move |repo: &Repository, + commit_id: &CommitId| + -> Result { + 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 { + 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>, + results: Arc>>, + repo: &Repository, + commit_id: &CommitId, +) -> Result { + 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, + ¤t_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, + ) -> Result { + 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, ¶ms), + ), + JobState::Response(result) => { + JobState::Response(result) + } + }); + } + + Ok(AsyncGitNotification::FileHistory) + } +} diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index d03ed30d5e..17a5c2a4b2 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -38,6 +38,7 @@ mod commit_files; mod diff; mod error; mod fetch_job; +mod file_history; mod filter_commits; mod progress; mod pull; @@ -58,6 +59,9 @@ pub use crate::{ diff::{AsyncDiff, DiffParams, DiffType}, error::{Error, Result}, fetch_job::AsyncFetchJob, + file_history::{ + AsyncFileHistoryJob, FileHistoryEntry, FileHistoryEntryDelta, + }, filter_commits::{AsyncCommitFilterJob, CommitFilterResult}, progress::ProgressPercent, pull::{AsyncPull, FetchRequest}, @@ -115,6 +119,8 @@ pub enum AsyncGitNotification { TreeFiles, /// CommitFilter, + /// + FileHistory, } /// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait diff --git a/asyncgit/src/revlog.rs b/asyncgit/src/revlog.rs index 974dcaf30c..e81f1182a3 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -8,6 +8,7 @@ use crate::{ use crossbeam_channel::Sender; use scopetime::scope_time; use std::{ + cell::RefCell, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -201,17 +202,17 @@ impl AsyncLog { ) -> Result<()> { 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 mut walker = - LogWalker::new(&r, LIMIT_COUNT)?.filter(filter); + LogWalker::new(&r, Some(LIMIT_COUNT))?.filter(filter); loop { - entries.clear(); - let read = walker.read(&mut entries)?; + entries.borrow_mut().clear(); + let read = walker.read(Some(&entries))?; let mut current = arc_current.lock()?; - current.commits.extend(entries.iter()); + current.commits.extend(entries.borrow().iter()); current.duration = start_time.elapsed(); if read == 0 { diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index d1af74d85d..8269fd2d3c 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -134,13 +134,14 @@ mod tests { }; use commit::{amend, tag_commit}; use git2::Repository; + use std::cell::RefCell; use std::{fs::File, io::Write, path::Path}; fn count_commits(repo: &Repository, max: usize) -> usize { - let mut items = Vec::new(); - let mut walk = LogWalker::new(repo, max).unwrap(); - walk.read(&mut items).unwrap(); - items.len() + let items = RefCell::new(Vec::new()); + let mut walk = LogWalker::new(repo, Some(max)).unwrap(); + walk.read(Some(&items)).unwrap(); + items.take().len() } #[test] diff --git a/asyncgit/src/sync/commit_filter.rs b/asyncgit/src/sync/commit_filter.rs index de70ff1ebb..ed13aa508a 100644 --- a/asyncgit/src/sync/commit_filter.rs +++ b/asyncgit/src/sync/commit_filter.rs @@ -1,71 +1,15 @@ -use super::{ - commit_files::{commit_contains_file, get_commit_diff}, - CommitId, -}; -use crate::{ - error::Result, sync::commit_files::commit_detect_file_rename, -}; +use super::{commit_files::get_commit_diff, CommitId}; +use crate::error::Result; use bitflags::bitflags; use fuzzy_matcher::FuzzyMatcher; use git2::{Diff, Repository}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; /// pub type SharedCommitFilterFn = Arc< Box Result + Send + Sync>, >; -/// -pub fn diff_contains_file( - file_path: Arc>, -) -> SharedCommitFilterFn { - Arc::new(Box::new( - move |repo: &Repository, - commit_id: &CommitId| - -> Result { - 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, - // ¤t_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! { /// pub struct SearchFields: u32 { diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 1f049e9003..9847d32ed8 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -1,6 +1,6 @@ use super::RepoPath; use crate::{error::Result, sync::repository::repo}; -use git2::{Commit, Error, Oid}; +use git2::{Commit, Error, Oid, Repository}; use scopetime::scope_time; use unicode_truncate::UnicodeTruncateStr; @@ -65,7 +65,7 @@ impl From for CommitId { } /// -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct CommitInfo { /// pub message: String, @@ -121,6 +121,14 @@ pub fn get_commit_info( 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 { let commit = repo.find_commit((*commit_id).into())?; let author = commit.author(); diff --git a/asyncgit/src/sync/logwalker.rs b/asyncgit/src/sync/logwalker.rs index 5ef608bdf4..f64eeab878 100644 --- a/asyncgit/src/sync/logwalker.rs +++ b/asyncgit/src/sync/logwalker.rs @@ -3,6 +3,7 @@ use super::{CommitId, SharedCommitFilterFn}; use crate::error::Result; use git2::{Commit, Oid, Repository}; use std::{ + cell::RefCell, cmp::Ordering, collections::{BinaryHeap, HashSet}, }; @@ -33,14 +34,17 @@ impl<'a> Ord for TimeOrderedCommit<'a> { pub struct LogWalker<'a> { commits: BinaryHeap>, visited: HashSet, - limit: usize, + limit: Option, repo: &'a Repository, filter: Option, } impl<'a> LogWalker<'a> { /// - pub fn new(repo: &'a Repository, limit: usize) -> Result { + pub fn new( + repo: &'a Repository, + limit: Option, + ) -> Result { let c = repo.head()?.peel_to_commit()?; let mut commits = BinaryHeap::with_capacity(10); @@ -70,7 +74,10 @@ impl<'a> LogWalker<'a> { } /// - pub fn read(&mut self, out: &mut Vec) -> Result { + pub fn read( + &mut self, + out: Option<&RefCell>>, + ) -> Result { let mut count = 0_usize; while let Some(c) = self.commits.pop() { @@ -87,11 +94,17 @@ impl<'a> LogWalker<'a> { }; if commit_should_be_included { - out.push(id); + if let Some(out) = out { + out.borrow_mut().push(id); + } } count += 1; - if count == self.limit { + if self + .limit + .map(|limit| limit == count) + .unwrap_or_default() + { break; } } @@ -112,6 +125,9 @@ impl<'a> LogWalker<'a> { mod tests { use super::*; 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::tests::{rename_file, write_commit_file}; use crate::sync::{ @@ -119,13 +135,47 @@ mod tests { tests::repo_init_empty, }; use crate::sync::{ - diff_contains_file, filter_commit_by_search, stage_add_all, - LogFilterSearch, LogFilterSearchOptions, RepoPath, + filter_commit_by_search, stage_add_all, LogFilterSearch, + LogFilterSearchOptions, RepoPath, }; use pretty_assertions::assert_eq; use std::sync::{Arc, RwLock}; use std::{fs::File, io::Write, path::Path}; + fn diff_contains_file( + file_path: Arc>, + ) -> SharedCommitFilterFn { + Arc::new(Box::new( + move |repo: &Repository, + commit_id: &CommitId| + -> Result { + 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] fn test_limit() -> Result<()> { let file_path = Path::new("foo"); @@ -141,9 +191,10 @@ mod tests { stage_add_file(repo_path, file_path).unwrap(); let oid2 = commit(repo_path, "commit2").unwrap(); - let mut items = Vec::new(); - let mut walk = LogWalker::new(&repo, 1)?; - walk.read(&mut items).unwrap(); + let items = RefCell::new(Vec::new()); + let mut walk = LogWalker::new(&repo, Some(1))?; + walk.read(Some(&items)).unwrap(); + let items = items.take(); assert_eq!(items.len(), 1); assert_eq!(items[0], oid2); @@ -166,9 +217,10 @@ mod tests { stage_add_file(repo_path, file_path).unwrap(); let oid2 = commit(repo_path, "commit2").unwrap(); - let mut items = Vec::new(); - let mut walk = LogWalker::new(&repo, 100)?; - walk.read(&mut items).unwrap(); + let items = RefCell::new(Vec::new()); + let mut walk = LogWalker::new(&repo, Some(100))?; + walk.read(Some(&items)).unwrap(); + let items = items.take(); let info = get_commits_info(repo_path, &items, 50).unwrap(); dbg!(&info); @@ -176,8 +228,9 @@ mod tests { assert_eq!(items.len(), 2); assert_eq!(items[0], oid2); - let mut items = Vec::new(); - walk.read(&mut items).unwrap(); + let items = RefCell::new(Vec::new()); + walk.read(Some(&items)).unwrap(); + let items = items.take(); assert_eq!(items.len(), 0); @@ -211,26 +264,29 @@ mod tests { let file_path = Arc::new(RwLock::new(String::from("baz"))); let diff_contains_baz = diff_contains_file(file_path); - let mut items = Vec::new(); - let mut walker = LogWalker::new(&repo, 100)? + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100))? .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[0], second_commit_id); - let mut items = Vec::new(); - walker.read(&mut items).unwrap(); + let items = RefCell::new(Vec::new()); + walker.read(Some(&items)).unwrap(); + let items = items.take(); assert_eq!(items.len(), 0); let file_path = Arc::new(RwLock::new(String::from("bar"))); let diff_contains_bar = diff_contains_file(file_path); - let mut items = Vec::new(); - let mut walker = LogWalker::new(&repo, 100)? + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100))? .filter(Some(diff_contains_bar)); - walker.read(&mut items).unwrap(); + walker.read(Some(&items)).unwrap(); + let items = items.take(); assert_eq!(items.len(), 0); @@ -258,11 +314,12 @@ mod tests { }), ); - let mut items = Vec::new(); - let mut walker = LogWalker::new(&repo, 100) + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100)) .unwrap() .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[0], second_commit_id); @@ -275,13 +332,13 @@ mod tests { }), ); - let mut items = Vec::new(); - let mut walker = LogWalker::new(&repo, 100) + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100)) .unwrap() .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] @@ -305,11 +362,12 @@ mod tests { Arc::new(RwLock::new(String::from("bar.txt"))); let log_filter = diff_contains_file(file_path.clone()); - let mut items = Vec::new(); - let mut walker = LogWalker::new(&repo, 100) + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100)) .unwrap() .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[1], rename_commit); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index ff9e6e6a07..37b4bf209d 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -50,11 +50,11 @@ pub use commit_details::{ }; pub use commit_files::get_commit_files; pub use commit_filter::{ - diff_contains_file, filter_commit_by_search, LogFilterSearch, - LogFilterSearchOptions, SearchFields, SearchOptions, - SharedCommitFilterFn, + filter_commit_by_search, LogFilterSearch, LogFilterSearchOptions, + SearchFields, SearchOptions, SharedCommitFilterFn, }; pub use commit_revert::{commit_revert, revert_commit, revert_head}; +pub(crate) use commits_info::get_commit_info_repo; pub use commits_info::{ get_commit_info, get_commits_info, CommitId, CommitInfo, }; @@ -118,7 +118,7 @@ mod tests { }; use crate::error::Result; use git2::Repository; - use std::{path::Path, process::Command}; + use std::{cell::RefCell, path::Path, process::Command}; use tempfile::TempDir; /// Calling `set_search_path` with an empty directory makes sure that there @@ -329,13 +329,13 @@ mod tests { r: &Repository, max_count: usize, ) -> Vec { - let mut commit_ids = Vec::::new(); - LogWalker::new(r, max_count) + let commit_ids = RefCell::new(Vec::::new()); + LogWalker::new(r, Some(max_count)) .unwrap() - .read(&mut commit_ids) + .read(Some(&commit_ids)) .unwrap(); - commit_ids + commit_ids.take() } fn debug_cmd(path: &RepoPath, cmd: &str) -> String { diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index c18fac7200..122c34a12e 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -1,6 +1,4 @@ -use std::sync::{Arc, RwLock}; - -use super::utils::logitems::ItemBatch; +use super::utils::logitems::LogEntry; use super::{visibility_blocking, BlameFileOpen, InspectCommitOpen}; use crate::keys::key_match; use crate::options::SharedOptions; @@ -16,12 +14,12 @@ use crate::{ ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; use anyhow::Result; +use asyncgit::asyncjob::AsyncSingleJob; use asyncgit::{ - sync::{ - diff_contains_file, get_commits_info, CommitId, RepoPathRef, - }, - AsyncDiff, AsyncGitNotification, AsyncLog, DiffParams, DiffType, + sync::{CommitId, RepoPathRef}, + AsyncDiff, AsyncGitNotification, DiffParams, DiffType, }; +use asyncgit::{AsyncFileHistoryJob, FileHistoryEntry}; use chrono::{DateTime, Local}; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -33,8 +31,6 @@ use ratatui::{ Frame, }; -const SLICE_SIZE: usize = 1200; - #[derive(Clone, Debug)] pub struct FileRevOpen { pub file_path: String, @@ -52,7 +48,7 @@ impl FileRevOpen { /// pub struct FileRevlogComponent { - git_log: Option, + git_history: Option>, git_diff: AsyncDiff, theme: SharedTheme, queue: Queue, @@ -62,7 +58,7 @@ pub struct FileRevlogComponent { repo_path: RepoPathRef, open_request: Option, table_state: std::cell::Cell, - items: ItemBatch, + items: Vec, count_total: usize, key_config: SharedKeyConfig, options: SharedOptions, @@ -92,16 +88,16 @@ impl FileRevlogComponent { true, options.clone(), ), - git_log: None, git_diff: AsyncDiff::new( repo_path.borrow().clone(), sender, ), + git_history: None, visible: false, repo_path: repo_path.clone(), open_request: None, table_state: std::cell::Cell::new(TableState::default()), - items: ItemBatch::default(), + items: Vec::new(), count_total: 0, key_config, current_width: std::cell::Cell::new(0), @@ -116,17 +112,17 @@ impl FileRevlogComponent { /// pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> { + self.items.clear(); self.open_request = Some(open_request.clone()); - let file_name = Arc::new(RwLock::new(open_request.file_path)); - let filter = diff_contains_file(file_name); - self.git_log = Some(AsyncLog::new( + let mut job = AsyncSingleJob::new(self.sender.clone()); + job.spawn(AsyncFileHistoryJob::new( self.repo_path.borrow().clone(), - &self.sender, - Some(filter), + open_request.file_path, )); - self.items.clear(); + self.git_history = Some(job); + self.set_selection(open_request.selection.unwrap_or(0)); self.show()?; @@ -143,19 +139,15 @@ impl FileRevlogComponent { pub fn any_work_pending(&self) -> bool { self.git_diff.is_pending() || self - .git_log + .git_history .as_ref() - .map_or(false, AsyncLog::is_pending) + .map_or(false, AsyncSingleJob::is_pending) } /// + //TODO: needed? pub fn update(&mut self) -> Result<()> { - if let Some(ref mut git_log) = self.git_log { - git_log.fetch()?; - - self.fetch_commits_if_needed()?; - self.update_diff()?; - } + self.update_list()?; Ok(()) } @@ -167,8 +159,9 @@ impl FileRevlogComponent { ) -> Result<()> { if self.visible { match event { - AsyncGitNotification::CommitFiles - | AsyncGitNotification::Log => self.update()?, + AsyncGitNotification::FileHistory => { + self.update_list()? + } AsyncGitNotification::Diff => self.update_diff()?, _ => (), } @@ -214,27 +207,40 @@ impl FileRevlogComponent { Ok(()) } - fn fetch_commits( - &mut self, - new_offset: usize, - new_max_offset: usize, - ) -> Result<()> { - if let Some(git_log) = &mut self.git_log { - let amount = new_max_offset - .saturating_sub(new_offset) - .max(SLICE_SIZE); - - let commits = get_commits_info( - &self.repo_path.borrow(), - &git_log.get_slice(new_offset, amount)?, - self.current_width.get(), - ); + pub fn update_list(&mut self) -> Result<()> { + let is_pending = self + .git_history + .as_ref() + .map(|git| git.is_pending()) + .unwrap_or_default(); - if let Ok(commits) = commits { - self.items.set_items(new_offset, commits, &None); + if is_pending { + if let Some(progress) = self + .git_history + .as_ref() + .and_then(|job| job.progress()) + { + let result = progress.extract_results()?; + + log::info!( + "file history update in progress: {}", + result.len() + ); + + self.items.extend(result.into_iter()); } + } + + if let Some(job) = + self.git_history.as_ref().and_then(|job| job.take_last()) + { + let result = job.extract_results()?; - self.count_total = git_log.count()?; + log::info!("file history finished: {}", result.len()); + + self.items.extend(result.into_iter()); + + self.git_history = None; } Ok(()) @@ -246,12 +252,9 @@ impl FileRevlogComponent { let commit_id = table_state.selected().and_then(|selected| { self.items .iter() - .nth( - selected - .saturating_sub(self.items.index_offset()), - ) + .nth(selected) .as_ref() - .map(|entry| entry.id) + .map(|entry| entry.commit) }); self.table_state.set(table_state); @@ -270,7 +273,7 @@ impl FileRevlogComponent { self.table_state.set(table); res }; - let revisions = self.get_max_selection(); + let revisions = self.items.len(); self.open_request.as_ref().map_or( "".into(), @@ -290,23 +293,31 @@ impl FileRevlogComponent { .map(|entry| { let spans = Line::from(vec![ Span::styled( - entry.hash_short.to_string(), + entry.commit.get_short_string(), self.theme.commit_hash(false), ), Span::raw(" "), 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), ), Span::raw(" "), Span::styled( - entry.author.to_string(), + entry.info.author.clone(), self.theme.commit_author(false), ), ]); 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)]; @@ -315,19 +326,13 @@ impl FileRevlogComponent { .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( &mut self, scroll_type: ScrollType, ) -> Result<()> { let old_selection = 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 new_selection = match scroll_type { @@ -351,7 +356,6 @@ impl FileRevlogComponent { } self.set_selection(new_selection); - self.fetch_commits_if_needed()?; Ok(()) } @@ -370,22 +374,6 @@ impl FileRevlogComponent { 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 { let table_state = self.table_state.take(); let selection = table_state.selected(); @@ -432,14 +420,10 @@ impl FileRevlogComponent { // at index 50. Subtracting the current offset from the selected index // yields the correct index in `self.items`, in this case 0. let mut adjusted_table_state = TableState::default() - .with_selected(table_state.selected().map(|selected| { - selected.saturating_sub(self.items.index_offset()) - })) - .with_offset( - table_state - .offset() - .saturating_sub(self.items.index_offset()), - ); + .with_selected( + table_state.selected().map(|selected| selected), + ) + .with_offset(table_state.offset()); f.render_widget(Clear, area); f.render_stateful_widget( diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 12d72862e9..0b2c60214f 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -26,18 +26,11 @@ impl From for LogEntry { fn from(c: CommitInfo) -> Self { let hash_short = c.id.get_short_string().into(); - let time = { - let date = NaiveDateTime::from_timestamp_opt(c.time, 0); - if date.is_none() { - log::error!("error reading commit date: {hash_short} - timestamp: {}",c.time); - } - DateTime::::from( - DateTime::::from_naive_utc_and_offset( - date.unwrap_or_default(), - Utc, - ), - ) - }; + let time = Self::timestamp_to_datetime(c.time); + if time.is_none() { + log::error!("error reading commit date: {hash_short} - timestamp: {}",c.time); + } + let time = time.unwrap_or_default(); let author = c.author; #[allow(unused_mut)] @@ -60,7 +53,25 @@ impl From for LogEntry { impl LogEntry { pub fn time_to_string(&self, now: DateTime) -> String { - let delta = now - self.time; + Self::time_as_string(self.time, now) + } + + pub fn timestamp_to_datetime( + time: i64, + ) -> Option> { + let date = NaiveDateTime::from_timestamp_opt(time, 0)?; + + Some(DateTime::::from( + DateTime::::from_naive_utc_and_offset(date, Utc), + )) + } + + /// + pub fn time_as_string( + time: DateTime, + now: DateTime, + ) -> String { + let delta = now - time; if delta < Duration::minutes(30) { let delta_str = if delta < Duration::minutes(1) { "<1m ago".to_string() @@ -68,10 +79,10 @@ impl LogEntry { format!("{:0>2}m ago", delta.num_minutes()) }; format!("{delta_str: <10}") - } else if self.time.date_naive() == now.date_naive() { - self.time.format("%T ").to_string() + } else if time.date_naive() == now.date_naive() { + time.format("%T ").to_string() } else { - self.time.format("%Y-%m-%d").to_string() + time.format("%Y-%m-%d").to_string() } } }