mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
make libgit diffing async
This commit is contained in:
parent
b040b90ebc
commit
f4dc2de961
10 changed files with 348 additions and 107 deletions
117
Cargo.lock
generated
117
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
12
asyncgit/Cargo.toml
Normal 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
108
asyncgit/src/diff.rs
Normal 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
73
asyncgit/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
23
src/app.rs
23
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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
22
src/main.rs
22
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<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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 || {
|
||||
|
|
|
|||
Loading…
Reference in a new issue