Add async wrapper to blame

- Rename `self.path` to `self.file_path`.
- Take into account that `draw_scrollbar` subtracts the area’s height
  before calculating the scrollbar’s position.
- Show in title if blame is pending.
This commit is contained in:
Christoph Rüßler 2021-04-25 11:19:08 +02:00 committed by Stephan Dilly
parent ea1415461f
commit 26fbc8650f
4 changed files with 284 additions and 30 deletions

182
asyncgit/src/blame.rs Normal file
View file

@ -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, A>(R, Option<A>);
#[derive(Default, Clone)]
struct LastResult<P, R> {
params: P,
hash: u64,
result: R,
}
///
pub struct AsyncBlame {
current: Arc<Mutex<Request<u64, FileBlame>>>,
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
sender: Sender<AsyncNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncBlame {
///
pub fn new(sender: &Sender<AsyncNotification>) -> 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<Option<(BlameParams, FileBlame)>> {
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<Option<FileBlame>> {
log::trace!("request");
let hash = hash(&params);
{
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<Option<LastResult<BlameParams, FileBlame>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
hash: u64,
) -> Result<bool> {
let file_blame = sync::blame::blame_file(
CWD,
&params.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<Option<BlameParams>> {
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(())
}
}

View file

@ -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 `./`

View file

@ -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()

View file

@ -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<String>,
file_path: Option<String>,
file_blame: Option<FileBlame>,
table_state: std::cell::Cell<TableState>,
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("<no path for blame available>");
let title = self.file_blame.as_ref().map_or_else(
|| {
format!(
"{} -- {} -- <no blame available>",
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 areas 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 dont have access to `table_state.offset`
// (its private), so we use `table_state.selected()` as a
// replacement.
@ -259,6 +245,7 @@ impl BlameFileComponent {
///
pub fn new(
queue: &Queue,
sender: &Sender<AsyncNotification>,
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!(
"{} -- {} -- <waiting for blame>",
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!(
"{} -- {} -- <no blame available>",
self.title, file_path
)
}
_ => format!("{} -- <no blame available>", self.title),
}
}
///
fn get_rows(&self, width: usize) -> Vec<Row> {
if let Some(ref file_blame) = self.file_blame {