mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
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:
parent
ea1415461f
commit
26fbc8650f
4 changed files with 284 additions and 30 deletions
182
asyncgit/src/blame.rs
Normal file
182
asyncgit/src/blame.rs
Normal 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(¶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<Option<LastResult<BlameParams, FileBlame>>>,
|
||||
>,
|
||||
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
|
||||
hash: u64,
|
||||
) -> Result<bool> {
|
||||
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<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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 `./`
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 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<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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue