diff --git a/Cargo.lock b/Cargo.lock index e27c7e81..e266bd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,17 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +[[package]] +name = "asyncgit" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "git2", + "log", + "rayon", + "scopetime", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -94,6 +105,52 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "crossbeam-channel" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +dependencies = [ + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -229,6 +286,8 @@ dependencies = [ name = "gitui" version = "0.1.0" dependencies = [ + "asyncgit", + "crossbeam-channel", "crossterm 0.15.0", "dirs", "git2", @@ -239,6 +298,15 @@ dependencies = [ "tui", ] +[[package]] +name = "hermit-abi" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +dependencies = [ + "libc", +] + [[package]] name = "idna" version = "0.2.0" @@ -372,6 +440,21 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memoffset" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +dependencies = [ + "autocfg", +] + [[package]] name = "mio" version = "0.6.21" @@ -433,6 +516,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "openssl-probe" version = "0.1.2" @@ -488,6 +581,30 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +[[package]] +name = "rayon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.1.56" diff --git a/Cargo.toml b/Cargo.toml index 03308909..60b399a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ itertools = "0.9" log = "0.4" simplelog = "0.7" dirs = "2.0" +crossbeam-channel = "0.4" scopetime = { path = "./scopetime" } +asyncgit = { path = "./asyncgit" } tui = { version = "0.8", default-features=false, features = ['crossterm'] } [features] diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml new file mode 100644 index 00000000..c77f3acf --- /dev/null +++ b/asyncgit/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "asyncgit" +version = "0.1.0" +authors = ["Stephan Dilly "] +edition = "2018" + +[dependencies] +git2 = "0.10" +rayon = "1.3" +crossbeam-channel = "0.4" +log = "0.4" +scopetime = { path = "../scopetime" } \ No newline at end of file diff --git a/asyncgit/src/diff.rs b/asyncgit/src/diff.rs new file mode 100644 index 00000000..ae9297b7 --- /dev/null +++ b/asyncgit/src/diff.rs @@ -0,0 +1,108 @@ +use git2::{ + DiffFormat, DiffOptions, Repository, RepositoryOpenFlags, +}; +use scopetime::scope_time; +use std::path::Path; + +/// +#[derive(Copy, Clone, PartialEq)] +pub enum DiffLineType { + None, + Header, + Add, + Delete, +} + +impl Default for DiffLineType { + fn default() -> Self { + DiffLineType::None + } +} + +/// +#[derive(Default, PartialEq, Clone)] +pub struct DiffLine { + pub content: String, + pub line_type: DiffLineType, +} + +/// +#[derive(Default, PartialEq, Clone)] +pub struct Diff(pub Vec); + +/// +pub fn get_diff(p: String, stage: bool) -> Diff { + scope_time!("get_diff"); + + let repo = repo(); + + let mut opt = DiffOptions::new(); + opt.pathspec(p); + + let diff = if !stage { + // diff against stage + repo.diff_index_to_workdir(None, Some(&mut opt)).unwrap() + } else { + // diff against head + let ref_head = repo.head().unwrap(); + let parent = + repo.find_commit(ref_head.target().unwrap()).unwrap(); + let tree = parent.tree().unwrap(); + repo.diff_tree_to_index( + Some(&tree), + Some(&repo.index().unwrap()), + Some(&mut opt), + ) + .unwrap() + }; + + let mut res = Vec::new(); + + diff.print(DiffFormat::Patch, |_delta, _hunk, line| { + let origin = line.origin(); + + if origin != 'F' { + let line_type = match origin { + 'H' => DiffLineType::Header, + '<' | '-' => DiffLineType::Delete, + '>' | '+' => DiffLineType::Add, + _ => DiffLineType::None, + }; + + let diff_line = DiffLine { + content: String::from_utf8_lossy(line.content()) + .to_string(), + line_type, + }; + + if line_type == DiffLineType::Header && res.len() > 0 { + res.push(DiffLine { + content: "\n".to_string(), + line_type: DiffLineType::None, + }); + } + + res.push(diff_line); + } + true + }) + .unwrap(); + + Diff(res) +} + +/// +pub fn repo() -> Repository { + let repo = Repository::open_ext( + "./", + RepositoryOpenFlags::empty(), + Vec::<&Path>::new(), + ) + .unwrap(); + + if repo.is_bare() { + panic!("bare repo") + } + + repo +} diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs new file mode 100644 index 00000000..31ceeea3 --- /dev/null +++ b/asyncgit/src/lib.rs @@ -0,0 +1,73 @@ +mod diff; + +use crossbeam_channel::Sender; +pub use diff::{get_diff, Diff, DiffLine, DiffLineType}; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + sync::{Arc, Mutex}, +}; + +#[derive(Default, Hash)] +struct DiffRequest(String, bool); + +struct Request(R, Option); + +pub struct AsyncDiff { + current: Arc>>, + sender: Sender<()>, +} + +impl AsyncDiff { + /// + pub fn new(sender: Sender<()>) -> Self { + Self { + current: Arc::new(Mutex::new(Request(0, None))), + sender, + } + } + + /// + pub fn request( + &mut self, + file_path: String, + stage: bool, + ) -> Option { + let request = DiffRequest(file_path.clone(), stage); + + let mut hasher = DefaultHasher::new(); + request.hash(&mut hasher); + let hash = hasher.finish(); + + { + let mut current = self.current.lock().unwrap(); + + if current.0 == hash { + return current.1.clone(); + } + + current.0 = hash; + current.1 = None; + } + + let arc_clone = Arc::clone(&self.current); + let sender = self.sender.clone(); + rayon::spawn(move || { + let res = get_diff(file_path.clone(), stage); + let mut notify = false; + { + let mut current = arc_clone.lock().unwrap(); + if current.0 == hash { + current.1 = Some(res); + notify = true; + } + } + + if notify { + sender.send(()).unwrap(); + } + }); + + None + } +} diff --git a/src/app.rs b/src/app.rs index ed7c4c82..d0bd6d67 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,8 @@ use crate::{ }, git_utils, keys, strings, }; +use asyncgit::AsyncDiff; +use crossbeam_channel::Sender; use crossterm::event::Event; use git2::StatusShow; use itertools::Itertools; @@ -41,12 +43,13 @@ pub struct App { index: IndexComponent, index_wd: IndexComponent, diff: DiffComponent, + async_diff: AsyncDiff, } // public interface impl App { /// - pub fn new() -> Self { + pub fn new(sender: Sender<()>) -> Self { Self { focus: Focus::Status, diff_target: DiffTarget::WorkingDir, @@ -63,6 +66,7 @@ impl App { false, ), diff: DiffComponent::default(), + async_diff: AsyncDiff::new(sender), } } @@ -137,6 +141,7 @@ impl App { /// pub fn event(&mut self, ev: Event) { trace!("event: {:?}", ev); + if self.commit.is_visible() && self.commit.event(ev) { if !self.commit.is_visible() { self.update(); @@ -210,7 +215,7 @@ impl App { } impl App { - fn update_diff(&mut self) { + pub fn update_diff(&mut self) { let (idx, is_stage) = match self.diff_target { DiffTarget::Stage => (&self.index, true), DiffTarget::WorkingDir => (&self.index_wd, false), @@ -220,13 +225,13 @@ impl App { let path = i.path; if self.diff.path() != path { - self.diff.update( - path.clone(), - git_utils::get_diff( - Path::new(path.as_str()), - is_stage, - ), - ); + if let Some(diff) = + self.async_diff.request(path.clone(), is_stage) + { + self.diff.update(path.clone(), diff); + } else { + self.diff.clear(); + } } } else { self.diff.clear(); diff --git a/src/components/diff.rs b/src/components/diff.rs index 6cc486fe..d86dd585 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -1,8 +1,8 @@ use crate::{ components::{CommandInfo, Component}, - git_utils::{Diff, DiffLine, DiffLineType}, strings, }; +use asyncgit::{Diff, DiffLine, DiffLineType}; use crossterm::event::{Event, KeyCode}; use tui::{ backend::Backend, diff --git a/src/git_utils.rs b/src/git_utils.rs index b99de9ae..1c3f9386 100644 --- a/src/git_utils.rs +++ b/src/git_utils.rs @@ -1,97 +1,10 @@ use git2::{ - build::CheckoutBuilder, DiffFormat, DiffOptions, IndexAddOption, - ObjectType, Repository, RepositoryOpenFlags, + build::CheckoutBuilder, IndexAddOption, ObjectType, Repository, + RepositoryOpenFlags, }; use scopetime::scope_time; use std::path::Path; -/// -#[derive(Copy, Clone, PartialEq)] -pub enum DiffLineType { - None, - Header, - Add, - Delete, -} - -impl Default for DiffLineType { - fn default() -> Self { - DiffLineType::None - } -} - -/// -#[derive(Default, PartialEq)] -pub struct DiffLine { - pub content: String, - pub line_type: DiffLineType, -} - -/// -#[derive(Default, PartialEq)] -pub struct Diff(pub Vec); - -/// -pub fn get_diff(p: &Path, stage: bool) -> Diff { - scope_time!("get_diff"); - - let repo = repo(); - - let mut opt = DiffOptions::new(); - opt.pathspec(p); - - let diff = if !stage { - // diff against stage - repo.diff_index_to_workdir(None, Some(&mut opt)).unwrap() - } else { - // diff against head - let ref_head = repo.head().unwrap(); - let parent = - repo.find_commit(ref_head.target().unwrap()).unwrap(); - let tree = parent.tree().unwrap(); - repo.diff_tree_to_index( - Some(&tree), - Some(&repo.index().unwrap()), - Some(&mut opt), - ) - .unwrap() - }; - - let mut res = Vec::new(); - - diff.print(DiffFormat::Patch, |_delta, _hunk, line| { - let origin = line.origin(); - - if origin != 'F' { - let line_type = match origin { - 'H' => DiffLineType::Header, - '<' | '-' => DiffLineType::Delete, - '>' | '+' => DiffLineType::Add, - _ => DiffLineType::None, - }; - - let diff_line = DiffLine { - content: String::from_utf8_lossy(line.content()) - .to_string(), - line_type, - }; - - if line_type == DiffLineType::Header && res.len() > 0 { - res.push(DiffLine { - content: "\n".to_string(), - line_type: DiffLineType::None, - }); - } - - res.push(diff_line); - } - true - }) - .unwrap(); - - Diff(res) -} - /// pub fn repo() -> Repository { let repo = Repository::open_ext( diff --git a/src/main.rs b/src/main.rs index b379fc6c..85f02570 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod strings; mod ui; use crate::{app::App, poll::QueueEvent}; +use crossbeam_channel::{select, unbounded}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, terminal::{ @@ -34,20 +35,29 @@ fn main() -> Result<()> { terminal.clear()?; - let mut app = App::new(); + let (tx, rx) = unbounded(); + + let mut app = App::new(tx); let receiver = poll::start_polling_thread(); + app.update(); + loop { - let events = receiver.recv().unwrap(); + let mut events: Vec = Vec::new(); + select! { + recv(receiver) -> inputs => events = inputs.unwrap(), + recv(rx) -> _ => events.push(QueueEvent::AsyncEvent), + } + { scope_time!("loop"); for e in events { - if let QueueEvent::InputEvent(ev) = e { - app.event(ev); - } else { - app.update(); + match e { + QueueEvent::InputEvent(ev) => app.event(ev), + QueueEvent::Tick => app.update(), + QueueEvent::AsyncEvent => app.update_diff(), } } diff --git a/src/poll.rs b/src/poll.rs index fb832690..abf6b288 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -1,6 +1,6 @@ +use crossbeam_channel::{unbounded, Receiver}; use crossterm::event::{self, Event}; use std::{ - sync::mpsc::{self, Receiver}, thread::{self, sleep}, time::{Duration, Instant}, }; @@ -9,6 +9,7 @@ use std::{ #[derive(Clone)] pub enum QueueEvent { Tick, + AsyncEvent, InputEvent(Event), } @@ -22,7 +23,7 @@ static TICK_DURATION: Duration = Duration::from_secs(5); /// Thread 1: /// We will pub fn start_polling_thread() -> Receiver> { - let (tx, rx) = mpsc::channel(); + let (tx, rx) = unbounded(); let tx1 = tx.clone(); thread::spawn(move || {