diff --git a/asyncgit/src/sync/merge.rs b/asyncgit/src/sync/merge.rs new file mode 100644 index 0000000000..c61e7411fa --- /dev/null +++ b/asyncgit/src/sync/merge.rs @@ -0,0 +1,49 @@ +use crate::{ + error::{Error, Result}, + sync::{reset_stage, reset_workdir, utils}, +}; +use git2::{BranchType, MergeOptions}; +use scopetime::scope_time; + +/// does these steps: +/// * reset all staged changes, +/// * revert all changes in workdir +/// * cleanup repo merge state +pub fn abort_merge(repo_path: &str) -> Result<()> { + scope_time!("cleanup_state"); + + let repo = utils::repo(repo_path)?; + + reset_stage(repo_path, "*")?; + reset_workdir(repo_path, "*")?; + + repo.cleanup_state()?; + + Ok(()) +} + +/// +pub fn merge_branch(repo_path: &str, branch: &str) -> Result<()> { + scope_time!("merge_branch"); + + let repo = utils::repo(repo_path)?; + + let branch = repo.find_branch(branch, BranchType::Local)?; + + let id = branch.into_reference().peel_to_commit()?; + + let annotated = repo.find_annotated_commit(id.id())?; + + let (analysis, _) = repo.merge_analysis(&[&annotated])?; + + //TODO: support merge on unborn + if analysis.is_unborn() { + return Err(Error::Generic("head is unborn".into())); + } + + let mut opt = MergeOptions::default(); + + repo.merge(&[&annotated], Some(&mut opt), None)?; + + Ok(()) +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 1bcba4b860..7bb41c95f2 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -15,6 +15,7 @@ mod hooks; mod hunks; mod ignore; mod logwalker; +mod merge; mod patches; pub mod remotes; mod reset; @@ -49,6 +50,7 @@ pub use hooks::{ pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::LogWalker; +pub use merge::{abort_merge, merge_branch}; pub use remotes::{ get_default_remote, get_remotes, push::AsyncProgress, tags::PushTagsProgress, diff --git a/src/app.rs b/src/app.rs index 801f01052d..0daa984a32 100644 --- a/src/app.rs +++ b/src/app.rs @@ -607,6 +607,10 @@ impl App { self.pull_popup.try_conflict_free_merge(rebase); flags.insert(NeedsUpdate::ALL); } + Action::AbortMerge => { + self.status_tab.abort_merge(); + flags.insert(NeedsUpdate::ALL); + } }; Ok(()) diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs index de41295c50..c79a22e407 100644 --- a/src/components/branchlist.rs +++ b/src/components/branchlist.rs @@ -12,7 +12,7 @@ use crate::{ use anyhow::Result; use asyncgit::{ sync::{ - branch::checkout_remote_branch, checkout_branch, + self, branch::checkout_remote_branch, checkout_branch, get_branches_info, BranchInfo, }, CWD, @@ -150,6 +150,14 @@ impl Component for BranchListComponent { self.local, )); + out.push(CommandInfo::new( + strings::commands::merge_branch_popup( + &self.key_config, + ), + !self.selection_is_cur_branch(), + self.local, + )); + out.push(CommandInfo::new( strings::commands::rename_branch_popup( &self.key_config, @@ -222,6 +230,16 @@ impl Component for BranchListComponent { ), ), ); + } else if e == self.key_config.merge_branch + && !self.selection_is_cur_branch() + && self.valid_selection() + { + try_or_popup!( + self, + "merge branch error:", + self.merge_branch() + ); + self.hide(); } else if e == self.key_config.tab_toggle { self.local = !self.local; self.update_branches()?; @@ -294,6 +312,16 @@ impl BranchListComponent { !self.branches.is_empty() } + fn merge_branch(&self) -> Result<()> { + if let Some(branch) = + self.branches.get(usize::from(self.selection)) + { + sync::merge_branch(CWD, &branch.name)?; + } + + Ok(()) + } + fn selection_is_cur_branch(&self) -> bool { self.branches .iter() diff --git a/src/components/reset.rs b/src/components/reset.rs index eeef90d1ea..2004545fab 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -138,8 +138,8 @@ impl ResetComponent { if let Some(ref a) = self.target { return match a { Action::Reset(_) => ( - strings::confirm_title_reset(&self.key_config), - strings::confirm_msg_reset(&self.key_config), + strings::confirm_title_reset(), + strings::confirm_msg_reset(), ), Action::StashDrop(_) => ( strings::confirm_title_stashdrop( @@ -152,12 +152,12 @@ impl ResetComponent { strings::confirm_msg_stashpop(&self.key_config), ), Action::ResetHunk(_, _) => ( - strings::confirm_title_reset(&self.key_config), + strings::confirm_title_reset(), 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()), + strings::confirm_title_reset(), + strings::confirm_msg_reset_lines(lines.len()), ), Action::DeleteBranch(branch_ref) => ( strings::confirm_title_delete_branch( @@ -181,6 +181,10 @@ impl ResetComponent { strings::confirm_title_merge(&self.key_config,*rebase), strings::confirm_msg_merge(&self.key_config,*incoming,*rebase), ), + Action::AbortMerge => ( + strings::confirm_title_abortmerge(), + strings::confirm_msg_abortmerge(), + ), }; } diff --git a/src/keys.rs b/src/keys.rs index 168ed72cdf..6ee72ea9f5 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -68,9 +68,11 @@ pub struct KeyConfig { pub rename_branch: KeyEvent, pub select_branch: KeyEvent, pub delete_branch: KeyEvent, + pub merge_branch: KeyEvent, pub push: KeyEvent, pub force_push: KeyEvent, pub pull: KeyEvent, + pub abort_merge: KeyEvent, } #[rustfmt::skip] @@ -121,13 +123,15 @@ impl Default for KeyConfig { log_tag_commit: KeyEvent { code: KeyCode::Char('t'), modifiers: KeyModifiers::empty()}, commit_amend: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL}, copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()}, - create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::NONE}, - rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::NONE}, - select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::NONE}, + create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, + rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()}, + select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, delete_branch: KeyEvent{code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, + merge_branch: KeyEvent{code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, + abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, } } } diff --git a/src/queue.rs b/src/queue.rs index 131de06593..a5226e51f5 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -33,6 +33,7 @@ pub enum Action { DeleteBranch(String), ForcePush(String, bool), PullMerge { incoming: usize, rebase: bool }, + AbortMerge, } /// diff --git a/src/strings.rs b/src/strings.rs index 01ce8faa16..c52298a4db 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -86,7 +86,7 @@ pub fn stash_popup_title(_key_config: &SharedKeyConfig) -> String { pub fn stash_popup_msg(_key_config: &SharedKeyConfig) -> String { "type name (optional)".to_string() } -pub fn confirm_title_reset(_key_config: &SharedKeyConfig) -> String { +pub fn confirm_title_reset() -> String { "Reset".to_string() } pub fn confirm_title_stashdrop( @@ -120,13 +120,17 @@ pub fn confirm_msg_merge( format!("Merge of {} incoming commits?", incoming) } } -pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String { + +pub fn confirm_title_abortmerge() -> String { + "Abort merge?".to_string() +} +pub fn confirm_msg_abortmerge() -> String { + "This will revert all changes. Are you sure?".to_string() +} +pub fn confirm_msg_reset() -> String { "confirm file reset?".to_string() } -pub fn confirm_msg_reset_lines( - _key_config: &SharedKeyConfig, - lines: usize, -) -> String { +pub fn confirm_msg_reset_lines(lines: usize) -> String { format!( "are you sure you want to discard {} selected lines?", lines @@ -520,6 +524,16 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn abort_merge(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Abort merge [{}]", + key_config.get_hint(key_config.abort_merge), + ), + "abort ongoing merge", + CMD_GROUP_GENERAL, + ) + } pub fn select_staging( key_config: &SharedKeyConfig, ) -> CommandText { @@ -918,6 +932,18 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn merge_branch_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Merge [{}]", + key_config.get_hint(key_config.merge_branch), + ), + "merge a branch", + CMD_GROUP_GENERAL, + ) + } pub fn select_branch_popup( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/status.rs b/src/tabs/status.rs index bb5f285a1e..cccca38321 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -8,7 +8,7 @@ use crate::{ }, keys::SharedKeyConfig, queue::{Action, InternalEvent, Queue, ResetItem}, - strings, + strings, try_or_popup, ui::style::SharedTheme, }; use anyhow::Result; @@ -465,6 +465,61 @@ impl Status { .as_ref() .map_or(true, |state| state.ahead > 0) } + + fn can_abort_merge() -> bool { + sync::repo_state(CWD).unwrap_or(RepoState::Clean) + == RepoState::Merge + } + + pub fn abort_merge(&self) { + try_or_popup!(self, "abort merge", sync::abort_merge(CWD)) + } + + fn commands_nav( + &self, + out: &mut Vec, + force_all: bool, + ) { + let focus_on_diff = self.is_focus_on_diff(); + out.push( + CommandInfo::new( + strings::commands::diff_focus_left(&self.key_config), + true, + (self.visible && focus_on_diff) || force_all, + ) + .order(strings::order::NAV), + ); + out.push( + CommandInfo::new( + strings::commands::diff_focus_right(&self.key_config), + self.can_focus_diff(), + (self.visible && !focus_on_diff) || force_all, + ) + .order(strings::order::NAV), + ); + out.push( + CommandInfo::new( + strings::commands::select_staging(&self.key_config), + !focus_on_diff, + (self.visible + && !focus_on_diff + && self.focus == Focus::WorkDir) + || force_all, + ) + .order(strings::order::NAV), + ); + out.push( + CommandInfo::new( + strings::commands::select_unstaged(&self.key_config), + !focus_on_diff, + (self.visible + && !focus_on_diff + && self.focus == Focus::Stage) + || force_all, + ) + .order(strings::order::NAV), + ); + } } impl Component for Status { @@ -507,6 +562,12 @@ impl Component for Status { true, !focus_on_diff, )); + + out.push(CommandInfo::new( + strings::commands::abort_merge(&self.key_config), + true, + Self::can_abort_merge() || force_all, + )); } { @@ -519,52 +580,6 @@ impl Component for Status { }, self.visible || force_all, )); - out.push( - CommandInfo::new( - strings::commands::diff_focus_left( - &self.key_config, - ), - true, - (self.visible && focus_on_diff) || force_all, - ) - .order(strings::order::NAV), - ); - out.push( - CommandInfo::new( - strings::commands::diff_focus_right( - &self.key_config, - ), - self.can_focus_diff(), - (self.visible && !focus_on_diff) || force_all, - ) - .order(strings::order::NAV), - ); - out.push( - CommandInfo::new( - strings::commands::select_staging( - &self.key_config, - ), - !focus_on_diff, - (self.visible - && !focus_on_diff - && self.focus == Focus::WorkDir) - || force_all, - ) - .order(strings::order::NAV), - ); - out.push( - CommandInfo::new( - strings::commands::select_unstaged( - &self.key_config, - ), - !focus_on_diff, - (self.visible - && !focus_on_diff - && self.focus == Focus::Stage) - || force_all, - ) - .order(strings::order::NAV), - ); out.push( CommandInfo::new( @@ -576,6 +591,8 @@ impl Component for Status { ) .hidden(), ); + + self.commands_nav(out, force_all); } visibility_blocking(self) @@ -653,6 +670,16 @@ impl Component for Status { && !self.is_focus_on_diff() { self.pull(); + Ok(EventState::Consumed) + } else if k == self.key_config.abort_merge + && Self::can_abort_merge() + { + self.queue.borrow_mut().push_back( + InternalEvent::ConfirmAction( + Action::AbortMerge, + ), + ); + Ok(EventState::Consumed) } else { Ok(EventState::NotConsumed) diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index abdf498631..0a64ae4daa 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -72,6 +72,9 @@ rename_branch: ( code: Char('r'), modifiers: ( bits: 0,),), select_branch: ( code: Char('b'), modifiers: ( bits: 0,),), delete_branch: ( code: Char('D'), modifiers: ( bits: 1,),), + merge_branch: ( code: Char('m'), modifiers: ( bits: 0,),), + abort_merge: ( code: Char('M'), modifiers: ( bits: 1,),), + push: ( code: Char('p'), modifiers: ( bits: 0,),), force_push: ( code: Char('P'), modifiers: ( bits: 1,),), pull: ( code: Char('f'), modifiers: ( bits: 0,),),