support discard selected lines (#571)

This commit is contained in:
Stephan Dilly 2021-03-08 18:00:30 +01:00 committed by GitHub
parent f86faf6406
commit 6e5db96c19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 763 additions and 80 deletions

View File

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- support discarding diff by lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
## [0.12.0] - 2020-03-03

View File

@ -50,8 +50,8 @@
edit_file: ( code: Char('I'), modifiers: ( bits: 1,),),
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
status_reset_item: ( code: Char('U'), modifiers: ( bits: 1,),),
status_reset_lines: ( code: Char('u'), modifiers: ( bits: 0,),),
status_ignore_file: ( code: Char('i'), modifiers: ( bits: 0,),),
stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),),

View File

@ -1,4 +1,4 @@
use std::string::FromUtf8Error;
use std::{num::TryFromIntError, string::FromUtf8Error};
use thiserror::Error;
#[derive(Error, Debug)]
@ -26,6 +26,9 @@ pub enum Error {
#[error("utf8 error:{0}")]
Utf8Error(#[from] FromUtf8Error),
#[error("TryFromInt error:{0}")]
IntError(#[from] TryFromIntError),
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -3,6 +3,7 @@
#![forbid(missing_docs)]
#![deny(unsafe_code)]
#![deny(unused_imports)]
#![deny(unused_must_use)]
#![deny(clippy::all)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]

View File

@ -92,14 +92,13 @@ pub fn merge_upstream_commit(
#[cfg(test)]
mod test {
use super::super::merge_ff::test::write_commit_file;
use super::*;
use crate::sync::{
branch_compare_upstream,
remotes::{fetch_origin, push::push},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare,
repo_init_bare, write_commit_file,
},
RepoState,
};

View File

@ -49,41 +49,12 @@ pub fn branch_merge_upstream_fastforward(
pub mod test {
use super::*;
use crate::sync::{
commit,
remotes::{fetch_origin, push::push},
stage_add_file,
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare,
repo_init_bare, write_commit_file,
},
CommitId,
};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
// write, stage and commit a file
pub fn write_commit_file(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
) -> CommitId {
File::create(
repo.workdir().unwrap().join(file).to_str().unwrap(),
)
.unwrap()
.write_all(content.as_bytes())
.unwrap();
stage_add_file(
repo.workdir().unwrap().to_str().unwrap(),
Path::new(file),
)
.unwrap();
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
.unwrap()
}
#[test]
fn test_merge_fastforward() {

View File

@ -39,14 +39,41 @@ pub struct DiffLine {
pub content: String,
///
pub line_type: DiffLineType,
///
pub position: DiffLinePosition,
}
///
#[derive(Clone, Copy, Default, Hash, Debug, PartialEq, Eq)]
pub struct DiffLinePosition {
///
pub old_lineno: Option<u32>,
///
pub new_lineno: Option<u32>,
}
impl PartialEq<&git2::DiffLine<'_>> for DiffLinePosition {
fn eq(&self, other: &&git2::DiffLine) -> bool {
other.new_lineno() == self.new_lineno
&& other.old_lineno() == self.old_lineno
}
}
impl From<&git2::DiffLine<'_>> for DiffLinePosition {
fn from(line: &git2::DiffLine<'_>) -> Self {
Self {
old_lineno: line.old_lineno(),
new_lineno: line.new_lineno(),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
pub(crate) struct HunkHeader {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
pub old_start: u32,
pub old_lines: u32,
pub new_start: u32,
pub new_lines: u32,
}
impl From<DiffHunk<'_>> for HunkHeader {
@ -89,10 +116,14 @@ pub(crate) fn get_diff_raw<'a>(
p: &str,
stage: bool,
reverse: bool,
context: Option<u32>,
) -> Result<Diff<'a>> {
// scope_time!("get_diff_raw");
let mut opt = DiffOptions::new();
if let Some(context) = context {
opt.context_lines(context);
}
opt.pathspec(p);
opt.reverse(reverse);
@ -133,7 +164,7 @@ pub fn get_diff(
let repo = utils::repo(repo_path)?;
let work_dir = work_dir(&repo)?;
let diff = get_diff_raw(&repo, &p, stage, false)?;
let diff = get_diff_raw(&repo, &p, stage, false, None)?;
raw_diff_to_file_diff(&diff, work_dir)
}
@ -209,6 +240,7 @@ fn raw_diff_to_file_diff<'a>(
};
let diff_line = DiffLine {
position: DiffLinePosition::from(&line),
content: String::from_utf8_lossy(line.content())
.to_string(),
line_type,

View File

@ -19,7 +19,7 @@ pub fn stage_hunk(
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, &file_path, false, false)?;
let diff = get_diff_raw(&repo, &file_path, false, false, None)?;
let mut opt = ApplyOptions::new();
opt.hunk_callback(|hunk| {
@ -46,7 +46,7 @@ pub fn reset_hunk(
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, &file_path, false, false)?;
let diff = get_diff_raw(&repo, &file_path, false, false, None)?;
let hunk_index = find_hunk_index(&diff, hunk_hash);
if let Some(hunk_index) = hunk_index {
@ -58,7 +58,8 @@ pub fn reset_hunk(
res
});
let diff = get_diff_raw(&repo, &file_path, false, true)?;
let diff =
get_diff_raw(&repo, &file_path, false, true, None)?;
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
@ -104,7 +105,7 @@ pub fn unstage_hunk(
let repo = repo(repo_path)?;
let diff = get_diff_raw(&repo, &file_path, true, false)?;
let diff = get_diff_raw(&repo, &file_path, true, false, None)?;
let diff_count_positive = diff.deltas().len();
let hunk_index = find_hunk_index(&diff, hunk_hash);
@ -113,7 +114,7 @@ pub fn unstage_hunk(
Ok,
)?;
let diff = get_diff_raw(&repo, &file_path, true, true)?;
let diff = get_diff_raw(&repo, &file_path, true, true, None)?;
if diff.deltas().len() != diff_count_positive {
return Err(Error::Generic(format!(

View File

@ -14,8 +14,10 @@ mod hooks;
mod hunks;
mod ignore;
mod logwalker;
mod patches;
pub mod remotes;
mod reset;
mod staging;
mod stash;
mod state;
pub mod status;
@ -47,6 +49,7 @@ pub use remotes::{
tags::PushTagsProgress,
};
pub use reset::{reset_stage, reset_workdir};
pub use staging::discard_lines;
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
pub use state::{repo_state, RepoState};
pub use tags::{get_tags, CommitTags, Tags};
@ -58,12 +61,14 @@ pub use utils::{
#[cfg(test)]
mod tests {
use super::{
commit, stage_add_file,
staging::repo_write_file,
status::{get_status, StatusType},
CommitId, LogWalker,
};
use crate::error::Result;
use git2::Repository;
use std::process::Command;
use std::{path::Path, process::Command};
use tempfile::TempDir;
/// Calling `set_search_path` with an empty directory makes sure that there
@ -88,6 +93,25 @@ mod tests {
});
}
/// write, stage and commit a file
pub fn write_commit_file(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
) -> CommitId {
repo_write_file(repo, file, content).unwrap();
stage_add_file(
repo.workdir().unwrap().to_str().unwrap(),
Path::new(file),
)
.unwrap();
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
.unwrap()
}
///
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
sandbox_config_files();

View File

@ -0,0 +1,74 @@
use super::diff::{get_diff_raw, HunkHeader};
use crate::error::{Error, Result};
use git2::{Diff, DiffLine, Patch, Repository};
//
pub(crate) struct HunkLines<'a> {
pub hunk: HunkHeader,
pub lines: Vec<DiffLine<'a>>,
}
///
pub(crate) fn get_file_diff_patch_and_hunklines<'a>(
repo: &'a Repository,
file: &str,
is_staged: bool,
reverse: bool,
) -> Result<(Patch<'a>, Vec<HunkLines<'a>>)> {
let diff =
get_diff_raw(&repo, file, is_staged, reverse, Some(1))?;
let patches = get_patches(&diff)?;
if patches.len() > 1 {
return Err(Error::Generic(String::from("patch error")));
}
let patch = patches.into_iter().next().ok_or_else(|| {
Error::Generic(String::from("no patch found"))
})?;
let lines = patch_get_hunklines(&patch)?;
Ok((patch, lines))
}
//
fn patch_get_hunklines<'a>(
patch: &Patch<'a>,
) -> Result<Vec<HunkLines<'a>>> {
let count_hunks = patch.num_hunks();
let mut res = Vec::with_capacity(count_hunks);
for hunk_idx in 0..count_hunks {
let (hunk, _) = patch.hunk(hunk_idx)?;
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
let mut hunk = HunkLines {
hunk: HunkHeader::from(hunk),
lines: Vec::with_capacity(count_lines),
};
for line_idx in 0..count_lines {
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
hunk.lines.push(line);
}
res.push(hunk);
}
Ok(res)
}
//
fn get_patches<'a>(diff: &Diff<'a>) -> Result<Vec<Patch<'a>>> {
let count = diff.deltas().len();
let mut res = Vec::with_capacity(count);
for idx in 0..count {
let p = Patch::from_diff(&diff, idx)?;
if let Some(p) = p {
res.push(p);
}
}
Ok(res)
}

View File

@ -1,7 +1,5 @@
//!
use std::collections::HashSet;
use super::{
push::{remote_callbacks, AsyncProgress},
utils,
@ -13,6 +11,7 @@ use crate::{
use crossbeam_channel::Sender;
use git2::{Direction, PushOptions};
use scopetime::scope_time;
use std::collections::HashSet;
///
#[derive(Debug, Copy, Clone, PartialEq)]
@ -157,37 +156,8 @@ mod tests {
self,
remotes::{fetch_origin, push::push},
tests::{repo_clone, repo_init_bare},
CommitId,
};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
// write, stage and commit a file
fn write_commit_file(
repo: &Repository,
file: &str,
content: &str,
commit_name: &str,
) -> CommitId {
File::create(
repo.workdir().unwrap().join(file).to_str().unwrap(),
)
.unwrap()
.write_all(content.as_bytes())
.unwrap();
sync::stage_add_file(
repo.workdir().unwrap().to_str().unwrap(),
Path::new(file),
)
.unwrap();
sync::commit(
repo.workdir().unwrap().to_str().unwrap(),
commit_name,
)
.unwrap()
}
use sync::tests::write_commit_file;
#[test]
fn test_push_pull_tags() {

View File

@ -0,0 +1,519 @@
use super::{
diff::DiffLinePosition,
patches::{get_file_diff_patch_and_hunklines, HunkLines},
utils::{repo, work_dir},
};
use crate::error::{Error, Result};
use git2::{DiffLine, Repository};
use scopetime::scope_time;
use std::{
collections::HashSet,
convert::TryFrom,
fs::File,
io::{Read, Write},
};
/// discards specific lines in an unstaged hunk of a diff
pub fn discard_lines(
repo_path: &str,
file_path: &str,
lines: &[DiffLinePosition],
) -> Result<()> {
scope_time!("discard_lines");
if lines.is_empty() {
return Ok(());
}
let repo = repo(repo_path)?;
//TODO: check that file is not new (status modified)
let new_content = {
let (_patch, hunks) = get_file_diff_patch_and_hunklines(
&repo, file_path, false, false,
)?;
let working_content = load_file(&repo, file_path)?;
let old_lines = working_content.lines().collect::<Vec<_>>();
apply_selection(lines, &hunks, old_lines, false, true)?
};
repo_write_file(&repo, file_path, new_content.as_str())?;
Ok(())
}
#[derive(Default)]
struct NewFromOldContent {
lines: Vec<String>,
old_index: usize,
}
impl NewFromOldContent {
fn add_from_hunk(&mut self, line: &DiffLine) -> Result<()> {
let line = String::from_utf8(line.content().into())?;
let line = if line.ends_with('\n') {
line[0..line.len() - 1].to_string()
} else {
line
};
self.lines.push(line);
Ok(())
}
fn skip_old_line(&mut self) {
self.old_index += 1;
}
fn add_old_line(&mut self, old_lines: &[&str]) {
self.lines.push(old_lines[self.old_index].to_string());
self.old_index += 1;
}
fn catchup_to_hunkstart(
&mut self,
hunk_start: usize,
old_lines: &[&str],
) {
while hunk_start > self.old_index + 1 {
self.add_old_line(old_lines);
}
}
fn finish(mut self, old_lines: &[&str]) -> String {
for line in old_lines.iter().skip(self.old_index) {
self.lines.push(line.to_string());
}
let lines = self.lines.join("\n");
if lines.ends_with('\n') {
lines
} else {
let mut lines = lines;
lines.push('\n');
lines
}
}
}
// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in nodegit: https://github.com/nodegit/nodegit
fn apply_selection(
lines: &[DiffLinePosition],
hunks: &[HunkLines],
old_lines: Vec<&str>,
is_staged: bool,
reverse: bool,
) -> Result<String> {
let mut new_content = NewFromOldContent::default();
let lines = lines.iter().collect::<HashSet<_>>();
let char_added = if reverse { '-' } else { '+' };
let char_deleted = if reverse { '+' } else { '-' };
let mut first_hunk_encountered = false;
for hunk in hunks {
let hunk_start = if is_staged || reverse {
usize::try_from(hunk.hunk.new_start)?
} else {
usize::try_from(hunk.hunk.old_start)?
};
if !first_hunk_encountered {
let any_slection_in_hunk =
hunk.lines.iter().any(|line| {
let line: DiffLinePosition = line.into();
lines.contains(&line)
});
first_hunk_encountered = any_slection_in_hunk;
}
if first_hunk_encountered {
// catchup until this hunk
new_content.catchup_to_hunkstart(hunk_start, &old_lines);
for hunk_line in &hunk.lines {
let hunk_line_pos: DiffLinePosition =
hunk_line.into();
let selected_line = lines.contains(&hunk_line_pos);
log::debug!(
// println!(
"{} line: {} [{:?} old, {:?} new] -> {}",
if selected_line { "*" } else { " " },
hunk_line.origin(),
hunk_line.old_lineno(),
hunk_line.new_lineno(),
String::from_utf8_lossy(hunk_line.content())
.trim()
);
if hunk_line.origin() == '<' {
break;
}
if (is_staged && !selected_line)
|| (!is_staged && selected_line)
{
if hunk_line.origin() == char_added {
new_content.add_from_hunk(hunk_line)?;
if is_staged {
new_content.skip_old_line();
}
} else if hunk_line.origin() == char_deleted {
if !is_staged {
new_content.skip_old_line();
}
} else {
new_content.add_old_line(&old_lines);
}
} else {
if hunk_line.origin() != char_added {
new_content.add_from_hunk(hunk_line)?;
}
if (is_staged
&& hunk_line.origin() != char_deleted)
|| (!is_staged
&& hunk_line.origin() != char_added)
{
new_content.skip_old_line();
}
}
}
}
}
Ok(new_content.finish(&old_lines))
}
fn load_file(repo: &Repository, file_path: &str) -> Result<String> {
let repo_path = work_dir(repo)?;
let mut file = File::open(repo_path.join(file_path).as_path())?;
let mut res = String::new();
file.read_to_string(&mut res)?;
Ok(res)
}
//TODO: use this in unittests instead of the test specific one
/// write a file in repo
pub(crate) fn repo_write_file(
repo: &Repository,
file: &str,
content: &str,
) -> Result<()> {
let dir = work_dir(repo)?.join(file);
let file_path = dir.to_str().ok_or_else(|| {
Error::Generic(String::from("invalid file path"))
})?;
let mut file = File::create(file_path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::tests::{repo_init, write_commit_file};
#[test]
fn test_discard() {
static FILE_1: &str = r"0
1
2
3
4
";
static FILE_2: &str = r"0
3
4
";
static FILE_3: &str = r"0
2
3
4
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(3),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard2() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(3),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard3() {
static FILE_1: &str = r"start
1
end
";
static FILE_2: &str = r"start
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard4() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
1
mid
2
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(4),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard_if_first_selected_line_is_not_in_any_hunk() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
end
";
static FILE_3: &str = r"start
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(1),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
//this test shows that we require at least a diff context around add/removes of 1
#[test]
fn test_discard_deletions_filestart_breaking_with_zero_context() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard5() {
static FILE_1: &str = r"start
";
static FILE_2: &str = r"start
1";
static FILE_3: &str = r"start
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
}

View File

@ -502,6 +502,10 @@ impl App {
sync::reset_hunk(CWD, path, hash)?;
flags.insert(NeedsUpdate::ALL);
}
Action::ResetLines(path, lines) => {
sync::discard_lines(CWD, &path, &lines)?;
flags.insert(NeedsUpdate::ALL);
}
Action::DeleteBranch(branch_ref) => {
if let Err(e) =
sync::delete_branch(CWD, &branch_ref)

View File

@ -9,7 +9,11 @@ use crate::{
ui::{self, calc_scroll_top, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::{hash, sync, DiffLine, DiffLineType, FileDiff, CWD};
use asyncgit::{
hash,
sync::{self, diff::DiffLinePosition},
DiffLine, DiffLineType, FileDiff, CWD,
};
use bytesize::ByteSize;
use crossterm::event::Event;
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
@ -509,6 +513,38 @@ impl DiffComponent {
}
}
fn reset_lines(&self) {
if let Some(diff) = &self.diff {
if self.selected_hunk.is_some() {
let selected_lines: Vec<DiffLinePosition> = diff
.hunks
.iter()
.flat_map(|hunk| hunk.lines.iter())
.enumerate()
.filter_map(|(i, line)| {
let is_add_or_delete = line.line_type
== DiffLineType::Add
|| line.line_type == DiffLineType::Delete;
if self.selection.contains(i)
&& is_add_or_delete
{
Some(line.position)
} else {
None
}
})
.collect();
self.queue.as_ref().borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::ResetLines(
self.current.path.clone(),
selected_lines,
)),
);
}
}
}
fn reset_untracked(&self) {
self.queue.as_ref().borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::Reset(ResetItem {
@ -634,11 +670,20 @@ impl Component for DiffComponent {
self.selected_hunk.is_some(),
self.focused && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_revert(
&self.key_config,
),
//TODO: only if any modifications are selected
true,
self.focused && !self.is_stage(),
));
}
CommandBlocking::PassingOn
}
#[allow(clippy::cognitive_complexity)]
fn event(&mut self, ev: Event) -> Result<bool> {
if self.focused {
if let Event::Key(e) = ev {
@ -688,6 +733,16 @@ impl Component for DiffComponent {
}
}
Ok(true)
} else if e == self.key_config.status_reset_lines
&& !self.is_immutable
&& !self.is_stage()
{
if let Some(diff) = &self.diff {
if !diff.untracked {
self.reset_lines();
}
}
Ok(true)
} else if e == self.key_config.copy {
self.copy_selection();
Ok(true)

View File

@ -151,6 +151,10 @@ impl ResetComponent {
strings::confirm_title_reset(&self.key_config),
strings::confirm_msg_resethunk(&self.key_config),
),
Action::ResetLines(_, lines) => (
strings::confirm_title_reset(&self.key_config),
strings::confirm_msg_reset_lines(&self.key_config,lines.len()),
),
Action::DeleteBranch(branch_ref) => (
strings::confirm_title_delete_branch(
&self.key_config,

View File

@ -51,6 +51,7 @@ pub struct KeyConfig {
pub edit_file: KeyEvent,
pub status_stage_all: KeyEvent,
pub status_reset_item: KeyEvent,
pub status_reset_lines: KeyEvent,
pub status_ignore_file: KeyEvent,
pub stashing_save: KeyEvent,
pub stashing_toggle_untracked: KeyEvent,
@ -105,6 +106,7 @@ impl Default for KeyConfig {
edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()},
status_stage_all: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::empty()},
status_reset_item: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
status_reset_lines: KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::empty()},
status_ignore_file: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()},
stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()},

View File

@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
#![deny(unused_imports)]
#![deny(unused_must_use)]
#![deny(clippy::cargo)]
#![deny(clippy::pedantic)]
#![deny(clippy::perf)]

View File

@ -1,5 +1,5 @@
use crate::tabs::StashingOptions;
use asyncgit::sync::{CommitId, CommitTags};
use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags};
use bitflags::bitflags;
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
@ -27,6 +27,7 @@ pub struct ResetItem {
pub enum Action {
Reset(ResetItem),
ResetHunk(String, u64),
ResetLines(String, Vec<DiffLinePosition>),
StashDrop(CommitId),
DeleteBranch(String),
ForcePush(String, bool),

View File

@ -107,6 +107,15 @@ pub fn confirm_msg_merge(
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
"confirm file reset?".to_string()
}
pub fn confirm_msg_reset_lines(
_key_config: &SharedKeyConfig,
lines: usize,
) -> String {
format!(
"are you sure you want to discard {} selected lines?",
lines
)
}
pub fn confirm_msg_stashdrop(
_key_config: &SharedKeyConfig,
) -> String {
@ -388,13 +397,25 @@ pub mod commands {
) -> CommandText {
CommandText::new(
format!(
"Revert hunk [{}]",
"Reset hunk [{}]",
key_config.get_hint(key_config.status_reset_item),
),
"reverts selected hunk",
CMD_GROUP_DIFF,
)
}
pub fn diff_lines_revert(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Reset lines [{}]",
key_config.get_hint(key_config.status_reset_lines),
),
"resets selected lines",
CMD_GROUP_DIFF,
)
}
pub fn diff_hunk_remove(
key_config: &SharedKeyConfig,
) -> CommandText {