From b5ef9b10f1e0e6e0c21a55d9083b4e40c1f759b8 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Wed, 10 Mar 2021 22:27:02 +0100 Subject: [PATCH] Stage/unstage lines (#575) --- CHANGELOG.md | 6 +- assets/vim_style_key_config.ron | 3 +- asyncgit/src/sync/diff.rs | 1 + asyncgit/src/sync/mod.rs | 4 +- asyncgit/src/sync/staging/discard_tracked.rs | 344 +++++++++++++++++ asyncgit/src/sync/staging/mod.rs | 382 +------------------ asyncgit/src/sync/staging/stage_tracked.rs | 157 ++++++++ asyncgit/src/sync/stash.rs | 2 +- asyncgit/src/sync/utils.rs | 19 +- src/components/diff.rs | 72 +++- src/keys.rs | 6 +- src/strings.rs | 62 ++- src/tabs/status.rs | 28 +- 13 files changed, 636 insertions(+), 450 deletions(-) create mode 100644 asyncgit/src/sync/staging/discard_tracked.rs create mode 100644 asyncgit/src/sync/staging/stage_tracked.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ae36c4865c..2104514713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed +- `[s]` key repurposed to trigger line based (un)stage + ### Added -- support discarding diff by lines ([#59](https://github.com/extrawurst/gitui/issues/59)) +- support stage/unstage selected lines ([#59](https://github.com/extrawurst/gitui/issues/59)) +- support discarding selected lines ([#59](https://github.com/extrawurst/gitui/issues/59)) - support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568)) - visualize *conflicted* files differently ([#576](https://github.com/extrawurst/gitui/issues/576)) diff --git a/assets/vim_style_key_config.ron b/assets/vim_style_key_config.ron index c6a0cced2f..c05174fa01 100644 --- a/assets/vim_style_key_config.ron +++ b/assets/vim_style_key_config.ron @@ -17,8 +17,6 @@ tab_toggle: ( code: Tab, modifiers: ( bits: 0,),), tab_toggle_reverse: ( code: BackTab, modifiers: ( bits: 1,),), - focus_workdir: ( code: Char('w'), modifiers: ( bits: 0,),), - focus_stage: ( code: Char('s'), modifiers: ( bits: 0,),), focus_right: ( code: Char('l'), modifiers: ( bits: 0,),), focus_left: ( code: Char('h'), modifiers: ( bits: 0,),), focus_above: ( code: Char('k'), modifiers: ( bits: 0,),), @@ -53,6 +51,7 @@ 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,),), + diff_stage_lines: ( code: Char('s'), modifiers: ( bits: 0,),), stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),), stashing_toggle_untracked: ( code: Char('u'), modifiers: ( bits: 0,),), diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index 5831f758fe..0ddc469b8d 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -157,6 +157,7 @@ pub(crate) fn get_diff_raw<'a>( /// returns diff of a specific file either in `stage` or workdir pub fn get_diff( repo_path: &str, + //TODO: make &str p: String, stage: bool, ) -> Result { diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index cc425d7bc2..519c87bb28 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -49,7 +49,7 @@ pub use remotes::{ tags::PushTagsProgress, }; pub use reset::{reset_stage, reset_workdir}; -pub use staging::discard_lines; +pub use staging::{discard_lines, stage_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}; @@ -62,8 +62,8 @@ pub use utils::{ mod tests { use super::{ commit, stage_add_file, - staging::repo_write_file, status::{get_status, StatusType}, + utils::repo_write_file, CommitId, LogWalker, }; use crate::error::Result; diff --git a/asyncgit/src/sync/staging/discard_tracked.rs b/asyncgit/src/sync/staging/discard_tracked.rs new file mode 100644 index 0000000000..270cc63b2f --- /dev/null +++ b/asyncgit/src/sync/staging/discard_tracked.rs @@ -0,0 +1,344 @@ +use super::{apply_selection, load_file}; +use crate::error::Result; +use crate::sync::{ + diff::DiffLinePosition, + patches::get_file_diff_patch_and_hunklines, + utils::{repo, repo_write_file}, +}; +use scopetime::scope_time; + +/// 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)?; + repo.index()?.read(true)?; + + //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::>(); + + apply_selection(lines, &hunks, old_lines, false, true)? + }; + + repo_write_file(&repo, file_path, new_content.as_str())?; + + 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); + } +} diff --git a/asyncgit/src/sync/staging/mod.rs b/asyncgit/src/sync/staging/mod.rs index c0039c8e99..72018aa364 100644 --- a/asyncgit/src/sync/staging/mod.rs +++ b/asyncgit/src/sync/staging/mod.rs @@ -1,48 +1,19 @@ +mod discard_tracked; +mod stage_tracked; + +pub use discard_tracked::discard_lines; +pub use stage_tracked::stage_lines; + use super::{ - diff::DiffLinePosition, - patches::{get_file_diff_patch_and_hunklines, HunkLines}, - utils::{repo, work_dir}, + diff::DiffLinePosition, patches::HunkLines, utils::work_dir, }; -use crate::error::{Error, Result}; +use crate::error::Result; use git2::{DiffLine, Repository}; -use scopetime::scope_time; use std::{ - collections::HashSet, - convert::TryFrom, - fs::File, - io::{Read, Write}, + collections::HashSet, convert::TryFrom, fs::File, io::Read, }; -/// 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::>(); - - apply_selection(lines, &hunks, old_lines, false, true)? - }; - - repo_write_file(&repo, file_path, new_content.as_str())?; - - Ok(()) -} +const NEWLINE: char = '\n'; #[derive(Default)] struct NewFromOldContent { @@ -54,7 +25,7 @@ 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') { + let line = if line.ends_with(NEWLINE) { line[0..line.len() - 1].to_string() } else { line @@ -89,18 +60,18 @@ impl NewFromOldContent { self.lines.push(line.to_string()); } let lines = self.lines.join("\n"); - if lines.ends_with('\n') { + if lines.ends_with(NEWLINE) { lines } else { let mut lines = lines; - lines.push('\n'); + lines.push(NEWLINE); 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( +pub(crate) fn apply_selection( lines: &[DiffLinePosition], hunks: &[HunkLines], old_lines: Vec<&str>, @@ -132,7 +103,6 @@ fn apply_selection( } if first_hunk_encountered { - // catchup until this hunk new_content.catchup_to_hunkstart(hunk_start, &old_lines); for hunk_line in &hunk.lines { @@ -190,7 +160,10 @@ fn apply_selection( Ok(new_content.finish(&old_lines)) } -fn load_file(repo: &Repository, file_path: &str) -> Result { +pub(crate) fn load_file( + repo: &Repository, + file_path: &str, +) -> Result { let repo_path = work_dir(repo)?; let mut file = File::open(repo_path.join(file_path).as_path())?; let mut res = String::new(); @@ -198,322 +171,3 @@ fn load_file(repo: &Repository, file_path: &str) -> Result { 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); - } -} diff --git a/asyncgit/src/sync/staging/stage_tracked.rs b/asyncgit/src/sync/staging/stage_tracked.rs new file mode 100644 index 0000000000..2872c3165d --- /dev/null +++ b/asyncgit/src/sync/staging/stage_tracked.rs @@ -0,0 +1,157 @@ +use super::apply_selection; +use crate::{ + error::{Error, Result}, + sync::{ + diff::DiffLinePosition, + patches::get_file_diff_patch_and_hunklines, utils::repo, + }, +}; +use scopetime::scope_time; +use std::path::Path; + +/// +pub fn stage_lines( + repo_path: &str, + file_path: &str, + is_stage: bool, + lines: &[DiffLinePosition], +) -> Result<()> { + scope_time!("stage_lines"); + + if lines.is_empty() { + return Ok(()); + } + + let repo = repo(repo_path)?; + // log::debug!("stage_lines: {:?}", lines); + + let mut index = repo.index()?; + index.read(true)?; + let mut idx = + index.get_path(Path::new(file_path), 0).ok_or_else(|| { + Error::Generic(String::from( + "only non new files supported", + )) + })?; + let blob = repo.find_blob(idx.id)?; + let indexed_content = String::from_utf8(blob.content().into())?; + + let new_content = { + let (_patch, hunks) = get_file_diff_patch_and_hunklines( + &repo, file_path, is_stage, false, + )?; + + let old_lines = indexed_content.lines().collect::>(); + + apply_selection(lines, &hunks, old_lines, is_stage, false)? + }; + + let blob_id = repo.blob(new_content.as_bytes())?; + + idx.id = blob_id; + idx.file_size = new_content.as_bytes().len() as u32; + //TODO: can we simply use add_frombuffer? + index.add(&idx)?; + + index.write()?; + index.read(true)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::sync::{ + diff::get_diff, + tests::{get_statuses, repo_init, write_commit_file}, + utils::{repo_write_file, stage_add_file}, + }; + + #[test] + fn test_stage() { + static FILE_1: &str = r"0 +"; + + static FILE_2: &str = r"0 +1 +2 +3 +"; + + 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(); + + stage_lines( + path, + "test.txt", + false, + &[DiffLinePosition { + old_lineno: None, + new_lineno: Some(2), + }], + ) + .unwrap(); + + let diff = + get_diff(path, String::from("test.txt"), true).unwrap(); + + assert_eq!(diff.lines, 3); + assert_eq!( + diff.hunks[0].lines[0].content, + String::from("@@ -1 +1,2 @@\n") + ); + } + + #[test] + fn test_unstage() { + static FILE_1: &str = r"0 +"; + + static FILE_2: &str = r"0 +1 +2 +3 +"; + + 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(); + + assert_eq!(get_statuses(path), (1, 0)); + + stage_add_file(path, &Path::new("test.txt")).unwrap(); + + assert_eq!(get_statuses(path), (0, 1)); + + let diff_before = + get_diff(path, String::from("test.txt"), true).unwrap(); + + assert_eq!(diff_before.lines, 5); + + stage_lines( + path, + "test.txt", + true, + &[DiffLinePosition { + old_lineno: None, + new_lineno: Some(2), + }], + ) + .unwrap(); + + assert_eq!(get_statuses(path), (1, 1)); + + let diff = + get_diff(path, String::from("test.txt"), true).unwrap(); + + assert_eq!(diff.lines, 4); + } +} diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index f4c6dd3aa0..6a87e665b7 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -118,11 +118,11 @@ mod tests { use super::*; use crate::sync::{ commit, get_commit_files, get_commits_info, stage_add_file, - staging::repo_write_file, tests::{ debug_cmd_print, get_statuses, repo_init, write_commit_file, }, + utils::repo_write_file, }; use std::{fs::File, io::Write, path::Path}; diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index f1ecc4ffc9..9ffadbce0a 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -4,7 +4,7 @@ use super::CommitId; use crate::error::{Error, Result}; use git2::{IndexAddOption, Repository, RepositoryOpenFlags}; use scopetime::scope_time; -use std::path::Path; +use std::{fs::File, io::Write, path::Path}; /// #[derive(PartialEq, Debug, Clone)] @@ -167,11 +167,26 @@ pub fn get_config_string( Ok(entry.value().map(|s| s.to_string())) } } -/// helper function + pub(crate) fn bytes2string(bytes: &[u8]) -> Result { Ok(String::from_utf8(bytes.to_vec())?) } +/// 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 tests { use super::*; diff --git a/src/components/diff.rs b/src/components/diff.rs index 2e9809308b..8cbfa59619 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -491,7 +491,7 @@ impl DiffComponent { Ok(()) } - fn queue_update(&mut self) { + fn queue_update(&self) { self.queue .as_ref() .borrow_mut() @@ -514,10 +514,41 @@ impl DiffComponent { } fn reset_lines(&self) { + self.queue.as_ref().borrow_mut().push_back( + InternalEvent::ConfirmAction(Action::ResetLines( + self.current.path.clone(), + self.selected_lines(), + )), + ); + } + + fn stage_lines(&self) { if let Some(diff) = &self.diff { - if self.selected_hunk.is_some() { - let selected_lines: Vec = diff - .hunks + //TODO: support untracked files aswell + if !diff.untracked { + let selected_lines = self.selected_lines(); + + try_or_popup!( + self, + "(un)stage lines:", + sync::stage_lines( + CWD, + &self.current.path, + self.is_stage(), + &selected_lines, + ) + ); + + self.queue_update(); + } + } + } + + fn selected_lines(&self) -> Vec { + self.diff + .as_ref() + .map(|diff| { + diff.hunks .iter() .flat_map(|hunk| hunk.lines.iter()) .enumerate() @@ -533,16 +564,9 @@ impl DiffComponent { None } }) - .collect(); - - self.queue.as_ref().borrow_mut().push_back( - InternalEvent::ConfirmAction(Action::ResetLines( - self.current.path.clone(), - selected_lines, - )), - ); - } - } + .collect() + }) + .unwrap_or_default() } fn reset_untracked(&self) { @@ -678,6 +702,20 @@ impl Component for DiffComponent { true, self.focused && !self.is_stage(), )); + out.push(CommandInfo::new( + strings::commands::diff_lines_stage(&self.key_config), + //TODO: only if any modifications are selected + true, + self.focused && !self.is_stage(), + )); + out.push(CommandInfo::new( + strings::commands::diff_lines_unstage( + &self.key_config, + ), + //TODO: only if any modifications are selected + true, + self.focused && self.is_stage(), + )); } CommandBlocking::PassingOn @@ -733,11 +771,17 @@ impl Component for DiffComponent { } } Ok(true) + } else if e == self.key_config.diff_stage_lines + && !self.is_immutable + { + self.stage_lines(); + Ok(true) } else if e == self.key_config.status_reset_lines && !self.is_immutable && !self.is_stage() { if let Some(diff) = &self.diff { + //TODO: reset untracked lines if !diff.untracked { self.reset_lines(); } diff --git a/src/keys.rs b/src/keys.rs index 42fb553dd4..d1b1538606 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -26,8 +26,6 @@ pub struct KeyConfig { pub tab_stashes: KeyEvent, pub tab_toggle: KeyEvent, pub tab_toggle_reverse: KeyEvent, - pub focus_workdir: KeyEvent, - pub focus_stage: KeyEvent, pub focus_right: KeyEvent, pub focus_left: KeyEvent, pub focus_above: KeyEvent, @@ -53,6 +51,7 @@ pub struct KeyConfig { pub status_reset_item: KeyEvent, pub status_reset_lines: KeyEvent, pub status_ignore_file: KeyEvent, + pub diff_stage_lines: KeyEvent, pub stashing_save: KeyEvent, pub stashing_toggle_untracked: KeyEvent, pub stashing_toggle_index: KeyEvent, @@ -81,8 +80,6 @@ impl Default for KeyConfig { tab_stashes: KeyEvent { code: KeyCode::Char('4'), modifiers: KeyModifiers::empty()}, tab_toggle: KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::empty()}, tab_toggle_reverse: KeyEvent { code: KeyCode::BackTab, modifiers: KeyModifiers::SHIFT}, - focus_workdir: KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::empty()}, - focus_stage: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()}, focus_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()}, focus_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()}, focus_above: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::empty()}, @@ -108,6 +105,7 @@ impl Default for KeyConfig { 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()}, + diff_stage_lines: 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_index: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()}, diff --git a/src/strings.rs b/src/strings.rs index 42945e042f..f7a615b6af 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -21,20 +21,14 @@ pub static PUSH_TAGS_STATES_DONE: &str = "done"; pub static SELECT_BRANCH_POPUP_MSG: &str = "Switch Branch"; -pub fn title_status(key_config: &SharedKeyConfig) -> String { - format!( - "Unstaged Changes [{}]", - key_config.get_hint(key_config.focus_workdir) - ) +pub fn title_status(_key_config: &SharedKeyConfig) -> String { + "Unstaged Changes".to_string() } pub fn title_diff(_key_config: &SharedKeyConfig) -> String { "Diff: ".to_string() } -pub fn title_index(key_config: &SharedKeyConfig) -> String { - format!( - "Staged Changes [{}]", - key_config.get_hint(key_config.focus_stage) - ) +pub fn title_index(_key_config: &SharedKeyConfig) -> String { + "Staged Changes".to_string() } pub fn tab_status(key_config: &SharedKeyConfig) -> String { format!("Status [{}]", key_config.get_hint(key_config.tab_status)) @@ -416,6 +410,30 @@ pub mod commands { CMD_GROUP_DIFF, ) } + pub fn diff_lines_stage( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Stage lines [{}]", + key_config.get_hint(key_config.diff_stage_lines), + ), + "stage selected lines", + CMD_GROUP_DIFF, + ) + } + pub fn diff_lines_unstage( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Unstage lines [{}]", + key_config.get_hint(key_config.diff_stage_lines), + ), + "unstage selected lines", + CMD_GROUP_DIFF, + ) + } pub fn diff_hunk_remove( key_config: &SharedKeyConfig, ) -> CommandText { @@ -460,18 +478,6 @@ pub mod commands { ) .hide_help() } - pub fn select_staging( - key_config: &SharedKeyConfig, - ) -> CommandText { - CommandText::new( - format!( - "To stage [{}]", - key_config.get_hint(key_config.focus_stage), - ), - "focus/select staging area", - CMD_GROUP_GENERAL, - ) - } pub fn select_status( key_config: &SharedKeyConfig, ) -> CommandText { @@ -485,18 +491,6 @@ pub mod commands { CMD_GROUP_GENERAL, ) } - pub fn select_unstaged( - key_config: &SharedKeyConfig, - ) -> CommandText { - CommandText::new( - format!( - "To unstaged [{}]", - key_config.get_hint(key_config.focus_workdir), - ), - "focus/select unstaged area", - CMD_GROUP_GENERAL, - ) - } pub fn commit_open(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!( diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 3009d39c8c..78a6611689 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -7,7 +7,7 @@ use crate::{ }, keys::SharedKeyConfig, queue::{Action, InternalEvent, Queue, ResetItem}, - strings::{self, order}, + strings, ui::style::SharedTheme, }; use anyhow::Result; @@ -522,26 +522,6 @@ impl Component for Status { .hidden(), ); - out.push( - CommandInfo::new( - strings::commands::select_staging(&self.key_config), - true, - (self.visible && self.focus == Focus::WorkDir) - || force_all, - ) - .order(order::NAV), - ); - - out.push( - CommandInfo::new( - strings::commands::select_unstaged(&self.key_config), - true, - (self.visible && self.focus == Focus::Stage) - || force_all, - ) - .order(order::NAV), - ); - visibility_blocking(self) } @@ -553,11 +533,7 @@ impl Component for Status { } if let Event::Key(k) = ev { - return if k == self.key_config.focus_workdir { - self.switch_focus(Focus::WorkDir) - } else if k == self.key_config.focus_stage { - self.switch_focus(Focus::Stage) - } else if k == self.key_config.edit_file + return if k == self.key_config.edit_file && (self.can_focus_diff() || self.focus == Focus::Diff) {