mirror of
https://github.com/gitui-org/gitui
synced 2026-05-22 16:38:28 +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};
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
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 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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
// ¤t_file_path
|
||||
// );
|
||||
|
||||
if matches!(delta, git2::Delta::Added) {
|
||||
let rename = commit_detect_file_rename(
|
||||
repo,
|
||||
*commit_id,
|
||||
current_file_path.as_str(),
|
||||
)?;
|
||||
|
||||
if let Some(old_name) = rename {
|
||||
// log::info!(
|
||||
// "rename: [{}] {:?} <- {:?}",
|
||||
// commit_id.get_short_string(),
|
||||
// current_file_path,
|
||||
// old_name,
|
||||
// );
|
||||
|
||||
(*file_path.write()?) = old_name;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
///
|
||||
pub struct SearchFields: u32 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue