mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
support discard selected lines (#571)
This commit is contained in:
parent
f86faf6406
commit
6e5db96c19
19 changed files with 763 additions and 80 deletions
|
|
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### 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))
|
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
|
||||||
|
|
||||||
## [0.12.0] - 2020-03-03
|
## [0.12.0] - 2020-03-03
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@
|
||||||
edit_file: ( code: Char('I'), modifiers: ( bits: 1,),),
|
edit_file: ( code: Char('I'), modifiers: ( bits: 1,),),
|
||||||
|
|
||||||
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
|
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
|
||||||
|
|
||||||
status_reset_item: ( code: Char('U'), modifiers: ( bits: 1,),),
|
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,),),
|
status_ignore_file: ( code: Char('i'), modifiers: ( bits: 0,),),
|
||||||
|
|
||||||
stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),),
|
stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::string::FromUtf8Error;
|
use std::{num::TryFromIntError, string::FromUtf8Error};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -26,6 +26,9 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("utf8 error:{0}")]
|
#[error("utf8 error:{0}")]
|
||||||
Utf8Error(#[from] FromUtf8Error),
|
Utf8Error(#[from] FromUtf8Error),
|
||||||
|
|
||||||
|
#[error("TryFromInt error:{0}")]
|
||||||
|
IntError(#[from] TryFromIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#![forbid(missing_docs)]
|
#![forbid(missing_docs)]
|
||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![deny(unused_imports)]
|
#![deny(unused_imports)]
|
||||||
|
#![deny(unused_must_use)]
|
||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
#![deny(clippy::unwrap_used)]
|
#![deny(clippy::unwrap_used)]
|
||||||
#![deny(clippy::panic)]
|
#![deny(clippy::panic)]
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,13 @@ pub fn merge_upstream_commit(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::super::merge_ff::test::write_commit_file;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sync::{
|
use crate::sync::{
|
||||||
branch_compare_upstream,
|
branch_compare_upstream,
|
||||||
remotes::{fetch_origin, push::push},
|
remotes::{fetch_origin, push::push},
|
||||||
tests::{
|
tests::{
|
||||||
debug_cmd_print, get_commit_ids, repo_clone,
|
debug_cmd_print, get_commit_ids, repo_clone,
|
||||||
repo_init_bare,
|
repo_init_bare, write_commit_file,
|
||||||
},
|
},
|
||||||
RepoState,
|
RepoState,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,41 +49,12 @@ pub fn branch_merge_upstream_fastforward(
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sync::{
|
use crate::sync::{
|
||||||
commit,
|
|
||||||
remotes::{fetch_origin, push::push},
|
remotes::{fetch_origin, push::push},
|
||||||
stage_add_file,
|
|
||||||
tests::{
|
tests::{
|
||||||
debug_cmd_print, get_commit_ids, repo_clone,
|
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]
|
#[test]
|
||||||
fn test_merge_fastforward() {
|
fn test_merge_fastforward() {
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,41 @@ pub struct DiffLine {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
///
|
///
|
||||||
pub line_type: DiffLineType,
|
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)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
|
||||||
pub(crate) struct HunkHeader {
|
pub(crate) struct HunkHeader {
|
||||||
old_start: u32,
|
pub old_start: u32,
|
||||||
old_lines: u32,
|
pub old_lines: u32,
|
||||||
new_start: u32,
|
pub new_start: u32,
|
||||||
new_lines: u32,
|
pub new_lines: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DiffHunk<'_>> for HunkHeader {
|
impl From<DiffHunk<'_>> for HunkHeader {
|
||||||
|
|
@ -89,10 +116,14 @@ pub(crate) fn get_diff_raw<'a>(
|
||||||
p: &str,
|
p: &str,
|
||||||
stage: bool,
|
stage: bool,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
|
context: Option<u32>,
|
||||||
) -> Result<Diff<'a>> {
|
) -> Result<Diff<'a>> {
|
||||||
// scope_time!("get_diff_raw");
|
// scope_time!("get_diff_raw");
|
||||||
|
|
||||||
let mut opt = DiffOptions::new();
|
let mut opt = DiffOptions::new();
|
||||||
|
if let Some(context) = context {
|
||||||
|
opt.context_lines(context);
|
||||||
|
}
|
||||||
opt.pathspec(p);
|
opt.pathspec(p);
|
||||||
opt.reverse(reverse);
|
opt.reverse(reverse);
|
||||||
|
|
||||||
|
|
@ -133,7 +164,7 @@ pub fn get_diff(
|
||||||
|
|
||||||
let repo = utils::repo(repo_path)?;
|
let repo = utils::repo(repo_path)?;
|
||||||
let work_dir = work_dir(&repo)?;
|
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)
|
raw_diff_to_file_diff(&diff, work_dir)
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +240,7 @@ fn raw_diff_to_file_diff<'a>(
|
||||||
};
|
};
|
||||||
|
|
||||||
let diff_line = DiffLine {
|
let diff_line = DiffLine {
|
||||||
|
position: DiffLinePosition::from(&line),
|
||||||
content: String::from_utf8_lossy(line.content())
|
content: String::from_utf8_lossy(line.content())
|
||||||
.to_string(),
|
.to_string(),
|
||||||
line_type,
|
line_type,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub fn stage_hunk(
|
||||||
|
|
||||||
let repo = repo(repo_path)?;
|
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();
|
let mut opt = ApplyOptions::new();
|
||||||
opt.hunk_callback(|hunk| {
|
opt.hunk_callback(|hunk| {
|
||||||
|
|
@ -46,7 +46,7 @@ pub fn reset_hunk(
|
||||||
|
|
||||||
let repo = repo(repo_path)?;
|
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);
|
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||||
if let Some(hunk_index) = hunk_index {
|
if let Some(hunk_index) = hunk_index {
|
||||||
|
|
@ -58,7 +58,8 @@ pub fn reset_hunk(
|
||||||
res
|
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))?;
|
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
|
||||||
|
|
||||||
|
|
@ -104,7 +105,7 @@ pub fn unstage_hunk(
|
||||||
|
|
||||||
let repo = repo(repo_path)?;
|
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 diff_count_positive = diff.deltas().len();
|
||||||
|
|
||||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||||
|
|
@ -113,7 +114,7 @@ pub fn unstage_hunk(
|
||||||
Ok,
|
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 {
|
if diff.deltas().len() != diff_count_positive {
|
||||||
return Err(Error::Generic(format!(
|
return Err(Error::Generic(format!(
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ mod hooks;
|
||||||
mod hunks;
|
mod hunks;
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod logwalker;
|
mod logwalker;
|
||||||
|
mod patches;
|
||||||
pub mod remotes;
|
pub mod remotes;
|
||||||
mod reset;
|
mod reset;
|
||||||
|
mod staging;
|
||||||
mod stash;
|
mod stash;
|
||||||
mod state;
|
mod state;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
@ -47,6 +49,7 @@ pub use remotes::{
|
||||||
tags::PushTagsProgress,
|
tags::PushTagsProgress,
|
||||||
};
|
};
|
||||||
pub use reset::{reset_stage, reset_workdir};
|
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 stash::{get_stashes, stash_apply, stash_drop, stash_save};
|
||||||
pub use state::{repo_state, RepoState};
|
pub use state::{repo_state, RepoState};
|
||||||
pub use tags::{get_tags, CommitTags, Tags};
|
pub use tags::{get_tags, CommitTags, Tags};
|
||||||
|
|
@ -58,12 +61,14 @@ pub use utils::{
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
|
commit, stage_add_file,
|
||||||
|
staging::repo_write_file,
|
||||||
status::{get_status, StatusType},
|
status::{get_status, StatusType},
|
||||||
CommitId, LogWalker,
|
CommitId, LogWalker,
|
||||||
};
|
};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
use std::process::Command;
|
use std::{path::Path, process::Command};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
/// Calling `set_search_path` with an empty directory makes sure that there
|
/// 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)> {
|
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
|
||||||
sandbox_config_files();
|
sandbox_config_files();
|
||||||
|
|
|
||||||
74
asyncgit/src/sync/patches.rs
Normal file
74
asyncgit/src/sync/patches.rs
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
push::{remote_callbacks, AsyncProgress},
|
push::{remote_callbacks, AsyncProgress},
|
||||||
utils,
|
utils,
|
||||||
|
|
@ -13,6 +11,7 @@ use crate::{
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use git2::{Direction, PushOptions};
|
use git2::{Direction, PushOptions};
|
||||||
use scopetime::scope_time;
|
use scopetime::scope_time;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
///
|
///
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
|
@ -157,37 +156,8 @@ mod tests {
|
||||||
self,
|
self,
|
||||||
remotes::{fetch_origin, push::push},
|
remotes::{fetch_origin, push::push},
|
||||||
tests::{repo_clone, repo_init_bare},
|
tests::{repo_clone, repo_init_bare},
|
||||||
CommitId,
|
|
||||||
};
|
};
|
||||||
use git2::Repository;
|
use sync::tests::write_commit_file;
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_push_pull_tags() {
|
fn test_push_pull_tags() {
|
||||||
|
|
|
||||||
519
asyncgit/src/sync/staging/mod.rs
Normal file
519
asyncgit/src/sync/staging/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -502,6 +502,10 @@ impl App {
|
||||||
sync::reset_hunk(CWD, path, hash)?;
|
sync::reset_hunk(CWD, path, hash)?;
|
||||||
flags.insert(NeedsUpdate::ALL);
|
flags.insert(NeedsUpdate::ALL);
|
||||||
}
|
}
|
||||||
|
Action::ResetLines(path, lines) => {
|
||||||
|
sync::discard_lines(CWD, &path, &lines)?;
|
||||||
|
flags.insert(NeedsUpdate::ALL);
|
||||||
|
}
|
||||||
Action::DeleteBranch(branch_ref) => {
|
Action::DeleteBranch(branch_ref) => {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
sync::delete_branch(CWD, &branch_ref)
|
sync::delete_branch(CWD, &branch_ref)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ use crate::{
|
||||||
ui::{self, calc_scroll_top, style::SharedTheme},
|
ui::{self, calc_scroll_top, style::SharedTheme},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
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 bytesize::ByteSize;
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
|
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) {
|
fn reset_untracked(&self) {
|
||||||
self.queue.as_ref().borrow_mut().push_back(
|
self.queue.as_ref().borrow_mut().push_back(
|
||||||
InternalEvent::ConfirmAction(Action::Reset(ResetItem {
|
InternalEvent::ConfirmAction(Action::Reset(ResetItem {
|
||||||
|
|
@ -634,11 +670,20 @@ impl Component for DiffComponent {
|
||||||
self.selected_hunk.is_some(),
|
self.selected_hunk.is_some(),
|
||||||
self.focused && !self.is_stage(),
|
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
|
CommandBlocking::PassingOn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn event(&mut self, ev: Event) -> Result<bool> {
|
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||||
if self.focused {
|
if self.focused {
|
||||||
if let Event::Key(e) = ev {
|
if let Event::Key(e) = ev {
|
||||||
|
|
@ -688,6 +733,16 @@ impl Component for DiffComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
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 {
|
} else if e == self.key_config.copy {
|
||||||
self.copy_selection();
|
self.copy_selection();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,10 @@ impl ResetComponent {
|
||||||
strings::confirm_title_reset(&self.key_config),
|
strings::confirm_title_reset(&self.key_config),
|
||||||
strings::confirm_msg_resethunk(&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) => (
|
Action::DeleteBranch(branch_ref) => (
|
||||||
strings::confirm_title_delete_branch(
|
strings::confirm_title_delete_branch(
|
||||||
&self.key_config,
|
&self.key_config,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ pub struct KeyConfig {
|
||||||
pub edit_file: KeyEvent,
|
pub edit_file: KeyEvent,
|
||||||
pub status_stage_all: KeyEvent,
|
pub status_stage_all: KeyEvent,
|
||||||
pub status_reset_item: KeyEvent,
|
pub status_reset_item: KeyEvent,
|
||||||
|
pub status_reset_lines: KeyEvent,
|
||||||
pub status_ignore_file: KeyEvent,
|
pub status_ignore_file: KeyEvent,
|
||||||
pub stashing_save: KeyEvent,
|
pub stashing_save: KeyEvent,
|
||||||
pub stashing_toggle_untracked: KeyEvent,
|
pub stashing_toggle_untracked: KeyEvent,
|
||||||
|
|
@ -105,6 +106,7 @@ impl Default for KeyConfig {
|
||||||
edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()},
|
edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()},
|
||||||
status_stage_all: KeyEvent { code: KeyCode::Char('a'), 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_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()},
|
status_ignore_file: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()},
|
||||||
stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
|
stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
|
||||||
stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()},
|
stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(unused_imports)]
|
#![deny(unused_imports)]
|
||||||
|
#![deny(unused_must_use)]
|
||||||
#![deny(clippy::cargo)]
|
#![deny(clippy::cargo)]
|
||||||
#![deny(clippy::pedantic)]
|
#![deny(clippy::pedantic)]
|
||||||
#![deny(clippy::perf)]
|
#![deny(clippy::perf)]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::tabs::StashingOptions;
|
use crate::tabs::StashingOptions;
|
||||||
use asyncgit::sync::{CommitId, CommitTags};
|
use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||||
|
|
||||||
|
|
@ -27,6 +27,7 @@ pub struct ResetItem {
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Reset(ResetItem),
|
Reset(ResetItem),
|
||||||
ResetHunk(String, u64),
|
ResetHunk(String, u64),
|
||||||
|
ResetLines(String, Vec<DiffLinePosition>),
|
||||||
StashDrop(CommitId),
|
StashDrop(CommitId),
|
||||||
DeleteBranch(String),
|
DeleteBranch(String),
|
||||||
ForcePush(String, bool),
|
ForcePush(String, bool),
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,15 @@ pub fn confirm_msg_merge(
|
||||||
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
|
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
|
||||||
"confirm file reset?".to_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(
|
pub fn confirm_msg_stashdrop(
|
||||||
_key_config: &SharedKeyConfig,
|
_key_config: &SharedKeyConfig,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
|
@ -388,13 +397,25 @@ pub mod commands {
|
||||||
) -> CommandText {
|
) -> CommandText {
|
||||||
CommandText::new(
|
CommandText::new(
|
||||||
format!(
|
format!(
|
||||||
"Revert hunk [{}]",
|
"Reset hunk [{}]",
|
||||||
key_config.get_hint(key_config.status_reset_item),
|
key_config.get_hint(key_config.status_reset_item),
|
||||||
),
|
),
|
||||||
"reverts selected hunk",
|
"reverts selected hunk",
|
||||||
CMD_GROUP_DIFF,
|
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(
|
pub fn diff_hunk_remove(
|
||||||
key_config: &SharedKeyConfig,
|
key_config: &SharedKeyConfig,
|
||||||
) -> CommandText {
|
) -> CommandText {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue