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