drop multiple stashes (#855)

This commit is contained in:
Stephan Dilly 2021-08-18 01:41:33 +02:00 committed by GitHub
parent 5c694bd696
commit 2b85b81a3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 49 deletions

View File

@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
**drop multiple stashes**
![drop-multiple-stashes](assets/drop-multiple-stashes.gif)
**branch name validation** **branch name validation**
![name-validation](assets/branch-validation.gif) ![name-validation](assets/branch-validation.gif)
## Added ## 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)) - 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)) - support deleting remote branch [[@zcorniere](https://github.com/zcorniere)] ([#622](https://github.com/extrawurst/gitui/issues/622))

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -4,12 +4,12 @@ use crate::{
components::{ components::{
event_pump, BlameFileComponent, BranchListComponent, event_pump, BlameFileComponent, BranchListComponent,
CommandBlocking, CommandInfo, CommitComponent, Component, CommandBlocking, CommandInfo, CommitComponent, Component,
CreateBranchComponent, DrawableComponent, ConfirmComponent, CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, HelpComponent, ExternalEditorComponent, HelpComponent,
InspectCommitComponent, MsgComponent, PullComponent, InspectCommitComponent, MsgComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent, PushComponent, PushTagsComponent, RenameBranchComponent,
ResetComponent, RevisionFilesPopup, StashMsgComponent, RevisionFilesPopup, StashMsgComponent, TagCommitComponent,
TagCommitComponent, TagListComponent, TagListComponent,
}, },
input::{Input, InputEvent, InputState}, input::{Input, InputEvent, InputState},
keys::{KeyConfig, SharedKeyConfig}, keys::{KeyConfig, SharedKeyConfig},
@ -42,7 +42,7 @@ pub struct App {
do_quit: bool, do_quit: bool,
help: HelpComponent, help: HelpComponent,
msg: MsgComponent, msg: MsgComponent,
reset: ResetComponent, reset: ConfirmComponent,
commit: CommitComponent, commit: CommitComponent,
blame_file_popup: BlameFileComponent, blame_file_popup: BlameFileComponent,
stashmsg_popup: StashMsgComponent, stashmsg_popup: StashMsgComponent,
@ -91,7 +91,7 @@ impl App {
Self { Self {
input, input,
reset: ResetComponent::new( reset: ConfirmComponent::new(
queue.clone(), queue.clone(),
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
@ -682,9 +682,13 @@ impl App {
} }
} }
Action::StashDrop(_) | Action::StashPop(_) => { Action::StashDrop(_) | Action::StashPop(_) => {
if self.stashlist_tab.action_confirmed(&action) { if let Err(e) = StashList::action_confirmed(&action) {
flags.insert(NeedsUpdate::ALL); self.queue.push(InternalEvent::ShowErrorMsg(
e.to_string(),
));
} }
flags.insert(NeedsUpdate::ALL);
} }
Action::ResetHunk(path, hash) => { Action::ResetHunk(path, hash) => {
sync::reset_hunk(CWD, &path, hash)?; sync::reset_hunk(CWD, &path, hash)?;

View File

@ -5,7 +5,7 @@ use crate::{
Component, DrawableComponent, EventState, ScrollType, Component, DrawableComponent, EventState, ScrollType,
}, },
keys::SharedKeyConfig, keys::SharedKeyConfig,
strings, strings::{self, symbol},
ui::calc_scroll_top, ui::calc_scroll_top,
ui::style::{SharedTheme, Theme}, 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<()> { pub fn copy_entry_hash(&self) -> Result<()> {
if let Some(e) = self.items.iter().nth( if let Some(e) = self.items.iter().nth(
self.selection.saturating_sub(self.items.index_offset()), self.selection.saturating_sub(self.items.index_offset()),
@ -223,14 +240,18 @@ impl CommitList {
ELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 }, 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 = let splitter =
Span::styled(splitter_txt, theme.text(true, selected)); Span::styled(splitter_txt, theme.text(true, selected));
// marker // marker
if let Some(marked) = marked { if let Some(marked) = marked {
txt.push(Span::styled( 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), theme.log_marker(selected),
)); ));
txt.push(splitter.clone()); txt.push(splitter.clone());
@ -433,6 +454,14 @@ impl Component for CommitList {
self.selected_entry().is_some(), self.selected_entry().is_some(),
true, true,
)); ));
out.push(CommandInfo::new(
strings::commands::commit_list_mark(
&self.key_config,
self.selected_entry_marked(),
),
true,
true,
));
CommandBlocking::PassingOn CommandBlocking::PassingOn
} }
} }

View File

@ -45,7 +45,7 @@ pub use pull::PullComponent;
pub use push::PushComponent; pub use push::PushComponent;
pub use push_tags::PushTagsComponent; pub use push_tags::PushTagsComponent;
pub use rename_branch::RenameBranchComponent; pub use rename_branch::RenameBranchComponent;
pub use reset::ResetComponent; pub use reset::ConfirmComponent;
pub use revision_files::RevisionFilesComponent; pub use revision_files::RevisionFilesComponent;
pub use revision_files_popup::RevisionFilesPopup; pub use revision_files_popup::RevisionFilesPopup;
pub use stashmsg::StashMsgComponent; pub use stashmsg::StashMsgComponent;

View File

@ -16,7 +16,7 @@ use tui::{
use ui::style::SharedTheme; use ui::style::SharedTheme;
/// ///
pub struct ResetComponent { pub struct ConfirmComponent {
target: Option<Action>, target: Option<Action>,
visible: bool, visible: bool,
queue: Queue, queue: Queue,
@ -24,7 +24,7 @@ pub struct ResetComponent {
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
} }
impl DrawableComponent for ResetComponent { impl DrawableComponent for ConfirmComponent {
fn draw<B: Backend>( fn draw<B: Backend>(
&self, &self,
f: &mut Frame<B>, f: &mut Frame<B>,
@ -50,7 +50,7 @@ impl DrawableComponent for ResetComponent {
} }
} }
impl Component for ResetComponent { impl Component for ConfirmComponent {
fn commands( fn commands(
&self, &self,
out: &mut Vec<CommandInfo>, out: &mut Vec<CommandInfo>,
@ -101,7 +101,7 @@ impl Component for ResetComponent {
} }
} }
impl ResetComponent { impl ConfirmComponent {
/// ///
pub fn new( pub fn new(
queue: Queue, queue: Queue,
@ -139,11 +139,11 @@ impl ResetComponent {
strings::confirm_title_reset(), strings::confirm_title_reset(),
strings::confirm_msg_reset(), strings::confirm_msg_reset(),
), ),
Action::StashDrop(_) => ( Action::StashDrop(ids) => (
strings::confirm_title_stashdrop( 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(_) => ( Action::StashPop(_) => (
strings::confirm_title_stashpop(&self.key_config), strings::confirm_title_stashpop(&self.key_config),

View File

@ -1,3 +1,4 @@
use crate::strings::symbol;
use crate::ui::Size; use crate::ui::Size;
use crate::{ use crate::{
components::{ components::{
@ -169,7 +170,7 @@ impl TextInputComponent {
let cursor_highlighting = { let cursor_highlighting = {
let mut h = HashMap::with_capacity(2); let mut h = HashMap::with_capacity(2);
h.insert("\n", "\u{21b5}\n\r"); h.insert("\n", "\u{21b5}\n\r");
h.insert(" ", "\u{00B7}"); h.insert(" ", symbol::WHITESPACE);
h h
}; };
@ -470,7 +471,10 @@ mod tests {
get_style(&txt.lines[0].0[0]), get_style(&txt.lines[0].0[0]),
Some(&not_underlined) Some(&not_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!( assert_eq!(
get_style(&txt.lines[0].0[1]), get_style(&txt.lines[0].0[1]),
Some(&underlined_whitespace) Some(&underlined_whitespace)

View File

@ -15,7 +15,7 @@ use std::{
rc::Rc, rc::Rc,
}; };
use crate::args::get_app_config_path; use crate::{args::get_app_config_path, strings::symbol};
pub type SharedKeyConfig = Rc<KeyConfig>; pub type SharedKeyConfig = Rc<KeyConfig>;
@ -248,6 +248,7 @@ impl KeyConfig {
self.get_key_symbol(ev.code) self.get_key_symbol(ev.code)
) )
} }
KeyCode::Char(' ') => String::from(symbol::SPACE),
KeyCode::Char(c) => { KeyCode::Char(c) => {
format!( format!(
"{}{}", "{}{}",

View File

@ -30,7 +30,7 @@ pub enum Action {
Reset(ResetItem), Reset(ResetItem),
ResetHunk(String, u64), ResetHunk(String, u64),
ResetLines(String, Vec<DiffLinePosition>), ResetLines(String, Vec<DiffLinePosition>),
StashDrop(CommitId), StashDrop(Vec<CommitId>),
StashPop(CommitId), StashPop(CommitId),
DeleteBranch(String, bool), DeleteBranch(String, bool),
DeleteTag(String), DeleteTag(String),

View File

@ -1,3 +1,5 @@
use asyncgit::sync::CommitId;
use crate::keys::SharedKeyConfig; use crate::keys::SharedKeyConfig;
pub mod order { 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_PUSHING: &str = "pushing";
pub static PUSH_TAGS_STATES_DONE: &str = "done"; 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 { pub fn title_branches() -> String {
"Branches".to_string() "Branches".to_string()
} }
@ -103,8 +112,9 @@ pub fn confirm_title_reset() -> String {
} }
pub fn confirm_title_stashdrop( pub fn confirm_title_stashdrop(
_key_config: &SharedKeyConfig, _key_config: &SharedKeyConfig,
multiple: bool,
) -> String { ) -> String {
"Drop".to_string() format!("Drop Stash{}", if multiple { "es" } else { "" })
} }
pub fn confirm_title_stashpop( pub fn confirm_title_stashpop(
_key_config: &SharedKeyConfig, _key_config: &SharedKeyConfig,
@ -151,8 +161,21 @@ pub fn confirm_msg_reset_lines(lines: usize) -> String {
} }
pub fn confirm_msg_stashdrop( pub fn confirm_msg_stashdrop(
_key_config: &SharedKeyConfig, _key_config: &SharedKeyConfig,
ids: &[CommitId],
) -> String { ) -> 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::<Vec<_>>()
.join(", ")
)
} }
pub fn confirm_msg_stashpop(_key_config: &SharedKeyConfig) -> String { pub fn confirm_msg_stashpop(_key_config: &SharedKeyConfig) -> String {
"The stash will be applied and removed from the stash list. Confirm stash pop?" "The stash will be applied and removed from the stash list. Confirm stash pop?"
@ -399,6 +422,20 @@ pub mod commands {
CMD_GROUP_GENERAL, 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 { pub fn copy(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!( format!(
@ -828,10 +865,16 @@ pub mod commands {
} }
pub fn stashlist_drop( pub fn stashlist_drop(
key_config: &SharedKeyConfig, key_config: &SharedKeyConfig,
marked: usize,
) -> CommandText { ) -> CommandText {
CommandText::new( CommandText::new(
format!( format!(
"Drop [{}]", "Drop{} [{}]",
if marked == 0 {
String::default()
} else {
format!(" {}", marked)
},
key_config.get_hint(key_config.stash_drop), key_config.get_hint(key_config.stash_drop),
), ),
"drop selected stash", "drop selected stash",

View File

@ -71,9 +71,13 @@ impl StashList {
} }
fn drop_stash(&mut self) { fn drop_stash(&mut self) {
if let Some(e) = self.list.selected_entry() { if self.list.marked_count() > 0 {
self.queue.push(InternalEvent::ConfirmAction( 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 /// Called when a pending stash action has been confirmed
pub fn action_confirmed(&self, action: &Action) -> bool { pub fn action_confirmed(action: &Action) -> Result<()> {
match *action { match action {
Action::StashDrop(id) => Self::drop(id), Action::StashDrop(ids) => Self::drop(ids)?,
Action::StashPop(id) => self.pop(id), Action::StashPop(id) => Self::pop(*id)?,
_ => false, _ => (),
} };
Ok(())
} }
fn drop(id: CommitId) -> bool { fn drop(ids: &[CommitId]) -> Result<()> {
sync::stash_drop(CWD, id).is_ok() for id in ids {
sync::stash_drop(CWD, *id)?;
} }
fn pop(&self, id: CommitId) -> bool { Ok(())
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, true,
)); ));
out.push(CommandInfo::new( out.push(CommandInfo::new(
strings::commands::stashlist_drop(&self.key_config), strings::commands::stashlist_drop(
&self.key_config,
self.list.marked_count(),
),
selection_valid, selection_valid,
true, true,
)); ));