From 4fdf09fc56073486b090b4f2286dfeafe465b994 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Mon, 23 Mar 2020 11:42:50 +0100 Subject: [PATCH] make git status fetching async --- README.md | 2 +- asyncgit/src/diff.rs | 18 +++---- asyncgit/src/lib.rs | 28 ++++++++++- asyncgit/src/status.rs | 93 +++++++++++++++++++++++++++++++++++++ asyncgit/src/sync/mod.rs | 1 - asyncgit/src/sync/status.rs | 4 +- src/app.rs | 40 +++++++++++----- src/components/index.rs | 20 +++----- src/main.rs | 12 ++--- src/poll.rs | 5 +- 10 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 asyncgit/src/status.rs diff --git a/README.md b/README.md index 0b5c8f1a..17d1ad0d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ gitui * [x] inspect diffs * [x] commit * [x] [input polling in thread](assets/perf_compare.jpg) -* [ ] put libgit calls in threadpool +* [x] async git API for fluid control * [ ] show content of new unstaged files * [ ] discard untracked files (remove) * [ ] (un)staging selected hunks diff --git a/asyncgit/src/diff.rs b/asyncgit/src/diff.rs index 5a52be3f..eae7bee4 100644 --- a/asyncgit/src/diff.rs +++ b/asyncgit/src/diff.rs @@ -1,8 +1,8 @@ -use crate::{sync, Diff}; +use crate::{hash, sync, AsyncNotification, Diff}; use crossbeam_channel::Sender; +use log::trace; use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, + hash::Hash, sync::{Arc, Mutex}, }; @@ -13,12 +13,12 @@ struct Request(R, Option); pub struct AsyncDiff { current: Arc>>, - sender: Sender<()>, + sender: Sender, } impl AsyncDiff { /// - pub fn new(sender: Sender<()>) -> Self { + pub fn new(sender: Sender) -> Self { Self { current: Arc::new(Mutex::new(Request(0, None))), sender, @@ -31,11 +31,11 @@ impl AsyncDiff { file_path: String, stage: bool, ) -> Option { + trace!("request"); + let request = DiffRequest(file_path.clone(), stage); - let mut hasher = DefaultHasher::new(); - request.hash(&mut hasher); - let hash = hasher.finish(); + let hash = hash(&request); { let mut current = self.current.lock().unwrap(); @@ -62,7 +62,7 @@ impl AsyncDiff { } if notify { - sender.send(()).unwrap(); + sender.send(AsyncNotification::Diff).unwrap(); } }); diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index 28d6c397..814f542c 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -1,10 +1,36 @@ mod diff; +mod status; pub mod sync; pub use crate::{ diff::AsyncDiff, + status::AsyncStatus, sync::{ diff::{Diff, DiffLine, DiffLineType}, - status::{StatusItem, StatusItemType, StatusType}, + status::{StatusItem, StatusItemType}, }, }; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Copy, Clone, Debug)] +pub enum AsyncNotification { + Status, + Diff, +} + +pub fn hash(v: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + v.hash(&mut hasher); + hasher.finish() +} + +pub fn current_tick() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 +} diff --git a/asyncgit/src/status.rs b/asyncgit/src/status.rs new file mode 100644 index 00000000..5a2975a1 --- /dev/null +++ b/asyncgit/src/status.rs @@ -0,0 +1,93 @@ +use crate::{hash, sync, AsyncNotification, StatusItem}; +use crossbeam_channel::Sender; +use log::trace; +use std::{ + hash::Hash, + sync::{Arc, Mutex}, +}; +use sync::status::StatusType; + +#[derive(Default, Hash, Clone)] +pub struct Status { + pub work_dir: Vec, + pub stage: Vec, +} + +struct Request(R, Option); + +/// +pub struct AsyncStatus { + current: Arc>>, + last: Arc>, + sender: Sender, +} + +impl AsyncStatus { + /// + pub fn new(sender: Sender) -> Self { + Self { + current: Arc::new(Mutex::new(Request(0, None))), + last: Arc::new(Mutex::new(Status::default())), + sender, + } + } + + /// + pub fn last(&mut self) -> Status { + let last = self.last.lock().unwrap(); + last.clone() + } + + /// + pub fn fetch(&mut self, request: u64) -> Option { + let hash_request = hash(&request); + + trace!("request: {} [hash: {}]", request, hash_request); + + { + let mut current = self.current.lock().unwrap(); + + if current.0 == hash_request { + return current.1.clone(); + } + + current.0 = hash_request; + current.1 = None; + } + + let arc_current = Arc::clone(&self.current); + let arc_last = Arc::clone(&self.last); + let sender = self.sender.clone(); + rayon_core::spawn(move || { + let res = Self::get_status(); + trace!("status fetched: {}", hash(&res)); + let mut notify = false; + { + let mut current = arc_current.lock().unwrap(); + if current.0 == hash_request { + current.1 = Some(res.clone()); + notify = true; + } + } + + { + let mut last = arc_last.lock().unwrap(); + *last = res; + } + + if notify { + sender.send(AsyncNotification::Status).unwrap(); + } + }); + + None + } + + fn get_status() -> Status { + let work_dir = + sync::status::get_index(StatusType::WorkingDir); + let stage = sync::status::get_index(StatusType::Stage); + + Status { stage, work_dir } + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 91cef5f5..18c19ab0 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -2,5 +2,4 @@ pub mod diff; pub mod status; pub mod utils; -pub use status::get_index; pub use utils::{commit, index_reset, stage_add, stage_reset}; diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 57772de2..e4b15d61 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -2,7 +2,7 @@ use crate::sync::utils; use git2::{Status, StatusOptions, StatusShow}; use scopetime::scope_time; -#[derive(PartialEq, Copy, Clone)] +#[derive(Copy, Clone, Hash)] pub enum StatusItemType { New, Modified, @@ -28,7 +28,7 @@ impl From for StatusItemType { } /// -#[derive(Default, PartialEq, Clone)] +#[derive(Default, Clone, Hash)] pub struct StatusItem { pub path: String, pub status: Option, diff --git a/src/app.rs b/src/app.rs index 7e94fb2b..81714272 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,9 @@ use crate::{ }, keys, strings, }; -use asyncgit::{sync, AsyncDiff, StatusType}; +use asyncgit::{ + current_tick, sync, AsyncDiff, AsyncNotification, AsyncStatus, +}; use crossbeam_channel::Sender; use crossterm::event::Event; use itertools::Itertools; @@ -43,12 +45,13 @@ pub struct App { index_wd: IndexComponent, diff: DiffComponent, async_diff: AsyncDiff, + async_status: AsyncStatus, } // public interface impl App { /// - pub fn new(sender: Sender<()>) -> Self { + pub fn new(sender: Sender) -> Self { Self { focus: Focus::Status, diff_target: DiffTarget::WorkingDir, @@ -56,16 +59,12 @@ impl App { commit: CommitComponent::default(), index_wd: IndexComponent::new( strings::TITLE_STATUS, - StatusType::WorkingDir, true, ), - index: IndexComponent::new( - strings::TITLE_INDEX, - StatusType::Stage, - false, - ), + index: IndexComponent::new(strings::TITLE_INDEX, false), diff: DiffComponent::default(), - async_diff: AsyncDiff::new(sender), + async_diff: AsyncDiff::new(sender.clone()), + async_status: AsyncStatus::new(sender), } } @@ -200,10 +199,27 @@ impl App { /// pub fn update(&mut self) { - trace!("app::update"); + trace!("update"); - self.index.update(); - self.index_wd.update(); + self.update_diff(); + + self.async_status.fetch(current_tick()); + } + + /// + pub fn update_git(&mut self, ev: AsyncNotification) { + trace!("update_git: {:?}", ev); + match ev { + AsyncNotification::Diff => self.update_diff(), + AsyncNotification::Status => self.update_status(), + } + } + + /// + pub fn update_status(&mut self) { + let status = self.async_status.last(); + self.index.update(&status.stage); + self.index_wd.update(&status.work_dir); self.update_diff(); } diff --git a/src/components/index.rs b/src/components/index.rs index 828b2735..e5214e3c 100644 --- a/src/components/index.rs +++ b/src/components/index.rs @@ -1,6 +1,6 @@ use crate::components::{CommandInfo, Component}; use crate::ui; -use asyncgit::{sync, StatusItem, StatusItemType, StatusType}; +use asyncgit::{hash, StatusItem, StatusItemType}; use crossterm::event::{Event, KeyCode}; use std::{borrow::Cow, cmp}; use tui::{ @@ -15,7 +15,6 @@ use tui::{ pub struct IndexComponent { title: String, items: Vec, - index_type: StatusType, selection: Option, focused: bool, show_selection: bool, @@ -23,26 +22,21 @@ pub struct IndexComponent { impl IndexComponent { /// - pub fn new( - title: &str, - index_type: StatusType, - focus: bool, - ) -> Self { + pub fn new(title: &str, focus: bool) -> Self { Self { title: title.to_string(), items: Vec::new(), - index_type, + selection: None, focused: focus, show_selection: focus, } } - /// - pub fn update(&mut self) { - let new_status = sync::get_index(self.index_type.into()); - if self.items != new_status { - self.items = new_status; + /// + pub fn update(&mut self, list: &Vec) { + if hash(&self.items) != hash(list) { + self.items = list.clone(); self.selection = if self.items.len() > 0 { Some(0) } else { None }; diff --git a/src/main.rs b/src/main.rs index d1931be4..4c4b8251 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,19 +33,19 @@ fn main() -> Result<()> { terminal.clear()?; - let (tx, rx) = unbounded(); + let (tx_git, rx_git) = unbounded(); - let mut app = App::new(tx); + let mut app = App::new(tx_git); - let receiver = poll::start_polling_thread(); + let rx_input = poll::start_polling_thread(); app.update(); loop { let mut events: Vec = Vec::new(); select! { - recv(receiver) -> inputs => events = inputs.unwrap(), - recv(rx) -> _ => events.push(QueueEvent::AsyncEvent), + recv(rx_input) -> inputs => events.append(&mut inputs.unwrap()), + recv(rx_git) -> ev => events.push(QueueEvent::GitEvent(ev.unwrap())), } { @@ -55,7 +55,7 @@ fn main() -> Result<()> { match e { QueueEvent::InputEvent(ev) => app.event(ev), QueueEvent::Tick => app.update(), - QueueEvent::AsyncEvent => app.update_diff(), + QueueEvent::GitEvent(ev) => app.update_git(ev), } } diff --git a/src/poll.rs b/src/poll.rs index abf6b288..d6ab2b7a 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -1,3 +1,4 @@ +use asyncgit::AsyncNotification; use crossbeam_channel::{unbounded, Receiver}; use crossterm::event::{self, Event}; use std::{ @@ -6,10 +7,10 @@ use std::{ }; /// -#[derive(Clone)] +#[derive(Clone, Copy)] pub enum QueueEvent { Tick, - AsyncEvent, + GitEvent(AsyncNotification), InputEvent(Event), }