diff --git a/asyncgit/src/blame.rs b/asyncgit/src/blame.rs new file mode 100644 index 00000000..e8570e5f --- /dev/null +++ b/asyncgit/src/blame.rs @@ -0,0 +1,182 @@ +use crate::{ + error::Result, + hash, + sync::{self, BlameAt, FileBlame}, + AsyncNotification, CWD, +}; +use crossbeam_channel::Sender; +use std::{ + hash::Hash, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +/// +#[derive(Hash, Clone, PartialEq)] +pub struct BlameParams { + /// path to the file to blame + pub file_path: String, +} + +struct Request(R, Option); + +#[derive(Default, Clone)] +struct LastResult { + params: P, + hash: u64, + result: R, +} + +/// +pub struct AsyncBlame { + current: Arc>>, + last: Arc>>>, + sender: Sender, + pending: Arc, +} + +impl AsyncBlame { + /// + pub fn new(sender: &Sender) -> Self { + Self { + current: Arc::new(Mutex::new(Request(0, None))), + last: Arc::new(Mutex::new(None)), + sender: sender.clone(), + pending: Arc::new(AtomicUsize::new(0)), + } + } + + /// + pub fn last( + &mut self, + ) -> Result> { + let last = self.last.lock()?; + + Ok(last.clone().map(|last_result| { + (last_result.params, last_result.result) + })) + } + + /// + pub fn refresh(&mut self) -> Result<()> { + if let Ok(Some(param)) = self.get_last_param() { + self.clear_current()?; + self.request(param)?; + } + Ok(()) + } + + /// + pub fn is_pending(&self) -> bool { + self.pending.load(Ordering::Relaxed) > 0 + } + + /// + pub fn request( + &mut self, + params: BlameParams, + ) -> Result> { + log::trace!("request"); + + let hash = hash(¶ms); + + { + let mut current = self.current.lock()?; + + if current.0 == hash { + return Ok(current.1.clone()); + } + + current.0 = hash; + current.1 = None; + } + + let arc_current = Arc::clone(&self.current); + let arc_last = Arc::clone(&self.last); + let sender = self.sender.clone(); + let arc_pending = Arc::clone(&self.pending); + + self.pending.fetch_add(1, Ordering::Relaxed); + + rayon_core::spawn(move || { + let notify = Self::get_blame_helper( + params, + &arc_last, + &arc_current, + hash, + ); + + let notify = match notify { + Err(err) => { + log::error!("get_blame_helper error: {}", err); + true + } + Ok(notify) => notify, + }; + + arc_pending.fetch_sub(1, Ordering::Relaxed); + + sender + .send(if notify { + AsyncNotification::Blame + } else { + AsyncNotification::FinishUnchanged + }) + .expect("error sending blame"); + }); + + Ok(None) + } + + fn get_blame_helper( + params: BlameParams, + arc_last: &Arc< + Mutex>>, + >, + arc_current: &Arc>>, + hash: u64, + ) -> Result { + let file_blame = sync::blame::blame_file( + CWD, + ¶ms.file_path, + &BlameAt::Head, + )?; + + let mut notify = false; + { + let mut current = arc_current.lock()?; + if current.0 == hash { + current.1 = Some(file_blame.clone()); + notify = true; + } + } + + { + let mut last = arc_last.lock()?; + *last = Some(LastResult { + result: file_blame, + hash, + params, + }); + } + + Ok(notify) + } + + fn get_last_param(&self) -> Result> { + Ok(self + .last + .lock()? + .clone() + .map(|last_result| last_result.params)) + } + + fn clear_current(&mut self) -> Result<()> { + let mut current = self.current.lock()?; + current.0 = 0; + current.1 = None; + Ok(()) + } +} diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index c82623db..1ed4a9cc 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -20,6 +20,7 @@ //TODO: get this in someday since expect still leads us to crashes sometimes // #![deny(clippy::expect_used)] +mod blame; pub mod cached; mod commit_files; mod diff; @@ -35,6 +36,7 @@ pub mod sync; mod tags; pub use crate::{ + blame::{AsyncBlame, BlameParams}, commit_files::AsyncCommitFiles, diff::{AsyncDiff, DiffParams, DiffType}, fetch::{AsyncFetch, FetchRequest}, @@ -75,6 +77,8 @@ pub enum AsyncNotification { PushTags, /// Fetch, + /// + Blame, } /// current working directory `./` diff --git a/src/app.rs b/src/app.rs index 354013e8..faea4f8e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -96,6 +96,7 @@ impl App { ), blame_file_popup: BlameFileComponent::new( &queue, + sender, &strings::blame_title(&key_config), theme.clone(), key_config.clone(), @@ -322,6 +323,7 @@ impl App { self.status_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?; self.revlog.update_git(ev)?; + self.blame_file_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?; self.push_popup.update_git(ev)?; self.push_tags_popup.update_git(ev)?; @@ -344,6 +346,7 @@ impl App { self.status_tab.anything_pending() || self.revlog.any_work_pending() || self.stashing_tab.anything_pending() + || self.blame_file_popup.any_work_pending() || self.inspect_commit_popup.any_work_pending() || self.input.is_state_changing() || self.push_popup.any_work_pending() diff --git a/src/components/blame_file.rs b/src/components/blame_file.rs index cba2458e..ae234c9a 100644 --- a/src/components/blame_file.rs +++ b/src/components/blame_file.rs @@ -11,9 +11,10 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ - sync::{blame_file, BlameAt, BlameHunk, CommitId, FileBlame}, - CWD, + sync::{BlameHunk, CommitId, FileBlame}, + AsyncBlame, AsyncNotification, BlameParams, }; +use crossbeam_channel::Sender; use crossterm::event::Event; use std::convert::TryInto; use tui::{ @@ -29,8 +30,9 @@ pub struct BlameFileComponent { title: String, theme: SharedTheme, queue: Queue, + async_blame: AsyncBlame, visible: bool, - path: Option, + file_path: Option, file_blame: Option, table_state: std::cell::Cell, key_config: SharedKeyConfig, @@ -66,27 +68,7 @@ impl DrawableComponent for BlameFileComponent { area: Rect, ) -> Result<()> { if self.is_visible() { - let path: &str = self - .path - .as_deref() - .unwrap_or(""); - - let title = self.file_blame.as_ref().map_or_else( - || { - format!( - "{} -- {} -- ", - self.title, path - ) - }, - |file_blame| { - format!( - "{} -- {} -- {}", - self.title, - path, - file_blame.commit_id.get_short_string() - ) - }, - ); + let title = self.get_title(); let rows = self.get_rows(area.width.into()); let author_width = get_author_width(area.width.into()); @@ -131,7 +113,11 @@ impl DrawableComponent for BlameFileComponent { f, area, &self.theme, - number_of_rows, + // April 2021: `draw_scrollbar` assumes that the last parameter + // is `scroll_top`. Therefore, it subtracts the area’s height + // before calculating the position of the scrollbar. To account + // for that, we add the current height. + number_of_rows + (area.height as usize), // April 2021: we don’t have access to `table_state.offset` // (it’s private), so we use `table_state.selected()` as a // replacement. @@ -259,6 +245,7 @@ impl BlameFileComponent { /// pub fn new( queue: &Queue, + sender: &Sender, title: &str, theme: SharedTheme, key_config: SharedKeyConfig, @@ -266,9 +253,10 @@ impl BlameFileComponent { Self { title: String::from(title), theme, + async_blame: AsyncBlame::new(sender), queue: queue.clone(), visible: false, - path: None, + file_path: None, file_blame: None, table_state: std::cell::Cell::new(TableState::default()), key_config, @@ -277,16 +265,93 @@ impl BlameFileComponent { } /// - pub fn open(&mut self, path: &str) -> Result<()> { - self.path = Some(path.into()); - self.file_blame = blame_file(CWD, path, &BlameAt::Head).ok(); + pub fn open(&mut self, file_path: &str) -> Result<()> { + self.file_path = Some(file_path.into()); + self.file_blame = None; self.table_state.get_mut().select(Some(0)); - self.show()?; + self.update()?; + Ok(()) } + /// + pub fn any_work_pending(&self) -> bool { + self.async_blame.is_pending() + } + + /// + pub fn update_git( + &mut self, + event: AsyncNotification, + ) -> Result<()> { + if self.is_visible() { + if let AsyncNotification::Blame = event { + self.update()? + } + } + + Ok(()) + } + + fn update(&mut self) -> Result<()> { + if self.is_visible() { + if let Some(file_path) = &self.file_path { + let blame_params = BlameParams { + file_path: file_path.into(), + }; + + if let Some(( + previous_blame_params, + last_file_blame, + )) = self.async_blame.last()? + { + if previous_blame_params == blame_params { + self.file_blame = Some(last_file_blame); + + return Ok(()); + } + } + + self.async_blame.request(blame_params)?; + } + } + + Ok(()) + } + + /// + fn get_title(&self) -> String { + match ( + self.any_work_pending(), + self.file_path.as_ref(), + self.file_blame.as_ref(), + ) { + (true, Some(file_path), _) => { + format!( + "{} -- {} -- ", + self.title, file_path + ) + } + (false, Some(file_path), Some(file_blame)) => { + format!( + "{} -- {} -- {}", + self.title, + file_path, + file_blame.commit_id.get_short_string() + ) + } + (false, Some(file_path), None) => { + format!( + "{} -- {} -- ", + self.title, file_path + ) + } + _ => format!("{} -- ", self.title), + } + } + /// fn get_rows(&self, width: usize) -> Vec { if let Some(ref file_blame) = self.file_blame {