From c99b320db3514327c46d71450f09d323e91e5233 Mon Sep 17 00:00:00 2001 From: extrawurst Date: Wed, 6 Sep 2023 12:21:33 +0200 Subject: [PATCH 01/10] file history rename detection find more renames that git-log --follow would simplify --- asyncgit/src/asyncjob/mod.rs | 1 + asyncgit/src/file_history.rs | 300 +++++++++++++++++++++++++++++ asyncgit/src/lib.rs | 6 + asyncgit/src/revlog.rs | 16 +- asyncgit/src/sync/commit.rs | 9 +- asyncgit/src/sync/commit_files.rs | 113 ++++++++++- asyncgit/src/sync/commit_filter.rs | 21 -- asyncgit/src/sync/commits_info.rs | 14 +- asyncgit/src/sync/logwalker.rs | 154 ++++++++++++--- asyncgit/src/sync/mod.rs | 24 ++- asyncgit/src/sync/repository.rs | 5 + src/components/mod.rs | 5 +- src/components/utils/logitems.rs | 28 ++- src/popups/file_revlog.rs | 193 ++++++++++--------- 14 files changed, 715 insertions(+), 174 deletions(-) create mode 100644 asyncgit/src/file_history.rs diff --git a/asyncgit/src/asyncjob/mod.rs b/asyncgit/src/asyncjob/mod.rs index 19058f9bac..a573a01683 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, diff --git a/asyncgit/src/file_history.rs b/asyncgit/src/file_history.rs new file mode 100644 index 0000000000..c1a6483afe --- /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()?; + log::trace!("pull entries {}", results.len()); + 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(); + + if fun_name(file_path, results.clone(), 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 filter = Self::file_history_filter( + file_name, + self.results.0.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: Default::default(), // TODO + // result: self.results.0.into_inner()?, + }; + + 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); + log::trace!("push entry {}", results.lock()?.len()); + + 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 fa022e4020..e77a6bac0f 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -40,6 +40,7 @@ mod commit_files; mod diff; mod error; mod fetch_job; +mod file_history; mod filter_commits; mod progress; mod pull; @@ -60,6 +61,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}, @@ -117,6 +121,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 38febb84a4..f6c04623db 100644 --- a/asyncgit/src/revlog.rs +++ b/asyncgit/src/revlog.rs @@ -9,6 +9,7 @@ use crate::{ use crossbeam_channel::Sender; use scopetime::scope_time; use std::{ + cell::RefCell, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -230,19 +231,20 @@ impl AsyncLog { ) -> Result<()> { let start_time = Instant::now(); - let mut entries = vec![CommitId::default(); LIMIT_COUNT]; - entries.resize(0, CommitId::default()); + let entries = + RefCell::new(vec![CommitId::default(); LIMIT_COUNT]); + entries.borrow_mut().resize(0, CommitId::default()); let r = repo(repo_path)?; - let mut walker = - LogWalker::new(&r, LIMIT_COUNT)?.filter(Some(filter)); + let mut walker = LogWalker::new(&r, Some(LIMIT_COUNT))? + .filter(Some(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 f4e0b194ea..3b37a4f75c 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -210,13 +210,14 @@ mod tests { }; use commit::{amend, commit_message_prettify, 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_files.rs b/asyncgit/src/sync/commit_files.rs index c03d7c13cf..a70270fdce 100644 --- a/asyncgit/src/sync/commit_files.rs +++ b/asyncgit/src/sync/commit_files.rs @@ -3,10 +3,10 @@ use super::{diff::DiffOptions, CommitId, RepoPath}; use crate::{ error::Result, - sync::{get_stashes, repository::repo}, - StatusItem, StatusItemType, + sync::{get_stashes, repository::repo, utils::bytes2string}, + Error, StatusItem, StatusItemType, }; -use git2::{Diff, Repository}; +use git2::{Diff, DiffFindOptions, Repository}; use scopetime::scope_time; use std::collections::HashSet; @@ -179,14 +179,93 @@ pub(crate) fn get_commit_diff<'a>( Ok(diff) } +/// +pub(crate) fn commit_contains_file( + repo: &Repository, + id: CommitId, + pathspec: &str, +) -> Result> { + let commit = repo.find_commit(id.into())?; + let commit_tree = commit.tree()?; + + let parent = if commit.parent_count() > 0 { + repo.find_commit(commit.parent_id(0)?) + .ok() + .and_then(|c| c.tree().ok()) + } else { + None + }; + + let mut opts = git2::DiffOptions::new(); + opts.pathspec(pathspec.to_string()) + .skip_binary_check(true) + .context_lines(0); + + let diff = repo.diff_tree_to_tree( + parent.as_ref(), + Some(&commit_tree), + Some(&mut opts), + )?; + + if diff.stats()?.files_changed() == 0 { + return Ok(None); + } + + Ok(diff.deltas().map(|delta| delta.status()).next()) +} + +/// +pub(crate) fn commit_detect_file_rename( + repo: &Repository, + id: CommitId, + pathspec: &str, +) -> Result> { + scope_time!("commit_detect_file_rename"); + + let mut diff = get_commit_diff(repo, id, None, None, None)?; + + diff.find_similar(Some( + DiffFindOptions::new() + .renames(true) + .renames_from_rewrites(true) + .rename_from_rewrite_threshold(100), + ))?; + + let current_path = std::path::Path::new(pathspec); + + for delta in diff.deltas() { + let new_file_matches = delta + .new_file() + .path() + .is_some_and(|path| path == current_path); + + if new_file_matches + && matches!(delta.status(), git2::Delta::Renamed) + { + return Ok(Some(bytes2string( + delta.old_file().path_bytes().ok_or_else(|| { + Error::Generic(String::from("old_file error")) + })?, + )?)); + } + } + + Ok(None) +} + #[cfg(test)] mod tests { use super::get_commit_files; use crate::{ error::Result, sync::{ - commit, stage_add_file, stash_save, - tests::{get_statuses, repo_init}, + commit, + commit_files::commit_detect_file_rename, + stage_add_all, stage_add_file, stash_save, + tests::{ + get_statuses, rename_file, repo_init, + repo_init_empty, write_commit_file, + }, RepoPath, }, StatusItemType, @@ -266,4 +345,28 @@ mod tests { Ok(()) } + + #[test] + fn test_rename_detection() { + let (td, repo) = repo_init_empty().unwrap(); + let repo_path: RepoPath = td.path().into(); + + write_commit_file(&repo, "foo.txt", "foobar", "c1"); + rename_file(&repo, "foo.txt", "bar.txt"); + stage_add_all( + &repo_path, + "*", + Some(crate::sync::ShowUntrackedFilesConfig::All), + ) + .unwrap(); + let rename_commit = commit(&repo_path, "c2").unwrap(); + + let rename = commit_detect_file_rename( + &repo, + rename_commit, + "bar.txt", + ) + .unwrap(); + assert_eq!(rename, Some(String::from("foo.txt"))); + } } diff --git a/asyncgit/src/sync/commit_filter.rs b/asyncgit/src/sync/commit_filter.rs index f4b3e8a6c6..805a1b3afd 100644 --- a/asyncgit/src/sync/commit_filter.rs +++ b/asyncgit/src/sync/commit_filter.rs @@ -13,27 +13,6 @@ pub type SharedCommitFilterFn = Arc< Box Result + Send + Sync>, >; -/// -pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn { - Arc::new(Box::new( - move |repo: &Repository, - commit_id: &CommitId| - -> Result { - let diff = get_commit_diff( - repo, - *commit_id, - Some(file_path.clone()), - None, - None, - )?; - - let contains_file = diff.deltas().len() > 0; - - Ok(contains_file) - }, - )) -} - bitflags! { /// #[derive(Debug, Clone, Copy)] diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index 111a2b9bce..888ce2fa0e 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -5,7 +5,7 @@ use crate::{ error::Result, sync::{commit_details::get_author_of_commit, repository::repo}, }; -use git2::{Commit, Error, Oid}; +use git2::{Commit, Error, Oid, Repository}; use scopetime::scope_time; use unicode_truncate::UnicodeTruncateStr; @@ -85,7 +85,7 @@ impl From for CommitId { } /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CommitInfo { /// pub message: String, @@ -143,9 +143,17 @@ pub fn get_commit_info( scope_time!("get_commit_info"); let repo = repo(repo_path)?; - let mailmap = repo.mailmap()?; + get_commit_info_repo(&repo, commit_id) +} + +/// +pub fn get_commit_info_repo( + repo: &Repository, + commit_id: &CommitId, +) -> Result { let commit = repo.find_commit((*commit_id).into())?; + let mailmap = repo.mailmap()?; let author = get_author_of_commit(&commit, &mailmap); Ok(CommitInfo { diff --git a/asyncgit/src/sync/logwalker.rs b/asyncgit/src/sync/logwalker.rs index 743376d34f..4f9871ecbf 100644 --- a/asyncgit/src/sync/logwalker.rs +++ b/asyncgit/src/sync/logwalker.rs @@ -4,6 +4,7 @@ use crate::error::Result; use git2::{Commit, Oid, Repository}; use gix::revision::Walk; use std::{ + cell::RefCell, cmp::Ordering, collections::{BinaryHeap, HashSet}, }; @@ -34,14 +35,17 @@ impl Ord for TimeOrderedCommit<'_> { 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); @@ -71,7 +75,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() { @@ -88,11 +95,13 @@ 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.is_some_and(|limit| limit == count) { break; } } @@ -184,19 +193,57 @@ impl<'a> LogWalkerWithoutFilter<'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::write_commit_file; + use crate::sync::tests::{rename_file, write_commit_file}; use crate::sync::{ commit, get_commits_info, stage_add_file, tests::repo_init_empty, }; use crate::sync::{ - diff_contains_file, filter_commit_by_search, LogFilterSearch, + 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"); @@ -212,9 +259,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); @@ -237,9 +285,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); @@ -247,8 +296,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); @@ -316,27 +366,32 @@ mod tests { let _third_commit_id = commit(&repo_path, "commit3").unwrap(); - let diff_contains_baz = diff_contains_file("baz".into()); + 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 diff_contains_bar = diff_contains_file("bar".into()); + 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); @@ -364,11 +419,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); @@ -381,12 +437,46 @@ 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] + fn test_logwalker_with_filter_rename() { + let (td, repo) = repo_init_empty().unwrap(); + let repo_path: RepoPath = td.path().into(); + + write_commit_file(&repo, "foo.txt", "foobar", "c1"); + rename_file(&repo, "foo.txt", "bar.txt"); + stage_add_all( + &repo_path, + "*", + Some(crate::sync::ShowUntrackedFilesConfig::All), + ) + .unwrap(); + let rename_commit = commit(&repo_path, "c2").unwrap(); + + write_commit_file(&repo, "bar.txt", "new content", "c3"); + + let file_path = + Arc::new(RwLock::new(String::from("bar.txt"))); + let log_filter = diff_contains_file(file_path.clone()); + + let items = RefCell::new(Vec::new()); + let mut walker = LogWalker::new(&repo, Some(100)) + .unwrap() + .filter(Some(log_filter)); + walker.read(Some(&items)).unwrap(); + let items = items.take(); + + assert_eq!(items.len(), 3); + assert_eq!(items[1], rename_commit); + + assert_eq!(file_path.read().unwrap().as_str(), "foo.txt"); } } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 09d4e6ef53..a7b6d84ad3 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -51,11 +51,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, }; @@ -123,7 +123,7 @@ pub mod tests { }; use crate::error::Result; use git2::Repository; - use std::{ffi::OsStr, path::Path, process::Command}; + use std::{ffi::OsStr, cell::RefCell, path::Path, process::Command}; use tempfile::TempDir; /// @@ -260,13 +260,21 @@ pub 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() + } + + /// + pub fn rename_file(repo: &Repository, old: &str, new: &str) { + let dir = repo.workdir().unwrap(); + let old = dir.join(old); + let new = dir.join(new); + std::fs::rename(old, new).unwrap(); } /// Same as `repo_init`, but the repo is a bare repo (--bare) diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index ea251c5e46..c15262ab49 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -53,6 +53,11 @@ impl From<&str> for RepoPath { Self::Path(PathBuf::from(p)) } } +impl From<&Path> for RepoPath { + fn from(p: &Path) -> Self { + Self::Path(PathBuf::from(p)) + } +} pub fn repo(repo_path: &RepoPath) -> Result { let repo = Repository::open_ext( diff --git a/src/components/mod.rs b/src/components/mod.rs index f20d3f981a..342169475d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -21,9 +21,8 @@ pub use revision_files::RevisionFilesComponent; pub use syntax_text::SyntaxTextComponent; pub use textinput::{InputType, TextInputComponent}; pub use utils::{ - filetree::FileTreeItemKind, logitems::ItemBatch, - scroll_vertical::VerticalScroll, string_width_align, - time_to_string, + filetree::FileTreeItemKind, scroll_vertical::VerticalScroll, + string_width_align, time_to_string, }; use crate::ui::style::Theme; diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 4c980b65fa..69846ffc6c 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -1,5 +1,5 @@ use asyncgit::sync::{CommitId, CommitInfo}; -use chrono::{DateTime, Duration, Local, Utc}; +use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc}; use indexmap::IndexSet; use std::{rc::Rc, slice::Iter}; @@ -61,7 +61,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(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::try_minutes(30).unwrap_or_default() { let delta_str = if delta < Duration::try_minutes(1).unwrap_or_default() @@ -71,10 +89,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() } } } diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index 6bbbffbe8a..fb4ab1140a 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -3,7 +3,7 @@ use crate::{ components::{ event_pump, visibility_blocking, CommandBlocking, CommandInfo, Component, DiffComponent, DrawableComponent, - EventState, ItemBatch, ScrollType, + EventState, ScrollType, }, keys::{key_match, SharedKeyConfig}, options::SharedOptions, @@ -12,13 +12,13 @@ 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 chrono::{DateTime, Local}; +use asyncgit::{AsyncFileHistoryJob, FileHistoryEntry}; +use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc}; use crossbeam_channel::Sender; use crossterm::event::Event; use ratatui::{ @@ -30,8 +30,6 @@ use ratatui::{ use super::{BlameFileOpen, InspectCommitOpen}; -const SLICE_SIZE: usize = 1200; - #[derive(Clone, Debug)] pub struct FileRevOpen { pub file_path: String, @@ -49,7 +47,7 @@ impl FileRevOpen { /// pub struct FileRevlogPopup { - git_log: Option, + git_history: Option>, git_diff: AsyncDiff, theme: SharedTheme, queue: Queue, @@ -59,7 +57,7 @@ pub struct FileRevlogPopup { repo_path: RepoPathRef, open_request: Option, table_state: std::cell::Cell, - items: ItemBatch, + items: Vec, count_total: usize, key_config: SharedKeyConfig, options: SharedOptions, @@ -75,16 +73,16 @@ impl FileRevlogPopup { queue: env.queue.clone(), sender: env.sender_git.clone(), diff: DiffComponent::new(env, true), - git_log: None, git_diff: AsyncDiff::new( env.repo.borrow().clone(), &env.sender_git, ), + git_history: None, visible: false, repo_path: env.repo.clone(), open_request: None, table_state: std::cell::Cell::new(TableState::default()), - items: ItemBatch::default(), + items: Vec::new(), count_total: 0, key_config: env.key_config.clone(), current_width: std::cell::Cell::new(0), @@ -99,16 +97,17 @@ impl FileRevlogPopup { /// pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> { + self.items.clear(); self.open_request = Some(open_request.clone()); - let filter = diff_contains_file(open_request.file_path); - self.git_log = Some(AsyncLog::new( + let 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()?; @@ -124,17 +123,16 @@ impl FileRevlogPopup { /// pub fn any_work_pending(&self) -> bool { self.git_diff.is_pending() - || self.git_log.as_ref().is_some_and(AsyncLog::is_pending) + || self + .git_history + .as_ref() + .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(()) } @@ -146,8 +144,9 @@ impl FileRevlogPopup { ) -> Result<()> { if self.visible { match event { - AsyncGitNotification::CommitFiles - | AsyncGitNotification::Log => self.update()?, + AsyncGitNotification::FileHistory => { + self.update_list()? + } AsyncGitNotification::Diff => self.update_diff()?, _ => (), } @@ -193,27 +192,40 @@ impl FileRevlogPopup { 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(()) @@ -225,12 +237,9 @@ impl FileRevlogPopup { 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); @@ -249,7 +258,7 @@ impl FileRevlogPopup { 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(), @@ -263,29 +272,68 @@ impl FileRevlogPopup { ) } + fn time_as_string( + time: DateTime, + now: DateTime, + ) -> String { + let delta = now - time; + if delta < Duration::try_minutes(30).unwrap_or_default() { + let delta_str = if delta + < Duration::try_minutes(1).unwrap_or_default() + { + "<1m ago".to_string() + } else { + format!("{:0>2}m ago", delta.num_minutes()) + }; + format!("{delta_str: <10}") + } else if time.date_naive() == now.date_naive() { + time.format("%T ").to_string() + } else { + time.format("%Y-%m-%d").to_string() + } + } + + pub fn timestamp_to_datetime( + time: i64, + ) -> Option> { + let date = NaiveDateTime::from_timestamp(time, 0); + + Some(DateTime::::from( + DateTime::::from_naive_utc_and_offset(date, Utc), + )) + } + fn get_rows(&self, now: DateTime) -> Vec { self.items .iter() .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), + Self::time_as_string( + Self::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)]; @@ -294,19 +342,13 @@ impl FileRevlogPopup { .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 { @@ -330,7 +372,6 @@ impl FileRevlogPopup { } self.set_selection(new_selection); - self.fetch_commits_if_needed()?; Ok(()) } @@ -349,22 +390,6 @@ impl FileRevlogPopup { 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(); @@ -410,14 +435,10 @@ impl FileRevlogPopup { // 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( From fc7c6931fb5dfd899ecb0828fad331d6b57d8d8e Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:27:18 +0200 Subject: [PATCH 02/10] Live-update commit list --- asyncgit/src/file_history.rs | 20 +++++++++++++------- src/popups/file_revlog.rs | 36 +++++++----------------------------- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/asyncgit/src/file_history.rs b/asyncgit/src/file_history.rs index c1a6483afe..82976a4609 100644 --- a/asyncgit/src/file_history.rs +++ b/asyncgit/src/file_history.rs @@ -74,7 +74,6 @@ pub struct FileHistoryEntry { /// pub struct CommitFilterResult { /// - pub result: Vec, pub duration: Duration, } @@ -91,6 +90,10 @@ pub struct AsyncFileHistoryResults(Arc>>); impl PartialEq for AsyncFileHistoryResults { fn eq(&self, other: &Self) -> bool { + if Arc::ptr_eq(&self.0, &other.0) { + return true; + } + if let Ok(left) = self.0.lock() { if let Ok(right) = other.0.lock() { return *left == *right; @@ -167,8 +170,16 @@ impl AsyncFileHistoryJob { -> Result { let file_path = file_path.clone(); - if fun_name(file_path, results.clone(), repo, commit_id)? { + if fun_name( + file_path, + results.clone(), + repo, + commit_id, + )? { params.send(AsyncGitNotification::FileHistory)?; + params.set_progress(AsyncFileHistoryResults( + results.clone(), + ))?; Ok(true) } else { Ok(false) @@ -202,13 +213,8 @@ impl AsyncFileHistoryJob { walker.read(None)?; -// let result = -// std::mem::replace(&mut *result.lock()?, Vec::new()); - let result = CommitFilterResult { duration: start.elapsed(), - result: Default::default(), // TODO - // result: self.results.0.into_inner()?, }; Ok(result) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index fb4ab1140a..633b530c89 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -193,39 +193,17 @@ impl FileRevlogPopup { } pub fn update_list(&mut self) -> Result<()> { - let is_pending = self - .git_history - .as_ref() - .map(|git| git.is_pending()) - .unwrap_or_default(); - - 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()) + if let Some(progress) = + self.git_history.as_ref().and_then(|job| job.progress()) { - let result = job.extract_results()?; + let result = progress.extract_results()?; - log::info!("file history finished: {}", result.len()); + log::info!( + "file history update in progress: {}", + result.len() + ); self.items.extend(result.into_iter()); - - self.git_history = None; } Ok(()) From dadb8daee1e973705207e79b2f24315e7c6ad959 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:02:48 +0200 Subject: [PATCH 03/10] Use len - 1 as maximum selection --- src/popups/file_revlog.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index 633b530c89..ae078f8909 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -326,7 +326,7 @@ impl FileRevlogPopup { ) -> Result<()> { let old_selection = self.table_state.get_mut().selected().unwrap_or(0); - let max_selection = self.items.len(); + let max_selection = self.items.len().saturating_sub(1); let height_in_items = self.current_height.get() / 2; let new_selection = match scroll_type { From 45613ccd15d6c61ace4f3f44a07cc40fedaa1533 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:07:52 +0200 Subject: [PATCH 04/10] Render 1-based index for n-of-m style texts --- src/popups/file_revlog.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index ae078f8909..f10b870394 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -243,7 +243,7 @@ impl FileRevlogPopup { |open_request| { strings::file_log_title( &open_request.file_path, - selected, + selected + 1, revisions, ) }, From 37907a565afc1ca37379fdc698d98d6432f1d57e Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:22:39 +0200 Subject: [PATCH 05/10] Update diff once entries become available --- src/popups/file_revlog.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index f10b870394..7362719d41 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -203,7 +203,14 @@ impl FileRevlogPopup { result.len() ); + let was_empty = self.items.is_empty(); + self.items.extend(result.into_iter()); + + if was_empty && !self.items.is_empty() { + self.queue + .push(InternalEvent::Update(NeedsUpdate::DIFF)); + } } Ok(()) From 1fe7f3d258666de81bd18b4c11b581d8fd530ae0 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:45:55 +0200 Subject: [PATCH 06/10] Move from NaiveDateTime to DateTime fcts --- src/components/utils/logitems.rs | 8 ++------ src/popups/file_revlog.rs | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 69846ffc6c..b8e39df078 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -1,5 +1,5 @@ use asyncgit::sync::{CommitId, CommitInfo}; -use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc}; +use chrono::{DateTime, Duration, Local, Utc}; use indexmap::IndexSet; use std::{rc::Rc, slice::Iter}; @@ -67,11 +67,7 @@ impl LogEntry { pub fn timestamp_to_datetime( time: i64, ) -> Option> { - let date = NaiveDateTime::from_timestamp(time, 0); - - Some(DateTime::::from( - DateTime::::from_naive_utc_and_offset(date, Utc), - )) + Some(DateTime::<_>::from(DateTime::from_timestamp(time, 0)?)) } /// diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index 7362719d41..79086c212a 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -18,7 +18,7 @@ use asyncgit::{ AsyncDiff, AsyncGitNotification, DiffParams, DiffType, }; use asyncgit::{AsyncFileHistoryJob, FileHistoryEntry}; -use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc}; +use chrono::{DateTime, Duration, Local}; use crossbeam_channel::Sender; use crossterm::event::Event; use ratatui::{ @@ -281,11 +281,7 @@ impl FileRevlogPopup { pub fn timestamp_to_datetime( time: i64, ) -> Option> { - let date = NaiveDateTime::from_timestamp(time, 0); - - Some(DateTime::::from( - DateTime::::from_naive_utc_and_offset(date, Utc), - )) + Some(DateTime::<_>::from(DateTime::from_timestamp(time, 0)?)) } fn get_rows(&self, now: DateTime) -> Vec { From e44f79660731619dab77b38d4560d6c9dafd5a13 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Mon, 31 Mar 2025 23:39:01 +0200 Subject: [PATCH 07/10] Fix clippy hints --- asyncgit/src/file_history.rs | 31 ++++++++++++----------------- src/popups/file_revlog.rs | 38 +++++++++++++++--------------------- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/asyncgit/src/file_history.rs b/asyncgit/src/file_history.rs index 82976a4609..e1a2e74bc5 100644 --- a/asyncgit/src/file_history.rs +++ b/asyncgit/src/file_history.rs @@ -44,21 +44,19 @@ impl From for FileHistoryEntryDelta { | 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 - } + | git2::Delta::Untracked => Self::None, + git2::Delta::Added => Self::Added, + git2::Delta::Deleted => Self::Deleted, + git2::Delta::Modified => Self::Modified, + git2::Delta::Renamed => Self::Renamed, + git2::Delta::Copied => Self::Copied, + git2::Delta::Typechange => Self::Typechange, } } } /// -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FileHistoryEntry { /// pub commit: CommitId, @@ -170,12 +168,7 @@ impl AsyncFileHistoryJob { -> Result { let file_path = file_path.clone(); - if fun_name( - file_path, - results.clone(), - repo, - commit_id, - )? { + if fun_name(&file_path, &results, repo, commit_id)? { params.send(AsyncGitNotification::FileHistory)?; params.set_progress(AsyncFileHistoryResults( results.clone(), @@ -222,8 +215,8 @@ impl AsyncFileHistoryJob { } fn fun_name( - file_path: Arc>, - results: Arc>>, + file_path: &Arc>, + results: &Arc>>, repo: &Repository, commit_id: &CommitId, ) -> Result { @@ -246,7 +239,7 @@ fn fun_name( let entry = FileHistoryEntry { commit: *commit_id, - delta: delta.clone().into(), + delta: delta.into(), info: commit_info, file_path: current_file_path.clone(), }; diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index 79086c212a..d75db3f5de 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -126,7 +126,7 @@ impl FileRevlogPopup { || self .git_history .as_ref() - .map_or(false, AsyncSingleJob::is_pending) + .is_some_and(AsyncSingleJob::is_pending) } /// @@ -145,7 +145,7 @@ impl FileRevlogPopup { if self.visible { match event { AsyncGitNotification::FileHistory => { - self.update_list()? + self.update_list()?; } AsyncGitNotification::Diff => self.update_diff()?, _ => (), @@ -193,8 +193,10 @@ impl FileRevlogPopup { } pub fn update_list(&mut self) -> Result<()> { - if let Some(progress) = - self.git_history.as_ref().and_then(|job| job.progress()) + if let Some(progress) = self + .git_history + .as_ref() + .and_then(asyncgit::asyncjob::AsyncSingleJob::progress) { let result = progress.extract_results()?; @@ -205,7 +207,7 @@ impl FileRevlogPopup { let was_empty = self.items.is_empty(); - self.items.extend(result.into_iter()); + self.items.extend(result); if was_empty && !self.items.is_empty() { self.queue @@ -221,8 +223,7 @@ impl FileRevlogPopup { let commit_id = table_state.selected().and_then(|selected| { self.items - .iter() - .nth(selected) + .get(selected) .as_ref() .map(|entry| entry.commit) }); @@ -323,10 +324,7 @@ impl FileRevlogPopup { .collect() } - fn move_selection( - &mut self, - scroll_type: ScrollType, - ) -> Result<()> { + fn move_selection(&mut self, scroll_type: ScrollType) { let old_selection = self.table_state.get_mut().selected().unwrap_or(0); let max_selection = self.items.len().saturating_sub(1); @@ -353,8 +351,6 @@ impl FileRevlogPopup { } self.set_selection(new_selection); - - Ok(()) } fn set_selection(&mut self, selection: usize) { @@ -416,9 +412,7 @@ impl FileRevlogPopup { // 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), - ) + .with_selected(table_state.selected()) .with_offset(table_state.offset()); f.render_widget(Clear, area); @@ -541,12 +535,12 @@ impl Component for FileRevlogPopup { } } else if key_match(key, self.key_config.keys.move_up) { - self.move_selection(ScrollType::Up)?; + self.move_selection(ScrollType::Up); } else if key_match( key, self.key_config.keys.move_down, ) { - self.move_selection(ScrollType::Down)?; + self.move_selection(ScrollType::Down); } else if key_match( key, self.key_config.keys.shift_up, @@ -554,7 +548,7 @@ impl Component for FileRevlogPopup { key, self.key_config.keys.home, ) { - self.move_selection(ScrollType::Home)?; + self.move_selection(ScrollType::Home); } else if key_match( key, self.key_config.keys.shift_down, @@ -562,15 +556,15 @@ impl Component for FileRevlogPopup { key, self.key_config.keys.end, ) { - self.move_selection(ScrollType::End)?; + self.move_selection(ScrollType::End); } else if key_match(key, self.key_config.keys.page_up) { - self.move_selection(ScrollType::PageUp)?; + self.move_selection(ScrollType::PageUp); } else if key_match( key, self.key_config.keys.page_down, ) { - self.move_selection(ScrollType::PageDown)?; + self.move_selection(ScrollType::PageDown); } } From c7d96c32a6a498c2af9696e09fb3cacdfb655d8d Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Tue, 1 Apr 2025 01:03:24 +0200 Subject: [PATCH 08/10] Open diff from correct commit --- src/popups/file_revlog.rs | 61 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index d75db3f5de..c5e3719f15 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -157,33 +157,29 @@ impl FileRevlogPopup { pub fn update_diff(&mut self) -> Result<()> { if self.is_visible() { - if let Some(commit_id) = self.selected_commit() { - if let Some(open_request) = &self.open_request { - let diff_params = DiffParams { - path: open_request.file_path.clone(), - diff_type: DiffType::Commit(commit_id), - options: self.options.borrow().diff_options(), - }; - - if let Some((params, last)) = - self.git_diff.last()? - { - if params == diff_params { - self.diff.update( - open_request.file_path.to_string(), - false, - last, - ); - - return Ok(()); - } + if let Some(item) = self.selected_item() { + let diff_params = DiffParams { + path: item.file_path.clone(), + diff_type: DiffType::Commit(item.commit), + options: self.options.borrow().diff_options(), + }; + + if let Some((params, last)) = self.git_diff.last()? { + if params == diff_params { + self.diff.update( + item.file_path.clone(), + false, + last, + ); + + return Ok(()); } + } - self.git_diff.request(diff_params)?; - self.diff.clear(true); + self.git_diff.request(diff_params)?; + self.diff.clear(true); - return Ok(()); - } + return Ok(()); } self.diff.clear(false); @@ -218,19 +214,20 @@ impl FileRevlogPopup { Ok(()) } - fn selected_commit(&self) -> Option { + fn selected_item(&self) -> Option<&FileHistoryEntry> { let table_state = self.table_state.take(); - let commit_id = table_state.selected().and_then(|selected| { - self.items - .get(selected) - .as_ref() - .map(|entry| entry.commit) - }); + let item = table_state + .selected() + .and_then(|selected| self.items.get(selected)); self.table_state.set(table_state); - commit_id + item + } + + fn selected_commit(&self) -> Option { + Some(self.selected_item()?.commit) } fn can_focus_diff(&self) -> bool { From 33a87f2700b71774af57f4c6c72d5c3da3c05270 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Tue, 1 Apr 2025 10:36:43 +0200 Subject: [PATCH 09/10] Blame from file_revlog with correct file --- src/popups/file_revlog.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index c5e3719f15..06101e4866 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -516,15 +516,19 @@ impl Component for FileRevlogPopup { )); } } else if key_match(key, self.key_config.keys.blame) { - if let Some(open_request) = - self.open_request.clone() + if let Some(selected_item) = + self.selected_item().map(ToOwned::to_owned) { self.hide_stacked(true); self.queue.push(InternalEvent::OpenPopup( StackablePopupOpen::BlameFile( BlameFileOpen { - file_path: open_request.file_path, - commit_id: self.selected_commit(), + file_path: selected_item + .file_path + .clone(), + commit_id: Some( + selected_item.commit, + ), selection: None, }, ), From c1d40f33f6f16358813ae36d39c5ce0282b98f69 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Tue, 15 Apr 2025 17:38:28 +0200 Subject: [PATCH 10/10] Remove unused --- src/components/utils/logitems.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index b8e39df078..d83a520390 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -64,12 +64,6 @@ impl LogEntry { Self::time_as_string(self.time, now) } - pub fn timestamp_to_datetime( - time: i64, - ) -> Option> { - Some(DateTime::<_>::from(DateTime::from_timestamp(time, 0)?)) - } - /// pub fn time_as_string( time: DateTime,