mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 01:18:21 +00:00
Support stage/unstage hunk
This commit is contained in:
parent
15ee7b8f87
commit
b71f39fbb3
13 changed files with 322 additions and 64 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -293,8 +293,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git2"
|
name = "git2"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/rust-lang/git2-rs.git?rev=617499d7fcf315cf92faa1ffde425666d3edd500#617499d7fcf315cf92faa1ffde425666d3edd500"
|
||||||
checksum = "ef222034f2069cfc5af01ce423574d3d9a3925bd4052912a14e5bcfd7ca9e47a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -404,8 +403,7 @@ checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
name = "libgit2-sys"
|
||||||
version = "0.12.2+1.0.0"
|
version = "0.12.2+1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/rust-lang/git2-rs.git?rev=617499d7fcf315cf92faa1ffde425666d3edd500#617499d7fcf315cf92faa1ffde425666d3edd500"
|
||||||
checksum = "a12c878ccc1a49ff71e264233a66d2114cdcc7fdc44c0ebe2b54075240831238"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
||||||
3
Makefile
3
Makefile
|
|
@ -4,6 +4,9 @@
|
||||||
debug:
|
debug:
|
||||||
GITUI_LOGGING=true cargo run --features=timing
|
GITUI_LOGGING=true cargo run --features=timing
|
||||||
|
|
||||||
|
build-release:
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,8 @@ to enable logging to `~/.gitui/gitui.log`:
|
||||||
GITUI_LOGGING=true gitui
|
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
|
* [ ] publish as homebrew-tap
|
||||||
|
|
||||||
# inspiration
|
# inspiration
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ license = "MIT"
|
||||||
categories = ["concurrency","asynchronous"]
|
categories = ["concurrency","asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
git2 = "0.13"
|
# git2 = "0.13"
|
||||||
|
git2 = { git = "https://github.com/rust-lang/git2-rs.git", rev = "617499d7fcf315cf92faa1ffde425666d3edd500" }
|
||||||
rayon-core = "1.7"
|
rayon-core = "1.7"
|
||||||
crossbeam-channel = "0.4"
|
crossbeam-channel = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{hash, sync, AsyncNotification, Diff, CWD};
|
use crate::{hash, sync, AsyncNotification, FileDiff, CWD};
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -21,8 +21,8 @@ struct LastResult<P, R> {
|
||||||
|
|
||||||
///
|
///
|
||||||
pub struct AsyncDiff {
|
pub struct AsyncDiff {
|
||||||
current: Arc<Mutex<Request<u64, Diff>>>,
|
current: Arc<Mutex<Request<u64, FileDiff>>>,
|
||||||
last: Arc<Mutex<Option<LastResult<DiffParams, Diff>>>>,
|
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
|
||||||
sender: Sender<AsyncNotification>,
|
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();
|
let last = self.last.lock().unwrap();
|
||||||
if let Some(res) = last.clone() {
|
if let Some(res) = last.clone() {
|
||||||
Some(res.result)
|
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");
|
trace!("request");
|
||||||
|
|
||||||
let hash = hash(¶ms);
|
let hash = hash(¶ms);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub use crate::{
|
||||||
diff::{AsyncDiff, DiffParams},
|
diff::{AsyncDiff, DiffParams},
|
||||||
status::AsyncStatus,
|
status::AsyncStatus,
|
||||||
sync::{
|
sync::{
|
||||||
diff::{Diff, DiffLine, DiffLineType},
|
diff::{DiffLine, DiffLineType, FileDiff},
|
||||||
status::{StatusItem, StatusItemType},
|
status::{StatusItem, StatusItemType},
|
||||||
utils::is_repo,
|
utils::is_repo,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
//! sync git api for fetching a diff
|
//! sync git api for fetching a diff
|
||||||
|
|
||||||
use super::utils;
|
use super::utils;
|
||||||
|
use crate::hash;
|
||||||
use git2::{
|
use git2::{
|
||||||
Delta, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
|
Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
|
||||||
|
Repository,
|
||||||
};
|
};
|
||||||
use scopetime::scope_time;
|
use scopetime::scope_time;
|
||||||
use std::{fs, path::Path};
|
use std::{fs, path::Path};
|
||||||
|
|
@ -35,9 +37,8 @@ pub struct DiffLine {
|
||||||
pub line_type: DiffLineType,
|
pub line_type: DiffLineType,
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
|
||||||
#[derive(Default, Clone, Copy, PartialEq)]
|
pub(crate) struct HunkHeader {
|
||||||
struct HunkHeader {
|
|
||||||
old_start: u32,
|
old_start: u32,
|
||||||
old_lines: u32,
|
old_lines: u32,
|
||||||
new_start: u32,
|
new_start: u32,
|
||||||
|
|
@ -57,20 +58,31 @@ impl From<DiffHunk<'_>> for HunkHeader {
|
||||||
|
|
||||||
///
|
///
|
||||||
#[derive(Default, Clone, Hash)]
|
#[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)]
|
#[derive(Default, Clone, Hash)]
|
||||||
pub struct Diff(pub Vec<Hunk>, pub u16);
|
pub struct FileDiff {
|
||||||
|
/// list of hunks
|
||||||
///
|
pub hunks: Vec<Hunk>,
|
||||||
pub fn get_diff(repo_path: &str, p: String, stage: bool) -> Diff {
|
/// lines total summed up over hunks
|
||||||
scope_time!("get_diff");
|
pub lines: u16,
|
||||||
|
}
|
||||||
let repo = utils::repo(repo_path);
|
|
||||||
|
|
||||||
|
pub(crate) fn get_diff_raw<'a>(
|
||||||
|
repo: &'a Repository,
|
||||||
|
p: &str,
|
||||||
|
stage: bool,
|
||||||
|
reverse: bool,
|
||||||
|
) -> (Diff<'a>, DiffOptions) {
|
||||||
let mut opt = DiffOptions::new();
|
let mut opt = DiffOptions::new();
|
||||||
opt.pathspec(p);
|
opt.pathspec(p);
|
||||||
|
opt.reverse(reverse);
|
||||||
|
|
||||||
let diff = if stage {
|
let diff = if stage {
|
||||||
// diff against head
|
// 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()
|
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_lines = Vec::new();
|
||||||
let mut current_hunk: Option<HunkHeader> = None;
|
let mut current_hunk: Option<HunkHeader> = None;
|
||||||
|
|
||||||
let mut adder = |lines: &Vec<DiffLine>| {
|
let mut adder = |header: &HunkHeader, lines: &Vec<DiffLine>| {
|
||||||
res.0.push(Hunk(lines.clone()));
|
res.hunks.push(Hunk {
|
||||||
res.1 += lines.len() as u16;
|
header_hash: hash(header),
|
||||||
|
lines: lines.clone(),
|
||||||
|
});
|
||||||
|
res.lines += lines.len() as u16;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut put = |hunk: Option<DiffHunk>, line: git2::DiffLine| {
|
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 {
|
match current_hunk {
|
||||||
None => current_hunk = Some(hunk_header),
|
None => current_hunk = Some(hunk_header),
|
||||||
Some(h) if h != hunk_header => {
|
Some(h) if h != hunk_header => {
|
||||||
adder(¤t_lines);
|
adder(&h, ¤t_lines);
|
||||||
current_lines.clear();
|
current_lines.clear();
|
||||||
current_hunk = Some(hunk_header)
|
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() {
|
if !current_lines.is_empty() {
|
||||||
adder(¤t_lines);
|
adder(¤t_hunk.unwrap(), ¤t_lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
res
|
||||||
|
|
@ -243,8 +269,8 @@ mod tests {
|
||||||
let diff =
|
let diff =
|
||||||
get_diff(repo_path, "foo/bar.txt".to_string(), false);
|
get_diff(repo_path, "foo/bar.txt".to_string(), false);
|
||||||
|
|
||||||
assert_eq!(diff.0.len(), 1);
|
assert_eq!(diff.hunks.len(), 1);
|
||||||
assert_eq!(diff.0[0].0[1].content, "test\n");
|
assert_eq!(diff.hunks[0].lines[1].content, "test\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -270,7 +296,7 @@ mod tests {
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(diff.0.len(), 1);
|
assert_eq!(diff.hunks.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static HUNK_A: &str = r"
|
static HUNK_A: &str = r"
|
||||||
|
|
@ -345,6 +371,6 @@ mod tests {
|
||||||
|
|
||||||
let res = get_diff(repo_path, "bar.txt".to_string(), false);
|
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
108
asyncgit/src/sync/hunks.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
//! sync git api
|
//! sync git api
|
||||||
|
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
|
mod hunks;
|
||||||
mod reset;
|
mod reset;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use hunks::{revert_hunk, stage_hunk};
|
||||||
pub use reset::{reset_stage, reset_workdir};
|
pub use reset::{reset_stage, reset_workdir};
|
||||||
pub use utils::{commit, stage_add};
|
pub use utils::{commit, stage_add};
|
||||||
|
|
||||||
|
|
|
||||||
34
src/app.rs
34
src/app.rs
|
|
@ -82,7 +82,7 @@ impl App {
|
||||||
false,
|
false,
|
||||||
queue.clone(),
|
queue.clone(),
|
||||||
),
|
),
|
||||||
diff: DiffComponent::default(),
|
diff: DiffComponent::new(queue.clone()),
|
||||||
git_diff: AsyncDiff::new(sender.clone()),
|
git_diff: AsyncDiff::new(sender.clone()),
|
||||||
git_status: AsyncStatus::new(sender),
|
git_status: AsyncStatus::new(sender),
|
||||||
current_commands: Vec::new(),
|
current_commands: Vec::new(),
|
||||||
|
|
@ -216,13 +216,7 @@ impl App {
|
||||||
// private impls
|
// private impls
|
||||||
impl App {
|
impl App {
|
||||||
fn update_diff(&mut self) {
|
fn update_diff(&mut self) {
|
||||||
let (idx, is_stage) = match self.diff_target {
|
if let Some((path, is_stage)) = self.selected_path() {
|
||||||
DiffTarget::Stage => (&self.index, true),
|
|
||||||
DiffTarget::WorkingDir => (&self.index_wd, false),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(i) = idx.selection() {
|
|
||||||
let path = i.path;
|
|
||||||
let diff_params = DiffParams(path.clone(), is_stage);
|
let diff_params = DiffParams(path.clone(), is_stage);
|
||||||
|
|
||||||
if self.diff.current() == (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) {
|
fn update_commands(&mut self) {
|
||||||
self.help.set_cmds(self.commands(true));
|
self.help.set_cmds(self.commands(true));
|
||||||
self.current_commands = self.commands(false);
|
self.current_commands = self.commands(false);
|
||||||
|
|
@ -284,6 +291,17 @@ impl App {
|
||||||
self.reset.open_for_path(p);
|
self.reset.open_for_path(p);
|
||||||
self.update_commands();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use super::{CommandBlocking, DrawableComponent, EventUpdate};
|
use super::{CommandBlocking, DrawableComponent, EventUpdate};
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{CommandInfo, Component},
|
components::{CommandInfo, Component},
|
||||||
|
queue::{InternalEvent, Queue},
|
||||||
strings,
|
strings,
|
||||||
};
|
};
|
||||||
use asyncgit::{hash, Diff, DiffLine, DiffLineType};
|
use asyncgit::{hash, DiffLine, DiffLineType, FileDiff};
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use std::{borrow::Cow, cmp, convert::TryFrom};
|
use std::{borrow::Cow, cmp, convert::TryFrom};
|
||||||
use strings::commands;
|
use strings::commands;
|
||||||
|
|
@ -16,57 +17,113 @@ use tui::{
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
///
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
struct Current {
|
||||||
|
path: String,
|
||||||
|
is_stage: bool,
|
||||||
|
hash: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
pub struct DiffComponent {
|
pub struct DiffComponent {
|
||||||
diff: Diff,
|
diff: FileDiff,
|
||||||
scroll: u16,
|
scroll: u16,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
current: (String, bool),
|
current: Current,
|
||||||
current_hash: u64,
|
selected_hunk: Option<u16>,
|
||||||
|
queue: Queue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffComponent {
|
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 {
|
fn can_scroll(&self) -> bool {
|
||||||
self.diff.1 > 1
|
self.diff.lines > 1
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
pub fn current(&self) -> (String, bool) {
|
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) {
|
pub fn clear(&mut self) {
|
||||||
self.current.0.clear();
|
self.current = Current::default();
|
||||||
self.diff = Diff::default();
|
self.diff = FileDiff::default();
|
||||||
self.current_hash = 0;
|
self.scroll = 0;
|
||||||
|
|
||||||
|
self.selected_hunk =
|
||||||
|
Self::find_selected_hunk(&self.diff, self.scroll);
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: String,
|
path: String,
|
||||||
is_stage: bool,
|
is_stage: bool,
|
||||||
diff: Diff,
|
diff: FileDiff,
|
||||||
) {
|
) {
|
||||||
let hash = hash(&diff);
|
let hash = hash(&diff);
|
||||||
|
|
||||||
if self.current_hash != hash {
|
if self.current.hash != hash {
|
||||||
self.current = (path, is_stage);
|
self.current = Current {
|
||||||
self.current_hash = hash;
|
path,
|
||||||
|
is_stage,
|
||||||
|
hash,
|
||||||
|
};
|
||||||
self.diff = diff;
|
self.diff = diff;
|
||||||
self.scroll = 0;
|
self.scroll = 0;
|
||||||
|
|
||||||
|
self.selected_hunk =
|
||||||
|
Self::find_selected_hunk(&self.diff, self.scroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll(&mut self, inc: bool) {
|
fn scroll(&mut self, inc: bool) {
|
||||||
|
let old = self.scroll;
|
||||||
if inc {
|
if inc {
|
||||||
self.scroll = cmp::min(
|
self.scroll = cmp::min(
|
||||||
self.diff.1.saturating_sub(1),
|
self.diff.lines.saturating_sub(1),
|
||||||
self.scroll.saturating_add(1),
|
self.scroll.saturating_add(1),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
self.scroll = self.scroll.saturating_sub(1);
|
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> {
|
fn get_text(&self, width: u16, height: u16) -> Vec<Text> {
|
||||||
|
|
@ -79,19 +136,21 @@ impl DiffComponent {
|
||||||
let mut line_cursor = 0_u16;
|
let mut line_cursor = 0_u16;
|
||||||
let mut lines_added = 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 {
|
if lines_added >= height {
|
||||||
break;
|
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_min = line_cursor;
|
||||||
let hunk_max = line_cursor + hunk_len;
|
let hunk_max = line_cursor + hunk_len;
|
||||||
|
|
||||||
if Self::hunk_visible(hunk_min, hunk_max, min, max) {
|
if Self::hunk_visible(hunk_min, hunk_max, min, max) {
|
||||||
let hunk_selected =
|
for (i, line) in hunk.lines.iter().enumerate() {
|
||||||
hunk_min <= selection && hunk_max > selection;
|
|
||||||
for (i, line) in hunk.0.iter().enumerate() {
|
|
||||||
if line_cursor >= min {
|
if line_cursor >= min {
|
||||||
Self::add_line(
|
Self::add_line(
|
||||||
&mut res,
|
&mut res,
|
||||||
|
|
@ -219,6 +278,17 @@ impl DiffComponent {
|
||||||
|
|
||||||
false
|
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 {
|
impl DrawableComponent for DiffComponent {
|
||||||
|
|
@ -255,6 +325,18 @@ impl Component for DiffComponent {
|
||||||
self.focused,
|
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
|
CommandBlocking::PassingOn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,6 +352,10 @@ impl Component for DiffComponent {
|
||||||
self.scroll(false);
|
self.scroll(false);
|
||||||
Some(EventUpdate::None)
|
Some(EventUpdate::None)
|
||||||
}
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.add_hunk();
|
||||||
|
Some(EventUpdate::None)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ pub enum InternalEvent {
|
||||||
ConfirmResetFile(String),
|
ConfirmResetFile(String),
|
||||||
///
|
///
|
||||||
ResetFile(String),
|
ResetFile(String),
|
||||||
|
///
|
||||||
|
AddHunk(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub mod commands {
|
||||||
use crate::components::CommandText;
|
use crate::components::CommandText;
|
||||||
|
|
||||||
static CMD_GROUP_GENERAL: &str = "General";
|
static CMD_GROUP_GENERAL: &str = "General";
|
||||||
|
static CMD_GROUP_DIFF: &str = "Diff";
|
||||||
static CMD_GROUP_CHANGES: &str = "Changes";
|
static CMD_GROUP_CHANGES: &str = "Changes";
|
||||||
static CMD_GROUP_COMMIT: &str = "Commit";
|
static CMD_GROUP_COMMIT: &str = "Commit";
|
||||||
|
|
||||||
|
|
@ -34,6 +35,18 @@ pub mod commands {
|
||||||
CMD_GROUP_GENERAL,
|
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(
|
pub static CLOSE_POPUP: CommandText = CommandText::new(
|
||||||
"Close [esc]",
|
"Close [esc]",
|
||||||
"close overlay (e.g commit, help)",
|
"close overlay (e.g commit, help)",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue