Support stage/unstage hunk

This commit is contained in:
Stephan Dilly 2020-04-06 10:18:26 +02:00
parent 15ee7b8f87
commit b71f39fbb3
13 changed files with 322 additions and 64 deletions

6
Cargo.lock generated
View File

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

View File

@ -4,6 +4,9 @@
debug:
GITUI_LOGGING=true cargo run --features=timing
build-release:
cargo build --release
test:
cargo test --workspace

View File

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

View File

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

View File

@ -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<P, R> {
///
pub struct AsyncDiff {
current: Arc<Mutex<Request<u64, Diff>>>,
last: Arc<Mutex<Option<LastResult<DiffParams, Diff>>>>,
current: Arc<Mutex<Request<u64, FileDiff>>>,
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
sender: Sender<AsyncNotification>,
}
@ -37,7 +37,7 @@ impl AsyncDiff {
}
///
pub fn last(&mut self) -> Option<Diff> {
pub fn last(&mut self) -> Option<FileDiff> {
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<Diff> {
pub fn request(
&mut self,
params: DiffParams,
) -> Option<FileDiff> {
trace!("request");
let hash = hash(&params);

View File

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

View File

@ -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<DiffHunk<'_>> for HunkHeader {
///
#[derive(Default, Clone, Hash)]
pub struct Hunk(pub Vec<DiffLine>);
pub struct Hunk {
///
pub header_hash: u64,
///
pub lines: Vec<DiffLine>,
}
///
#[derive(Default, Clone, Hash)]
pub struct Diff(pub Vec<Hunk>, 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<Hunk>,
/// 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<HunkHeader> = None;
let mut adder = |lines: &Vec<DiffLine>| {
res.0.push(Hunk(lines.clone()));
res.1 += lines.len() as u16;
let mut adder = |header: &HunkHeader, lines: &Vec<DiffLine>| {
res.hunks.push(Hunk {
header_hash: hash(header),
lines: lines.clone(),
});
res.lines += lines.len() as u16;
};
let mut put = |hunk: Option<DiffHunk>, 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(&current_lines);
adder(&h, &current_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(&current_lines);
adder(&current_hunk.unwrap(), &current_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)
}
}

108
asyncgit/src/sync/hunks.rs Normal file
View File

@ -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<usize> {
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
}

View File

@ -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};

View File

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

View File

@ -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<u16>,
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<u16> {
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<Text> {
@ -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,
};
}

View File

@ -6,6 +6,8 @@ pub enum InternalEvent {
ConfirmResetFile(String),
///
ResetFile(String),
///
AddHunk(u64),
}
///

View File

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