diff --git a/Cargo.lock b/Cargo.lock
index d238ba77..39f4d868 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -293,8 +293,7 @@ dependencies = [
[[package]]
name = "git2"
version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef222034f2069cfc5af01ce423574d3d9a3925bd4052912a14e5bcfd7ca9e47a"
+source = "git+https://github.com/rust-lang/git2-rs.git?rev=617499d7fcf315cf92faa1ffde425666d3edd500#617499d7fcf315cf92faa1ffde425666d3edd500"
dependencies = [
"bitflags",
"libc",
@@ -404,8 +403,7 @@ checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0"
[[package]]
name = "libgit2-sys"
version = "0.12.2+1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12c878ccc1a49ff71e264233a66d2114cdcc7fdc44c0ebe2b54075240831238"
+source = "git+https://github.com/rust-lang/git2-rs.git?rev=617499d7fcf315cf92faa1ffde425666d3edd500#617499d7fcf315cf92faa1ffde425666d3edd500"
dependencies = [
"cc",
"libc",
diff --git a/Makefile b/Makefile
index ff1d5109..961ee9db 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,9 @@
debug:
GITUI_LOGGING=true cargo run --features=timing
+build-release:
+ cargo build --release
+
test:
cargo test --workspace
diff --git a/README.md b/README.md
index 9755cb5b..881f7fb9 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,8 @@ to enable logging to `~/.gitui/gitui.log`:
GITUI_LOGGING=true gitui
```
-# todo for 0.1 (first release)
+# todo for 0.2 (first release)
-* [ ] make staging/unstaging async
-* [ ] (un)staging selected hunks
* [ ] publish as homebrew-tap
# inspiration
diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml
index bd805283..8202507a 100644
--- a/asyncgit/Cargo.toml
+++ b/asyncgit/Cargo.toml
@@ -10,7 +10,8 @@ license = "MIT"
categories = ["concurrency","asynchronous"]
[dependencies]
-git2 = "0.13"
+# git2 = "0.13"
+git2 = { git = "https://github.com/rust-lang/git2-rs.git", rev = "617499d7fcf315cf92faa1ffde425666d3edd500" }
rayon-core = "1.7"
crossbeam-channel = "0.4"
log = "0.4"
diff --git a/asyncgit/src/diff.rs b/asyncgit/src/diff.rs
index 07d60445..1c45e05c 100644
--- a/asyncgit/src/diff.rs
+++ b/asyncgit/src/diff.rs
@@ -1,4 +1,4 @@
-use crate::{hash, sync, AsyncNotification, Diff, CWD};
+use crate::{hash, sync, AsyncNotification, FileDiff, CWD};
use crossbeam_channel::Sender;
use log::trace;
use std::{
@@ -21,8 +21,8 @@ struct LastResult
{
///
pub struct AsyncDiff {
- current: Arc>>,
- last: Arc>>>,
+ current: Arc>>,
+ last: Arc>>>,
sender: Sender,
}
@@ -37,7 +37,7 @@ impl AsyncDiff {
}
///
- pub fn last(&mut self) -> Option {
+ pub fn last(&mut self) -> Option {
let last = self.last.lock().unwrap();
if let Some(res) = last.clone() {
Some(res.result)
@@ -55,7 +55,10 @@ impl AsyncDiff {
}
///
- pub fn request(&mut self, params: DiffParams) -> Option {
+ pub fn request(
+ &mut self,
+ params: DiffParams,
+ ) -> Option {
trace!("request");
let hash = hash(¶ms);
diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs
index 29e28263..c1705813 100644
--- a/asyncgit/src/lib.rs
+++ b/asyncgit/src/lib.rs
@@ -12,7 +12,7 @@ pub use crate::{
diff::{AsyncDiff, DiffParams},
status::AsyncStatus,
sync::{
- diff::{Diff, DiffLine, DiffLineType},
+ diff::{DiffLine, DiffLineType, FileDiff},
status::{StatusItem, StatusItemType},
utils::is_repo,
},
diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs
index c8664c92..4b37719a 100644
--- a/asyncgit/src/sync/diff.rs
+++ b/asyncgit/src/sync/diff.rs
@@ -1,8 +1,10 @@
//! sync git api for fetching a diff
use super::utils;
+use crate::hash;
use git2::{
- Delta, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
+ Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
+ Repository,
};
use scopetime::scope_time;
use std::{fs, path::Path};
@@ -35,9 +37,8 @@ pub struct DiffLine {
pub line_type: DiffLineType,
}
-///
-#[derive(Default, Clone, Copy, PartialEq)]
-struct HunkHeader {
+#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
+pub(crate) struct HunkHeader {
old_start: u32,
old_lines: u32,
new_start: u32,
@@ -57,20 +58,31 @@ impl From> for HunkHeader {
///
#[derive(Default, Clone, Hash)]
-pub struct Hunk(pub Vec);
+pub struct Hunk {
+ ///
+ pub header_hash: u64,
+ ///
+ pub lines: Vec,
+}
///
#[derive(Default, Clone, Hash)]
-pub struct Diff(pub Vec, pub u16);
-
-///
-pub fn get_diff(repo_path: &str, p: String, stage: bool) -> Diff {
- scope_time!("get_diff");
-
- let repo = utils::repo(repo_path);
+pub struct FileDiff {
+ /// list of hunks
+ pub hunks: Vec,
+ /// lines total summed up over hunks
+ pub lines: u16,
+}
+pub(crate) fn get_diff_raw<'a>(
+ repo: &'a Repository,
+ p: &str,
+ stage: bool,
+ reverse: bool,
+) -> (Diff<'a>, DiffOptions) {
let mut opt = DiffOptions::new();
opt.pathspec(p);
+ opt.reverse(reverse);
let diff = if stage {
// diff against head
@@ -98,13 +110,27 @@ pub fn get_diff(repo_path: &str, p: String, stage: bool) -> Diff {
repo.diff_index_to_workdir(None, Some(&mut opt)).unwrap()
};
- let mut res: Diff = Diff::default();
+ (diff, opt)
+}
+
+///
+pub fn get_diff(repo_path: &str, p: String, stage: bool) -> FileDiff {
+ scope_time!("get_diff");
+
+ let repo = utils::repo(repo_path);
+
+ let (diff, mut opt) = get_diff_raw(&repo, &p, stage, false);
+
+ let mut res: FileDiff = FileDiff::default();
let mut current_lines = Vec::new();
let mut current_hunk: Option = None;
- let mut adder = |lines: &Vec| {
- res.0.push(Hunk(lines.clone()));
- res.1 += lines.len() as u16;
+ let mut adder = |header: &HunkHeader, lines: &Vec| {
+ res.hunks.push(Hunk {
+ header_hash: hash(header),
+ lines: lines.clone(),
+ });
+ res.lines += lines.len() as u16;
};
let mut put = |hunk: Option, line: git2::DiffLine| {
@@ -114,7 +140,7 @@ pub fn get_diff(repo_path: &str, p: String, stage: bool) -> Diff {
match current_hunk {
None => current_hunk = Some(hunk_header),
Some(h) if h != hunk_header => {
- adder(¤t_lines);
+ adder(&h, ¤t_lines);
current_lines.clear();
current_hunk = Some(hunk_header)
}
@@ -184,7 +210,7 @@ pub fn get_diff(repo_path: &str, p: String, stage: bool) -> Diff {
}
if !current_lines.is_empty() {
- adder(¤t_lines);
+ adder(¤t_hunk.unwrap(), ¤t_lines);
}
res
@@ -243,8 +269,8 @@ mod tests {
let diff =
get_diff(repo_path, "foo/bar.txt".to_string(), false);
- assert_eq!(diff.0.len(), 1);
- assert_eq!(diff.0[0].0[1].content, "test\n");
+ assert_eq!(diff.hunks.len(), 1);
+ assert_eq!(diff.hunks[0].lines[1].content, "test\n");
}
#[test]
@@ -270,7 +296,7 @@ mod tests {
true,
);
- assert_eq!(diff.0.len(), 1);
+ assert_eq!(diff.hunks.len(), 1);
}
static HUNK_A: &str = r"
@@ -345,6 +371,6 @@ mod tests {
let res = get_diff(repo_path, "bar.txt".to_string(), false);
- assert_eq!(res.0.len(), 2)
+ assert_eq!(res.hunks.len(), 2)
}
}
diff --git a/asyncgit/src/sync/hunks.rs b/asyncgit/src/sync/hunks.rs
new file mode 100644
index 00000000..3fdda32e
--- /dev/null
+++ b/asyncgit/src/sync/hunks.rs
@@ -0,0 +1,108 @@
+use super::{
+ diff::{get_diff_raw, HunkHeader},
+ utils::repo,
+};
+use crate::hash;
+use git2::{ApplyLocation, ApplyOptions, Diff};
+use log::error;
+use scopetime::scope_time;
+
+///
+pub fn stage_hunk(
+ repo_path: &str,
+ file_path: String,
+ hunk_hash: u64,
+) -> bool {
+ scope_time!("stage_hunk");
+
+ let repo = repo(repo_path);
+
+ let (diff, _) = get_diff_raw(&repo, &file_path, false, false);
+
+ let mut opt = ApplyOptions::new();
+ opt.hunk_callback(|hunk| {
+ let header = HunkHeader::from(hunk.unwrap());
+ hash(&header) == hunk_hash
+ });
+
+ repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))
+ .is_ok()
+}
+
+fn find_hunk_index(diff: &Diff, hunk_hash: u64) -> Option {
+ let mut result = None;
+
+ let mut hunk_count = 0;
+
+ let foreach_result = diff.foreach(
+ &mut |_, _| true,
+ None,
+ Some(&mut |_, hunk| {
+ let header = HunkHeader::from(hunk);
+ if hash(&header) == hunk_hash {
+ result = Some(hunk_count);
+ }
+ hunk_count += 1;
+ true
+ }),
+ None,
+ );
+
+ if foreach_result.is_ok() {
+ result
+ } else {
+ None
+ }
+}
+
+///
+pub fn revert_hunk(
+ repo_path: &str,
+ file_path: String,
+ hunk_hash: u64,
+) -> bool {
+ scope_time!("revert_hunk");
+
+ let repo = repo(repo_path);
+
+ let (diff, _) = get_diff_raw(&repo, &file_path, true, false);
+ let diff_count_positive = diff.deltas().len();
+
+ let hunk_index = find_hunk_index(&diff, hunk_hash);
+
+ if hunk_index.is_none() {
+ error!("hunk not found");
+ return false;
+ }
+
+ let (diff, _) = get_diff_raw(&repo, &file_path, true, true);
+
+ assert_eq!(diff.deltas().len(), diff_count_positive);
+
+ let mut count = 0;
+ {
+ let mut hunk_idx = 0;
+ let mut opt = ApplyOptions::new();
+ opt.hunk_callback(|_hunk| {
+ let res = if hunk_idx == hunk_index.unwrap() {
+ count += 1;
+ true
+ } else {
+ false
+ };
+
+ hunk_idx += 1;
+
+ res
+ });
+ if repo
+ .apply(&diff, ApplyLocation::Index, Some(&mut opt))
+ .is_err()
+ {
+ error!("apply failed");
+ return false;
+ }
+ }
+
+ count == 1
+}
diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs
index c1779406..b35f4b8f 100644
--- a/asyncgit/src/sync/mod.rs
+++ b/asyncgit/src/sync/mod.rs
@@ -1,10 +1,12 @@
//! sync git api
pub mod diff;
+mod hunks;
mod reset;
pub mod status;
pub mod utils;
+pub use hunks::{revert_hunk, stage_hunk};
pub use reset::{reset_stage, reset_workdir};
pub use utils::{commit, stage_add};
diff --git a/src/app.rs b/src/app.rs
index fe4f809c..3bf613a9 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -82,7 +82,7 @@ impl App {
false,
queue.clone(),
),
- diff: DiffComponent::default(),
+ diff: DiffComponent::new(queue.clone()),
git_diff: AsyncDiff::new(sender.clone()),
git_status: AsyncStatus::new(sender),
current_commands: Vec::new(),
@@ -216,13 +216,7 @@ impl App {
// private impls
impl App {
fn update_diff(&mut self) {
- let (idx, is_stage) = match self.diff_target {
- DiffTarget::Stage => (&self.index, true),
- DiffTarget::WorkingDir => (&self.index_wd, false),
- };
-
- if let Some(i) = idx.selection() {
- let path = i.path;
+ if let Some((path, is_stage)) = self.selected_path() {
let diff_params = DiffParams(path.clone(), is_stage);
if self.diff.current() == (path.clone(), is_stage) {
@@ -245,6 +239,19 @@ impl App {
}
}
+ fn selected_path(&self) -> Option<(String, bool)> {
+ let (idx, is_stage) = match self.diff_target {
+ DiffTarget::Stage => (&self.index, true),
+ DiffTarget::WorkingDir => (&self.index_wd, false),
+ };
+
+ if let Some(i) = idx.selection() {
+ Some((i.path, is_stage))
+ } else {
+ None
+ }
+ }
+
fn update_commands(&mut self) {
self.help.set_cmds(self.commands(true));
self.current_commands = self.commands(false);
@@ -284,6 +291,17 @@ impl App {
self.reset.open_for_path(p);
self.update_commands();
}
+ InternalEvent::AddHunk(hash) => {
+ if let Some((path, is_stage)) = self.selected_path() {
+ if is_stage {
+ if sync::revert_hunk(CWD, path, *hash) {
+ self.update();
+ }
+ } else if sync::stage_hunk(CWD, path, *hash) {
+ self.update();
+ }
+ }
+ }
};
}
diff --git a/src/components/diff.rs b/src/components/diff.rs
index 705f5eb9..829ac44d 100644
--- a/src/components/diff.rs
+++ b/src/components/diff.rs
@@ -1,9 +1,10 @@
use super::{CommandBlocking, DrawableComponent, EventUpdate};
use crate::{
components::{CommandInfo, Component},
+ queue::{InternalEvent, Queue},
strings,
};
-use asyncgit::{hash, Diff, DiffLine, DiffLineType};
+use asyncgit::{hash, DiffLine, DiffLineType, FileDiff};
use crossterm::event::{Event, KeyCode};
use std::{borrow::Cow, cmp, convert::TryFrom};
use strings::commands;
@@ -16,57 +17,113 @@ use tui::{
Frame,
};
-///
#[derive(Default)]
+struct Current {
+ path: String,
+ is_stage: bool,
+ hash: u64,
+}
+
+///
pub struct DiffComponent {
- diff: Diff,
+ diff: FileDiff,
scroll: u16,
focused: bool,
- current: (String, bool),
- current_hash: u64,
+ current: Current,
+ selected_hunk: Option,
+ queue: Queue,
}
impl DiffComponent {
+ ///
+ pub fn new(queue: Queue) -> Self {
+ Self {
+ focused: false,
+ queue,
+ current: Current::default(),
+ selected_hunk: None,
+ diff: FileDiff::default(),
+ scroll: 0,
+ }
+ }
///
fn can_scroll(&self) -> bool {
- self.diff.1 > 1
+ self.diff.lines > 1
}
///
pub fn current(&self) -> (String, bool) {
- (self.current.0.clone(), self.current.1)
+ (self.current.path.clone(), self.current.is_stage)
}
///
pub fn clear(&mut self) {
- self.current.0.clear();
- self.diff = Diff::default();
- self.current_hash = 0;
+ self.current = Current::default();
+ self.diff = FileDiff::default();
+ self.scroll = 0;
+
+ self.selected_hunk =
+ Self::find_selected_hunk(&self.diff, self.scroll);
}
///
pub fn update(
&mut self,
path: String,
is_stage: bool,
- diff: Diff,
+ diff: FileDiff,
) {
let hash = hash(&diff);
- if self.current_hash != hash {
- self.current = (path, is_stage);
- self.current_hash = hash;
+ if self.current.hash != hash {
+ self.current = Current {
+ path,
+ is_stage,
+ hash,
+ };
self.diff = diff;
self.scroll = 0;
+
+ self.selected_hunk =
+ Self::find_selected_hunk(&self.diff, self.scroll);
}
}
fn scroll(&mut self, inc: bool) {
+ let old = self.scroll;
if inc {
self.scroll = cmp::min(
- self.diff.1.saturating_sub(1),
+ self.diff.lines.saturating_sub(1),
self.scroll.saturating_add(1),
);
} else {
self.scroll = self.scroll.saturating_sub(1);
}
+
+ if old != self.scroll {
+ self.selected_hunk =
+ Self::find_selected_hunk(&self.diff, self.scroll);
+ }
+ }
+
+ fn find_selected_hunk(
+ diff: &FileDiff,
+ line_selected: u16,
+ ) -> Option {
+ let mut line_cursor = 0_u16;
+ for (i, hunk) in diff.hunks.iter().enumerate() {
+ let hunk_len = u16::try_from(hunk.lines.len()).unwrap();
+ let hunk_min = line_cursor;
+ let hunk_max = line_cursor + hunk_len;
+
+ let hunk_selected =
+ hunk_min <= line_selected && hunk_max > line_selected;
+
+ if hunk_selected {
+ return Some(u16::try_from(i).unwrap());
+ }
+
+ line_cursor += hunk_len;
+ }
+
+ None
}
fn get_text(&self, width: u16, height: u16) -> Vec {
@@ -79,19 +136,21 @@ impl DiffComponent {
let mut line_cursor = 0_u16;
let mut lines_added = 0_u16;
- for hunk in &self.diff.0 {
+ for (i, hunk) in self.diff.hunks.iter().enumerate() {
+ let hunk_selected = self
+ .selected_hunk
+ .map_or(false, |s| s == u16::try_from(i).unwrap());
+
if lines_added >= height {
break;
}
- let hunk_len = u16::try_from(hunk.0.len()).unwrap();
+ let hunk_len = u16::try_from(hunk.lines.len()).unwrap();
let hunk_min = line_cursor;
let hunk_max = line_cursor + hunk_len;
if Self::hunk_visible(hunk_min, hunk_max, min, max) {
- let hunk_selected =
- hunk_min <= selection && hunk_max > selection;
- for (i, line) in hunk.0.iter().enumerate() {
+ for (i, line) in hunk.lines.iter().enumerate() {
if line_cursor >= min {
Self::add_line(
&mut res,
@@ -219,6 +278,17 @@ impl DiffComponent {
false
}
+
+ fn add_hunk(&self) {
+ if let Some(hunk) = self.selected_hunk {
+ let hash = self.diff.hunks
+ [usize::try_from(hunk).unwrap()]
+ .header_hash;
+ self.queue
+ .borrow_mut()
+ .push_back(InternalEvent::AddHunk(hash));
+ }
+ }
}
impl DrawableComponent for DiffComponent {
@@ -255,6 +325,18 @@ impl Component for DiffComponent {
self.focused,
));
+ let cmd_text = if self.current.is_stage {
+ commands::DIFF_HUNK_ADD
+ } else {
+ commands::DIFF_HUNK_REMOVE
+ };
+
+ out.push(CommandInfo::new(
+ cmd_text,
+ self.selected_hunk.is_some(),
+ self.focused,
+ ));
+
CommandBlocking::PassingOn
}
@@ -270,6 +352,10 @@ impl Component for DiffComponent {
self.scroll(false);
Some(EventUpdate::None)
}
+ KeyCode::Enter => {
+ self.add_hunk();
+ Some(EventUpdate::None)
+ }
_ => None,
};
}
diff --git a/src/queue.rs b/src/queue.rs
index 3f570a1a..62d9d5d3 100644
--- a/src/queue.rs
+++ b/src/queue.rs
@@ -6,6 +6,8 @@ pub enum InternalEvent {
ConfirmResetFile(String),
///
ResetFile(String),
+ ///
+ AddHunk(u64),
}
///
diff --git a/src/strings.rs b/src/strings.rs
index 91572b4b..82518e59 100644
--- a/src/strings.rs
+++ b/src/strings.rs
@@ -18,6 +18,7 @@ pub mod commands {
use crate::components::CommandText;
static CMD_GROUP_GENERAL: &str = "General";
+ static CMD_GROUP_DIFF: &str = "Diff";
static CMD_GROUP_CHANGES: &str = "Changes";
static CMD_GROUP_COMMIT: &str = "Commit";
@@ -34,6 +35,18 @@ pub mod commands {
CMD_GROUP_GENERAL,
);
///
+ pub static DIFF_HUNK_ADD: CommandText = CommandText::new(
+ "Add hunk [enter]",
+ "adds selected hunk to stage",
+ CMD_GROUP_DIFF,
+ );
+ ///
+ pub static DIFF_HUNK_REMOVE: CommandText = CommandText::new(
+ "Remove hunk [enter]",
+ "removes selected hunk from stage",
+ CMD_GROUP_DIFF,
+ );
+ ///
pub static CLOSE_POPUP: CommandText = CommandText::new(
"Close [esc]",
"close overlay (e.g commit, help)",