diff --git a/CHANGELOG.md b/CHANGELOG.md index bba7bf15..49eceb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +**reset to commit** + +![reset](assets/reset_in_log.gif) + ### Added * changes in commit message inside external editor [[@bc-universe]](https://github.com/bc-universe) ([#1420](https://github.com/extrawurst/gitui/issues/1420)) * allow detaching HEAD and checking out specific commit from log view [[@fralcow]](https://github.com/fralcow) ([#1499](https://github.com/extrawurst/gitui/pull/1499)) * add no-verify option on commits to not run hooks [[@dam5h]](https://github.com/dam5h) ([#1374](https://github.com/extrawurst/gitui/issues/1374)) * allow `fetch` on status tab [[@alensiljak]](https://github.com/alensiljak) ([#1471](https://github.com/extrawurst/gitui/issues/1471)) +* allow reset (soft,mixed,hard) from commit log ([#1500](https://github.com/extrawurst/gitui/issues/1500)) ### Fixes * commit msg history ordered the wrong way ([#1445](https://github.com/extrawurst/gitui/issues/1445)) diff --git a/assets/reset_in_log.gif b/assets/reset_in_log.gif new file mode 100644 index 00000000..05f66f0f Binary files /dev/null and b/assets/reset_in_log.gif differ diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 9194bf03..07a955a8 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -75,7 +75,7 @@ pub use remotes::{ }; pub(crate) use repository::repo; pub use repository::{RepoPath, RepoPathRef}; -pub use reset::{reset_stage, reset_workdir}; +pub use reset::{reset_repo, reset_stage, reset_workdir}; pub use staging::{discard_lines, stage_lines}; pub use stash::{ get_stashes, stash_apply, stash_drop, stash_pop, stash_save, @@ -96,6 +96,8 @@ pub use utils::{ stage_add_file, stage_addremoved, Head, }; +pub use git2::ResetType; + #[cfg(test)] mod tests { use super::{ diff --git a/asyncgit/src/sync/reset.rs b/asyncgit/src/sync/reset.rs index e4ba87eb..41422559 100644 --- a/asyncgit/src/sync/reset.rs +++ b/asyncgit/src/sync/reset.rs @@ -1,6 +1,6 @@ -use super::{utils::get_head_repo, RepoPath}; +use super::{utils::get_head_repo, CommitId, RepoPath}; use crate::{error::Result, sync::repository::repo}; -use git2::{build::CheckoutBuilder, ObjectType}; +use git2::{build::CheckoutBuilder, ObjectType, ResetType}; use scopetime::scope_time; /// @@ -38,6 +38,23 @@ pub fn reset_workdir(repo_path: &RepoPath, path: &str) -> Result<()> { Ok(()) } +/// +pub fn reset_repo( + repo_path: &RepoPath, + commit: CommitId, + kind: ResetType, +) -> Result<()> { + scope_time!("reset_repo"); + + let repo = repo(repo_path)?; + + let c = repo.find_commit(commit.into())?; + + repo.reset(c.as_object(), kind, None)?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::{reset_stage, reset_workdir}; diff --git a/src/app.rs b/src/app.rs index be0cc917..98ebd348 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use crate::{ FileRevlogComponent, HelpComponent, InspectCommitComponent, MsgComponent, OptionsPopupComponent, PullComponent, PushComponent, PushTagsComponent, RenameBranchComponent, - RevisionFilesPopup, StashMsgComponent, + ResetPopupComponent, RevisionFilesPopup, StashMsgComponent, SubmodulesListComponent, TagCommitComponent, TagListComponent, }, @@ -84,6 +84,7 @@ pub struct App { options_popup: OptionsPopupComponent, submodule_popup: SubmodulesListComponent, tags_popup: TagListComponent, + reset_popup: ResetPopupComponent, cmdbar: RefCell, tab: usize, revlog: Revlog, @@ -204,6 +205,12 @@ impl App { theme.clone(), key_config.clone(), ), + reset_popup: ResetPopupComponent::new( + &queue, + &repo, + theme.clone(), + key_config.clone(), + ), pull_popup: PullComponent::new( &repo, &queue, @@ -484,6 +491,7 @@ impl App { self.files_tab.update()?; self.stashing_tab.update()?; self.stashlist_tab.update()?; + self.reset_popup.update()?; self.update_commands(); @@ -590,6 +598,7 @@ impl App { revision_files_popup, submodule_popup, tags_popup, + reset_popup, options_popup, help, revlog, @@ -615,6 +624,7 @@ impl App { select_branch_popup, submodule_popup, tags_popup, + reset_popup, create_branch_popup, rename_branch_popup, revision_files_popup, @@ -926,6 +936,9 @@ impl App { self.do_quit = QuitState::OpenSubmodule(submodule_repo_path); } + InternalEvent::OpenResetPopup(id) => { + self.reset_popup.open(id)?; + } }; Ok(flags) diff --git a/src/components/mod.rs b/src/components/mod.rs index 8d1b5114..77824de8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -22,6 +22,7 @@ mod push; mod push_tags; mod rename_branch; mod reset; +mod reset_popup; mod revision_files; mod revision_files_popup; mod stashmsg; @@ -57,6 +58,7 @@ pub use push::PushComponent; pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ConfirmComponent; +pub use reset_popup::ResetPopupComponent; pub use revision_files::RevisionFilesComponent; pub use revision_files_popup::{FileTreeOpen, RevisionFilesPopup}; pub use stashmsg::StashMsgComponent; diff --git a/src/components/reset_popup.rs b/src/components/reset_popup.rs new file mode 100644 index 00000000..cbe3316c --- /dev/null +++ b/src/components/reset_popup.rs @@ -0,0 +1,272 @@ +use super::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, +}; +use crate::{ + keys::{key_match, SharedKeyConfig}, + queue::Queue, + strings, try_or_popup, + ui::{self, style::SharedTheme}, +}; +use anyhow::Result; +use asyncgit::{ + cached, + sync::{CommitId, RepoPath, RepoPathRef, ResetType}, +}; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::{Alignment, Rect}, + text::{Span, Spans}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +const fn type_to_string( + kind: ResetType, +) -> (&'static str, &'static str) { + const RESET_TYPE_DESC_SOFT: &str = + " 🟢 Keep all changes. Stage differences"; + const RESET_TYPE_DESC_MIXED: &str = + " 🟡 Keep all changes. Unstage differences"; + const RESET_TYPE_DESC_HARD: &str = + " 🔴 Discard all local changes"; + + match kind { + ResetType::Soft => ("Soft", RESET_TYPE_DESC_SOFT), + ResetType::Mixed => ("Mixed", RESET_TYPE_DESC_MIXED), + ResetType::Hard => ("Hard", RESET_TYPE_DESC_HARD), + } +} + +pub struct ResetPopupComponent { + queue: Queue, + repo: RepoPath, + commit: Option, + kind: ResetType, + git_branch_name: cached::BranchName, + visible: bool, + key_config: SharedKeyConfig, + theme: SharedTheme, +} + +impl ResetPopupComponent { + /// + pub fn new( + queue: &Queue, + repo: &RepoPathRef, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + queue: queue.clone(), + repo: repo.borrow().clone(), + commit: None, + kind: ResetType::Soft, + git_branch_name: cached::BranchName::new(repo.clone()), + visible: false, + key_config, + theme, + } + } + + fn get_text(&self, _width: u16) -> Vec { + let mut txt: Vec = Vec::with_capacity(10); + + txt.push(Spans::from(vec![ + Span::styled( + String::from("Branch: "), + self.theme.text(true, false), + ), + Span::styled( + self.git_branch_name.last().unwrap_or_default(), + self.theme.branch(false, true), + ), + ])); + + txt.push(Spans::from(vec![ + Span::styled( + String::from("Reset to: "), + self.theme.text(true, false), + ), + Span::styled( + self.commit + .map(|c| c.to_string()) + .unwrap_or_default(), + self.theme.commit_hash(false), + ), + ])); + + let (kind_name, kind_desc) = type_to_string(self.kind); + + txt.push(Spans::from(vec![ + Span::styled( + String::from("How: "), + self.theme.text(true, false), + ), + Span::styled(kind_name, self.theme.text(true, true)), + Span::styled(kind_desc, self.theme.text(true, false)), + ])); + + txt + } + + /// + pub fn open(&mut self, id: CommitId) -> Result<()> { + self.show()?; + + self.commit = Some(id); + + Ok(()) + } + + /// + #[allow(clippy::unnecessary_wraps)] + pub fn update(&mut self) -> Result<()> { + self.git_branch_name.lookup().map(Some).unwrap_or(None); + + Ok(()) + } + + fn reset(&mut self) { + if let Some(id) = self.commit { + try_or_popup!( + self, + "reset:", + asyncgit::sync::reset_repo(&self.repo, id, self.kind) + ); + } + + self.hide(); + } + + fn change_kind(&mut self, incr: bool) { + self.kind = if incr { + match self.kind { + ResetType::Soft => ResetType::Mixed, + ResetType::Mixed => ResetType::Hard, + ResetType::Hard => ResetType::Soft, + } + } else { + match self.kind { + ResetType::Soft => ResetType::Hard, + ResetType::Mixed => ResetType::Soft, + ResetType::Hard => ResetType::Mixed, + } + }; + } +} + +impl DrawableComponent for ResetPopupComponent { + fn draw( + &self, + f: &mut Frame, + area: Rect, + ) -> Result<()> { + if self.is_visible() { + const SIZE: (u16, u16) = (55, 5); + let area = + ui::centered_rect_absolute(SIZE.0, SIZE.1, area); + + let width = area.width; + + f.render_widget(Clear, area); + f.render_widget( + Paragraph::new(self.get_text(width)) + .block( + Block::default() + .borders(Borders::ALL) + .title(Span::styled( + "Reset", + self.theme.title(true), + )) + .border_style(self.theme.block(true)), + ) + .alignment(Alignment::Left), + area, + ); + } + + Ok(()) + } +} + +impl Component for ResetPopupComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + + out.push( + CommandInfo::new( + strings::commands::reset_commit(&self.key_config), + true, + true, + ) + .order(1), + ); + + out.push( + CommandInfo::new( + strings::commands::reset_type(&self.key_config), + true, + true, + ) + .order(1), + ); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: &crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = &event { + if key_match(key, self.key_config.keys.exit_popup) { + self.hide(); + } else if key_match( + key, + self.key_config.keys.move_down, + ) { + self.change_kind(true); + } else if key_match(key, self.key_config.keys.move_up) + { + self.change_kind(false); + } else if key_match(key, self.key_config.keys.enter) { + self.reset(); + } + } + + return Ok(EventState::Consumed); + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 06f07bae..f70397b5 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -16,10 +16,12 @@ macro_rules! try_or_popup { ($self:ident, $msg:expr, $e:expr) => { if let Err(err) = $e { ::log::error!("{} {}", $msg, err); - $self.queue.push(InternalEvent::ShowErrorMsg(format!( - "{}\n{}", - $msg, err - ))); + $self.queue.push( + $crate::queue::InternalEvent::ShowErrorMsg(format!( + "{}\n{}", + $msg, err + )), + ); } }; } diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 96fbaeeb..05cbba99 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -88,6 +88,7 @@ pub struct KeysList { pub log_tag_commit: GituiKeyEvent, pub log_mark_commit: GituiKeyEvent, pub log_checkout_commit: GituiKeyEvent, + pub log_reset_comit: GituiKeyEvent, pub commit_amend: GituiKeyEvent, pub toggle_verify: GituiKeyEvent, pub copy: GituiKeyEvent, @@ -173,6 +174,7 @@ impl Default for KeysList { log_tag_commit: GituiKeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()), log_mark_commit: GituiKeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()), log_checkout_commit: GituiKeyEvent { code: KeyCode::Char('S'), modifiers: KeyModifiers::SHIFT }, + log_reset_comit: GituiKeyEvent { code: KeyCode::Char('R'), modifiers: KeyModifiers::SHIFT }, commit_amend: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), toggle_verify: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), copy: GituiKeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()), diff --git a/src/keys/key_list_file.rs b/src/keys/key_list_file.rs index ae8b4f2f..7832a3d9 100644 --- a/src/keys/key_list_file.rs +++ b/src/keys/key_list_file.rs @@ -59,6 +59,7 @@ pub struct KeysListFile { pub log_tag_commit: Option, pub log_mark_commit: Option, pub log_checkout_commit: Option, + pub log_reset_commit: Option, pub commit_amend: Option, pub toggle_verify: Option, pub copy: Option, @@ -153,6 +154,7 @@ impl KeysListFile { log_tag_commit: self.log_tag_commit.unwrap_or(default.log_tag_commit), log_mark_commit: self.log_mark_commit.unwrap_or(default.log_mark_commit), log_checkout_commit: self.log_checkout_commit.unwrap_or(default.log_checkout_commit), + log_reset_comit: self.log_reset_commit.unwrap_or(default.log_reset_comit), commit_amend: self.commit_amend.unwrap_or(default.commit_amend), toggle_verify: self.toggle_verify.unwrap_or(default.toggle_verify), copy: self.copy.unwrap_or(default.copy), diff --git a/src/queue.rs b/src/queue.rs index dd033bed..6f958410 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -126,6 +126,8 @@ pub enum InternalEvent { ViewSubmodules, /// OpenRepo { path: PathBuf }, + /// + OpenResetPopup(CommitId), } /// single threaded simple queue for components to communicate with each other diff --git a/src/strings.rs b/src/strings.rs index 37609499..a27a1407 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1233,6 +1233,39 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn log_reset_commit( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Reset [{}]", + key_config.get_hint(key_config.keys.log_reset_comit), + ), + "reset to commit", + CMD_GROUP_LOG, + ) + } + pub fn reset_commit(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Confirm [{}]", + key_config.get_hint(key_config.keys.enter), + ), + "confirm reset", + CMD_GROUP_LOG, + ) + } + pub fn reset_type(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Change Type [{}{}]", + key_config.get_hint(key_config.keys.move_up), + key_config.get_hint(key_config.keys.move_down) + ), + "change reset type", + CMD_GROUP_LOG, + ) + } pub fn tag_commit_confirm_msg( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 0b0ad7d4..a7886207 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -327,6 +327,19 @@ impl Component for Revlog { } else if key_match(k, self.key_config.keys.tags) { self.queue.push(InternalEvent::Tags); return Ok(EventState::Consumed); + } else if key_match( + k, + self.key_config.keys.log_reset_comit, + ) { + return self.selected_commit().map_or( + Ok(EventState::NotConsumed), + |id| { + self.queue.push( + InternalEvent::OpenResetPopup(id), + ); + Ok(EventState::Consumed) + }, + ); } else if key_match( k, self.key_config.keys.compare_commits, @@ -449,6 +462,12 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::log_reset_commit(&self.key_config), + self.selected_commit().is_some(), + self.visible || force_all, + )); + visibility_blocking(self) }