diff --git a/CHANGELOG.md b/CHANGELOG.md index 02ad5dfb8a..c8ff450101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Added +- undo-last-commit command under `[U]` key [[@remique](https://github.com/remique)] ([#758](https://github.com/extrawurst/gitui/issues/758)) + ## Fixed - openssl vendoring broken on macos ([#772](https://github.com/extrawurst/gitui/issues/772)) diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index e1c245eb34..1fbafff525 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -149,6 +149,21 @@ pub fn stage_add_all(repo_path: &str, pattern: &str) -> Result<()> { Ok(()) } +/// Undo last commit in repo +pub fn undo_last_commit(repo_path: &str) -> Result<()> { + let repo = repo(repo_path)?; + let previous_commit = repo.revparse_single("HEAD~")?; + + Repository::reset( + &repo, + &previous_commit, + git2::ResetType::Soft, + None, + )?; + + Ok(()) +} + /// stage a removed file pub fn stage_addremoved(repo_path: &str, path: &Path) -> Result<()> { scope_time!("stage_addremoved"); @@ -206,9 +221,11 @@ mod tests { use super::*; use crate::sync::{ commit, + diff::get_diff, status::{get_status, StatusType}, tests::{ - debug_cmd_print, get_statuses, repo_init, repo_init_empty, + debug_cmd_print, get_statuses, repo_init, + repo_init_empty, write_commit_file, }, }; use std::{ @@ -282,6 +299,43 @@ mod tests { Ok(()) } + #[test] + fn test_undo_commit_empty_repo() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + // expect to fail + assert!(undo_last_commit(repo_path).is_err()); + } + + #[test] + fn test_undo_commit() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + // write commit file test.txt + let c1 = + write_commit_file(&repo, "test.txt", "content1", "c1"); + let _c2 = + write_commit_file(&repo, "test.txt", "content2", "c2"); + assert!(undo_last_commit(repo_path).is_ok()); + + // Make sure that HEAD points to c1 + assert_eq!(c1, get_head_repo(&repo).unwrap()); + + // Make sure that now we have 1 file staged + assert_eq!(get_statuses(repo_path), (0, 1)); + + // And that file is test.txt + let diff = get_diff(repo_path, "test.txt", true).unwrap(); + assert_eq!( + diff.hunks[0].lines[0].content, + String::from("@@ -1 +1 @@\n") + ); + } + #[test] fn test_not_staging_untracked_folder() -> Result<()> { let (_td, repo) = repo_init().unwrap(); diff --git a/src/keys.rs b/src/keys.rs index d424785fb8..9ace952513 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -81,6 +81,7 @@ pub struct KeyConfig { pub force_push: KeyEvent, pub pull: KeyEvent, pub abort_merge: KeyEvent, + pub undo_commit: KeyEvent, } #[rustfmt::skip] @@ -144,6 +145,7 @@ impl Default for KeyConfig { select_tag: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()}, push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, + undo_commit: KeyEvent { code: KeyCode::Char('U'), modifiers: KeyModifiers::SHIFT}, pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, diff --git a/src/strings.rs b/src/strings.rs index b1eeabf191..ee57e7b6c0 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -579,6 +579,16 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn undo_commit(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Undo Commit [{}]", + key_config.get_hint(key_config.undo_commit), + ), + "undo last commit", + 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 43c7b50793..173cfa235b 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -7,7 +7,7 @@ use crate::{ FileTreeItemKind, }, keys::SharedKeyConfig, - queue::{Action, InternalEvent, Queue, ResetItem}, + queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, strings, try_or_popup, ui::style::SharedTheme, }; @@ -468,6 +468,14 @@ impl Status { } } + fn undo_last_commit(&self) { + try_or_popup!( + self, + "undo commit failed:", + sync::utils::undo_last_commit(CWD) + ); + } + fn branch_compare(&mut self) { self.git_branch_state = self.git_branch_name.last().and_then(|branch| { @@ -579,6 +587,12 @@ impl Component for Status { !focus_on_diff, )); + out.push(CommandInfo::new( + strings::commands::undo_commit(&self.key_config), + true, + !focus_on_diff, + )); + out.push(CommandInfo::new( strings::commands::abort_merge(&self.key_config), true, @@ -687,6 +701,14 @@ impl Component for Status { { self.pull(); Ok(EventState::Consumed) + } else if k == self.key_config.undo_commit + && !self.is_focus_on_diff() + { + self.undo_last_commit(); + self.queue.borrow_mut().push_back( + InternalEvent::Update(NeedsUpdate::ALL), + ); + Ok(EventState::Consumed) } else if k == self.key_config.abort_merge && Self::can_abort_merge() { diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index f23e1d958a..8fc1575904 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -34,6 +34,7 @@ // Also just plain text characters will not work because the commit // msg editor will interpret them as text input open_commit_editor: ( code: Char('e'), modifiers: ( bits: 2,),), + undo_commit: ( code: Char('U'), modifiers: ( bits: 1,),), move_left: ( code: Char('h'), modifiers: ( bits: 0,),), move_right: ( code: Char('l'), modifiers: ( bits: 0,),),