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};
/// 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<T: Copy + Send, P: Clone + Send + Sync + PartialEq>
true
})
}
///
pub fn progress(&self) -> P {
self.progress.read().cl
}
}
/// 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 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

View file

@ -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 {

View file

@ -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]

View file

@ -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<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! {
///
pub struct SearchFields: u32 {

View file

@ -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<Oid> 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<CommitInfo> {
let commit = repo.find_commit((*commit_id).into())?;
let author = commit.author();

View file

@ -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<TimeOrderedCommit<'a>>,
visited: HashSet<Oid>,
limit: usize,
limit: Option<usize>,
repo: &'a Repository,
filter: Option<SharedCommitFilterFn>,
}
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 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;
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<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]
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);

View file

@ -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<CommitId> {
let mut commit_ids = Vec::<CommitId>::new();
LogWalker::new(r, max_count)
let commit_ids = RefCell::new(Vec::<CommitId>::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 {

View file

@ -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<AsyncLog>,
git_history: Option<AsyncSingleJob<AsyncFileHistoryJob>>,
git_diff: AsyncDiff,
theme: SharedTheme,
queue: Queue,
@ -62,7 +58,7 @@ pub struct FileRevlogComponent {
repo_path: RepoPathRef,
open_request: Option<FileRevOpen>,
table_state: std::cell::Cell<TableState>,
items: ItemBatch,
items: Vec<FileHistoryEntry>,
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);
pub fn update_list(&mut self) -> Result<()> {
let is_pending = self
.git_history
.as_ref()
.map(|git| git.is_pending())
.unwrap_or_default();
let commits = get_commits_info(
&self.repo_path.borrow(),
&git_log.get_slice(new_offset, amount)?,
self.current_width.get(),
);
if is_pending {
if let Some(progress) = self
.git_history
.as_ref()
.and_then(|job| job.progress())
{
let result = progress.extract_results()?;
if let Ok(commits) = commits {
self.items.set_items(new_offset, commits, &None);
log::info!(
"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(())
@ -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(
"<no history available>".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<usize> {
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(

View file

@ -26,18 +26,11 @@ impl From<CommitInfo> 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::<Local>::from(
DateTime::<Utc>::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<CommitInfo> for LogEntry {
impl LogEntry {
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) {
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()
}
}
}