diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf2bd14..28c09db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +**drop multiple stashes** + +![drop-multiple-stashes](assets/drop-multiple-stashes.gif) **branch name validation** ![name-validation](assets/branch-validation.gif) ## Added +- mark and drop multiple stashes ([#854](https://github.com/extrawurst/gitui/issues/854)) - check branch name validity while typing ([#559](https://github.com/extrawurst/gitui/issues/559)) - support deleting remote branch [[@zcorniere](https://github.com/zcorniere)] ([#622](https://github.com/extrawurst/gitui/issues/622)) diff --git a/assets/drop-multiple-stashes.gif b/assets/drop-multiple-stashes.gif new file mode 100644 index 00000000..ae0ac262 Binary files /dev/null and b/assets/drop-multiple-stashes.gif differ diff --git a/src/app.rs b/src/app.rs index e58c889f..e938547a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,12 +4,12 @@ use crate::{ components::{ event_pump, BlameFileComponent, BranchListComponent, CommandBlocking, CommandInfo, CommitComponent, Component, - CreateBranchComponent, DrawableComponent, + ConfirmComponent, CreateBranchComponent, DrawableComponent, ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PullComponent, PushComponent, PushTagsComponent, RenameBranchComponent, - ResetComponent, RevisionFilesPopup, StashMsgComponent, - TagCommitComponent, TagListComponent, + RevisionFilesPopup, StashMsgComponent, TagCommitComponent, + TagListComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -42,7 +42,7 @@ pub struct App { do_quit: bool, help: HelpComponent, msg: MsgComponent, - reset: ResetComponent, + reset: ConfirmComponent, commit: CommitComponent, blame_file_popup: BlameFileComponent, stashmsg_popup: StashMsgComponent, @@ -91,7 +91,7 @@ impl App { Self { input, - reset: ResetComponent::new( + reset: ConfirmComponent::new( queue.clone(), theme.clone(), key_config.clone(), @@ -682,9 +682,13 @@ impl App { } } Action::StashDrop(_) | Action::StashPop(_) => { - if self.stashlist_tab.action_confirmed(&action) { - flags.insert(NeedsUpdate::ALL); + if let Err(e) = StashList::action_confirmed(&action) { + self.queue.push(InternalEvent::ShowErrorMsg( + e.to_string(), + )); } + + flags.insert(NeedsUpdate::ALL); } Action::ResetHunk(path, hash) => { sync::reset_hunk(CWD, &path, hash)?; diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 43ad68ef..b2d0806f 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -5,7 +5,7 @@ use crate::{ Component, DrawableComponent, EventState, ScrollType, }, keys::SharedKeyConfig, - strings, + strings::{self, symbol}, ui::calc_scroll_top, ui::style::{SharedTheme, Theme}, }; @@ -120,6 +120,23 @@ impl CommitList { ) } + /// + pub fn selected_entry_marked(&self) -> bool { + self.selected_entry() + .and_then(|e| self.is_marked(&e.id)) + .unwrap_or_default() + } + + /// + pub fn marked_count(&self) -> usize { + self.marked.len() + } + + /// + pub fn marked(&self) -> &[CommitId] { + &self.marked + } + pub fn copy_entry_hash(&self) -> Result<()> { if let Some(e) = self.items.iter().nth( self.selection.saturating_sub(self.items.index_offset()), @@ -223,14 +240,18 @@ impl CommitList { ELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 }, ); - let splitter_txt = Cow::from(" "); + let splitter_txt = Cow::from(symbol::EMPTY_SPACE); let splitter = Span::styled(splitter_txt, theme.text(true, selected)); // marker if let Some(marked) = marked { txt.push(Span::styled( - Cow::from(if marked { "\u{2713}" } else { " " }), + Cow::from(if marked { + symbol::CHECKMARK + } else { + symbol::EMPTY_SPACE + }), theme.log_marker(selected), )); txt.push(splitter.clone()); @@ -433,6 +454,14 @@ impl Component for CommitList { self.selected_entry().is_some(), true, )); + out.push(CommandInfo::new( + strings::commands::commit_list_mark( + &self.key_config, + self.selected_entry_marked(), + ), + true, + true, + )); CommandBlocking::PassingOn } } diff --git a/src/components/mod.rs b/src/components/mod.rs index d9d0916b..358013cf 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -45,7 +45,7 @@ pub use pull::PullComponent; pub use push::PushComponent; pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; -pub use reset::ResetComponent; +pub use reset::ConfirmComponent; pub use revision_files::RevisionFilesComponent; pub use revision_files_popup::RevisionFilesPopup; pub use stashmsg::StashMsgComponent; diff --git a/src/components/reset.rs b/src/components/reset.rs index 1b00ef0e..02bef26e 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -16,7 +16,7 @@ use tui::{ use ui::style::SharedTheme; /// -pub struct ResetComponent { +pub struct ConfirmComponent { target: Option, visible: bool, queue: Queue, @@ -24,7 +24,7 @@ pub struct ResetComponent { key_config: SharedKeyConfig, } -impl DrawableComponent for ResetComponent { +impl DrawableComponent for ConfirmComponent { fn draw( &self, f: &mut Frame, @@ -50,7 +50,7 @@ impl DrawableComponent for ResetComponent { } } -impl Component for ResetComponent { +impl Component for ConfirmComponent { fn commands( &self, out: &mut Vec, @@ -101,7 +101,7 @@ impl Component for ResetComponent { } } -impl ResetComponent { +impl ConfirmComponent { /// pub fn new( queue: Queue, @@ -139,11 +139,11 @@ impl ResetComponent { strings::confirm_title_reset(), strings::confirm_msg_reset(), ), - Action::StashDrop(_) => ( + Action::StashDrop(ids) => ( strings::confirm_title_stashdrop( - &self.key_config, + &self.key_config,ids.len()>1 ), - strings::confirm_msg_stashdrop(&self.key_config), + strings::confirm_msg_stashdrop(&self.key_config,ids), ), Action::StashPop(_) => ( strings::confirm_title_stashpop(&self.key_config), diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 63c94527..072e5b49 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -1,3 +1,4 @@ +use crate::strings::symbol; use crate::ui::Size; use crate::{ components::{ @@ -169,7 +170,7 @@ impl TextInputComponent { let cursor_highlighting = { let mut h = HashMap::with_capacity(2); h.insert("\n", "\u{21b5}\n\r"); - h.insert(" ", "\u{00B7}"); + h.insert(" ", symbol::WHITESPACE); h }; @@ -470,7 +471,10 @@ mod tests { get_style(&txt.lines[0].0[0]), Some(¬_underlined) ); - assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{00B7}")); + assert_eq!( + get_text(&txt.lines[0].0[1]), + Some(symbol::WHITESPACE) + ); assert_eq!( get_style(&txt.lines[0].0[1]), Some(&underlined_whitespace) diff --git a/src/keys.rs b/src/keys.rs index e511fc31..9666abcd 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -15,7 +15,7 @@ use std::{ rc::Rc, }; -use crate::args::get_app_config_path; +use crate::{args::get_app_config_path, strings::symbol}; pub type SharedKeyConfig = Rc; @@ -248,6 +248,7 @@ impl KeyConfig { self.get_key_symbol(ev.code) ) } + KeyCode::Char(' ') => String::from(symbol::SPACE), KeyCode::Char(c) => { format!( "{}{}", diff --git a/src/queue.rs b/src/queue.rs index cac4ab9e..ff1c00dc 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -30,7 +30,7 @@ pub enum Action { Reset(ResetItem), ResetHunk(String, u64), ResetLines(String, Vec), - StashDrop(CommitId), + StashDrop(Vec), StashPop(CommitId), DeleteBranch(String, bool), DeleteTag(String), diff --git a/src/strings.rs b/src/strings.rs index 949cbed2..6f1f3fef 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1,3 +1,5 @@ +use asyncgit::sync::CommitId; + use crate::keys::SharedKeyConfig; pub mod order { @@ -20,6 +22,13 @@ pub static PUSH_TAGS_STATES_FETCHING: &str = "fetching"; pub static PUSH_TAGS_STATES_PUSHING: &str = "pushing"; pub static PUSH_TAGS_STATES_DONE: &str = "done"; +pub mod symbol { + pub const WHITESPACE: &str = "\u{00B7}"; //· + pub const CHECKMARK: &str = "\u{2713}"; //✓ + pub const SPACE: &str = "\u{02FD}"; //˽ + pub const EMPTY_SPACE: &str = " "; +} + pub fn title_branches() -> String { "Branches".to_string() } @@ -103,8 +112,9 @@ pub fn confirm_title_reset() -> String { } pub fn confirm_title_stashdrop( _key_config: &SharedKeyConfig, + multiple: bool, ) -> String { - "Drop".to_string() + format!("Drop Stash{}", if multiple { "es" } else { "" }) } pub fn confirm_title_stashpop( _key_config: &SharedKeyConfig, @@ -151,8 +161,21 @@ pub fn confirm_msg_reset_lines(lines: usize) -> String { } pub fn confirm_msg_stashdrop( _key_config: &SharedKeyConfig, + ids: &[CommitId], ) -> String { - "confirm stash drop?".to_string() + format!( + "Sure you want to drop following {}stash{}?\n\n{}", + if ids.len() > 1 { + format!("{} ", ids.len()) + } else { + String::default() + }, + if ids.len() > 1 { "es" } else { "" }, + ids.iter() + .map(CommitId::get_short_string) + .collect::>() + .join(", ") + ) } pub fn confirm_msg_stashpop(_key_config: &SharedKeyConfig) -> String { "The stash will be applied and removed from the stash list. Confirm stash pop?" @@ -399,6 +422,20 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn commit_list_mark( + key_config: &SharedKeyConfig, + marked: bool, + ) -> CommandText { + CommandText::new( + format!( + "{} [{}]", + if marked { "Unmark" } else { "Mark" }, + key_config.get_hint(key_config.log_mark_commit), + ), + "mark multiple commits", + CMD_GROUP_GENERAL, + ) + } pub fn copy(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!( @@ -828,10 +865,16 @@ pub mod commands { } pub fn stashlist_drop( key_config: &SharedKeyConfig, + marked: usize, ) -> CommandText { CommandText::new( format!( - "Drop [{}]", + "Drop{} [{}]", + if marked == 0 { + String::default() + } else { + format!(" {}", marked) + }, key_config.get_hint(key_config.stash_drop), ), "drop selected stash", diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index f7c50309..339f88e0 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -71,9 +71,13 @@ impl StashList { } fn drop_stash(&mut self) { - if let Some(e) = self.list.selected_entry() { + if self.list.marked_count() > 0 { self.queue.push(InternalEvent::ConfirmAction( - Action::StashDrop(e.id), + Action::StashDrop(self.list.marked().to_vec()), + )); + } else if let Some(e) = self.list.selected_entry() { + self.queue.push(InternalEvent::ConfirmAction( + Action::StashDrop(vec![e.id]), )); } } @@ -93,31 +97,27 @@ impl StashList { } /// Called when a pending stash action has been confirmed - pub fn action_confirmed(&self, action: &Action) -> bool { - match *action { - Action::StashDrop(id) => Self::drop(id), - Action::StashPop(id) => self.pop(id), - _ => false, - } + pub fn action_confirmed(action: &Action) -> Result<()> { + match action { + Action::StashDrop(ids) => Self::drop(ids)?, + Action::StashPop(id) => Self::pop(*id)?, + _ => (), + }; + + Ok(()) } - fn drop(id: CommitId) -> bool { - sync::stash_drop(CWD, id).is_ok() + fn drop(ids: &[CommitId]) -> Result<()> { + for id in ids { + sync::stash_drop(CWD, *id)?; + } + + Ok(()) } - fn pop(&self, id: CommitId) -> bool { - match sync::stash_pop(CWD, id) { - Ok(_) => { - self.queue.push(InternalEvent::TabSwitch); - true - } - Err(e) => { - self.queue.push(InternalEvent::ShowErrorMsg( - format!("stash pop error:\n{}", e,), - )); - true - } - } + fn pop(id: CommitId) -> Result<()> { + sync::stash_pop(CWD, id)?; + Ok(()) } } @@ -155,7 +155,10 @@ impl Component for StashList { true, )); out.push(CommandInfo::new( - strings::commands::stashlist_drop(&self.key_config), + strings::commands::stashlist_drop( + &self.key_config, + self.list.marked_count(), + ), selection_valid, true, ));