make libgit diffing async

This commit is contained in:
Stephan Dilly 2020-03-22 21:08:48 +01:00
parent b040b90ebc
commit f4dc2de961
10 changed files with 348 additions and 107 deletions

117
Cargo.lock generated
View file

@ -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"

View file

@ -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]

12
asyncgit/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "asyncgit"
version = "0.1.0"
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
edition = "2018"
[dependencies]
git2 = "0.10"
rayon = "1.3"
crossbeam-channel = "0.4"
log = "0.4"
scopetime = { path = "../scopetime" }

108
asyncgit/src/diff.rs Normal file
View file

@ -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<DiffLine>);
///
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
}

73
asyncgit/src/lib.rs Normal file
View file

@ -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, A>(R, Option<A>);
pub struct AsyncDiff {
current: Arc<Mutex<Request<u64, Diff>>>,
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<Diff> {
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
}
}

View file

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

View file

@ -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,

View file

@ -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<DiffLine>);
///
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(

View file

@ -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<QueueEvent> = 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(),
}
}

View file

@ -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<Vec<QueueEvent>> {
let (tx, rx) = mpsc::channel();
let (tx, rx) = unbounded();
let tx1 = tx.clone();
thread::spawn(move || {