diff --git a/Cargo.lock b/Cargo.lock index bb25911e2a..1f25d4a1f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,6 +502,7 @@ dependencies = [ "serde", "simplelog", "syntect", + "tempfile", "textwrap 0.13.4", "tui", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 13a6ebde8f..44a89155d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ serde = "1.0" anyhow = "1.0" unicode-width = "0.1" textwrap = "0.13" +tempfile = "3.2" unicode-truncate = "0.2" unicode-segmentation = "1.7" easy-cast = "0.4" diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index a22c9dbda6..4f4a366af4 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -18,6 +18,12 @@ pub enum Error { #[error("git: work dir error")] NoWorkDir, + #[error("git: no parent of commit found")] + NoParent, + + #[error("git: not on a branch")] + NoBranch, + #[error("git: uncommitted changes")] UncommittedChanges, diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index edfd073067..b32df9d590 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -44,6 +44,32 @@ pub(crate) fn get_branch_name_repo( Err(Error::NoHead) } +/// Gets the current branch the user is on. +/// Returns none if they are not on a branch +/// and Err if there was a problem finding the branch +pub(crate) fn get_cur_branch( + repo: &Repository, +) -> Result> { + for b in repo.branches(None)? { + let branch = b?.0; + if branch.is_head() { + return Ok(Some(branch)); + } + } + Ok(None) +} + +/// Convenience function to get the current branch reference +pub fn get_head_refname(repo_path: &str) -> Result> { + let repo = utils::repo(repo_path)?; + if let Ok(Some(b)) = get_cur_branch(&repo) { + return Ok(Some(String::from_utf8( + b.get().name_bytes().to_vec(), + )?)); + } + Ok(None) +} + /// #[derive(Debug)] pub struct LocalBranch { diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 52d57144c0..f57b4c6653 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -17,6 +17,7 @@ mod ignore; mod logwalker; mod merge; mod patches; +mod rebase; pub mod remotes; mod reset; mod staging; @@ -54,6 +55,7 @@ pub use logwalker::LogWalker; pub use merge::{ abort_merge, merge_branch, merge_commit, merge_msg, mergehead_ids, }; +pub use rebase::reword; pub use remotes::{ get_default_remote, get_remotes, push::AsyncProgress, tags::PushTagsProgress, diff --git a/asyncgit/src/sync/rebase.rs b/asyncgit/src/sync/rebase.rs new file mode 100644 index 0000000000..2071ebd82a --- /dev/null +++ b/asyncgit/src/sync/rebase.rs @@ -0,0 +1,163 @@ +//! + +use super::branch::get_head_refname; +use super::commit::signature_allow_undefined_name; +use crate::{ + error::Error, + error::Result, + sync::{ + branch::get_cur_branch, + utils::{self, bytes2string}, + }, +}; +use git2::{Oid, RebaseOptions}; + +/// This is the same as reword, but will abort and fix the repo if something goes wrong +pub fn reword( + repo_path: &str, + commit_oid: Oid, + message: &str, +) -> Result<()> { + let repo = utils::repo(repo_path)?; + let cur_branch_ref = get_head_refname(repo_path)?; + + match reword_internal(repo_path, commit_oid, message) { + Ok(()) => Ok(()), + // Something went wrong, checkout the previous branch then error + Err(e) => { + if let Ok(mut rebase) = repo.open_rebase(None) { + match cur_branch_ref { + Some(cur_branch) => { + rebase.abort()?; + repo.set_head(&cur_branch)?; + repo.checkout_head(None)?; + } + None => return Err(Error::NoBranch), + } + } + Err(e) + } + } +} + +/// Changes the commit message of a commit with a specified oid +/// +/// While this function is most commonly associated with doing a +/// reword opperation in an interactive rebase, that is not how it +/// is implemented in git2rs +/// +/// This is dangerous if it errors, as the head will be detached so this should +/// always be wrapped by another function which aborts the rebase if something goes wrong +fn reword_internal( + repo_path: &str, + commit_oid: Oid, + message: &str, +) -> Result<()> { + let repo = utils::repo(repo_path)?; + let sig = signature_allow_undefined_name(&repo)?; + + let parent_commit_oid = if let Ok(parent_commit) = + repo.find_commit(commit_oid)?.parent(0) + { + Some(parent_commit.id()) + } else { + None + }; + + let commit_to_change = if let Some(pc_oid) = parent_commit_oid { + // Need to start at one previous to the commit, so + // first rebase.next() points to the actual commit we want to change + repo.find_annotated_commit(pc_oid)? + } else { + return Err(Error::NoParent); + }; + + // If we are on a branch + if let Ok(Some(branch)) = get_cur_branch(&repo) { + let cur_branch_ref = bytes2string(branch.get().name_bytes())?; + let cur_branch_name = bytes2string(branch.name_bytes()?)?; + let top_branch_commit = repo.find_annotated_commit( + branch.get().peel_to_commit()?.id(), + )?; + + let mut rebase = repo.rebase( + Some(&top_branch_commit), + Some(&commit_to_change), + None, + Some(&mut RebaseOptions::default()), + )?; + + let mut target; + + rebase.next(); + if parent_commit_oid.is_none() { + return Err(Error::NoParent); + } + target = rebase.commit(None, &sig, Some(message))?; + + // Set target to top commit, don't know when the rebase will end + // so have to loop till end + while rebase.next().is_some() { + target = rebase.commit(None, &sig, None)?; + } + rebase.finish(None)?; + + // Now override the previous branch + repo.branch( + &cur_branch_name, + &repo.find_commit(target)?, + true, + )?; + + // Reset the head back to the branch then checkout head + repo.set_head(&cur_branch_ref)?; + repo.checkout_head(None)?; + return Ok(()); + } + // Repo is not on a branch, possibly detached head + Err(Error::NoBranch) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::{ + commit, stage_add_file, tests::repo_init_empty, + }; + use std::{fs::File, io::Write, path::Path}; + + #[test] + fn test_reword() -> Result<()> { + let file_path = Path::new("foo"); + let (_td, repo) = repo_init_empty().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + File::create(&root.join(file_path))?.write_all(b"a")?; + stage_add_file(repo_path, file_path).unwrap(); + commit(repo_path, "commit1").unwrap(); + File::create(&root.join(file_path))?.write_all(b"ab")?; + stage_add_file(repo_path, file_path).unwrap(); + let oid2 = commit(repo_path, "commit2").unwrap(); + + let branch = + repo.branches(None).unwrap().next().unwrap().unwrap().0; + let branch_ref = branch.get(); + let commit_ref = branch_ref.peel_to_commit().unwrap(); + let message = commit_ref.message().unwrap(); + + assert_eq!(message, "commit2"); + + reword(repo_path, oid2.into(), "NewCommitMessage").unwrap(); + + // Need to get the branch again as top oid has changed + let branch = + repo.branches(None).unwrap().next().unwrap().unwrap().0; + let branch_ref = branch.get(); + let commit_ref_new = branch_ref.peel_to_commit().unwrap(); + let message_new = commit_ref_new.message().unwrap(); + assert_eq!(message_new, "NewCommitMessage"); + + Ok(()) + } +} diff --git a/src/app.rs b/src/app.rs index 81ab77aa96..6861433372 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,8 +8,8 @@ use crate::{ ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PullComponent, PushComponent, PushTagsComponent, RenameBranchComponent, - ResetComponent, RevisionFilesComponent, StashMsgComponent, - TagCommitComponent, + ResetComponent, RevisionFilesComponent, RewordComponent, + StashMsgComponent, TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -18,7 +18,7 @@ use crate::{ tabs::{Revlog, StashList, Stashing, Status}, ui::style::{SharedTheme, Theme}, }; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use asyncgit::{sync, AsyncNotification, CWD}; use crossbeam_channel::Sender; use crossterm::event::{Event, KeyEvent}; @@ -35,7 +35,14 @@ use tui::{ Frame, }; -/// the main app type +/// Used to determine where the user should +/// be put when the external editor is closed +pub enum EditorSource { + Commit, + Reword, +} + +/// pub struct App { do_quit: bool, help: HelpComponent, @@ -51,6 +58,7 @@ pub struct App { push_tags_popup: PushTagsComponent, pull_popup: PullComponent, tag_commit_popup: TagCommitComponent, + reword_popup: RewordComponent, create_branch_popup: CreateBranchComponent, rename_branch_popup: RenameBranchComponent, select_branch_popup: BranchListComponent, @@ -64,10 +72,10 @@ pub struct App { theme: SharedTheme, key_config: SharedKeyConfig, input: Input, + external_editor: Option<(Option, EditorSource)>, // "Flags" requires_redraw: Cell, - file_to_open: Option, } // public interface @@ -147,6 +155,11 @@ impl App { theme.clone(), key_config.clone(), ), + reword_popup: RewordComponent::new( + queue.clone(), + theme.clone(), + key_config.clone(), + ), create_branch_popup: CreateBranchComponent::new( queue.clone(), theme.clone(), @@ -200,7 +213,7 @@ impl App { theme, key_config, requires_redraw: Cell::new(false), - file_to_open: None, + external_editor: None, } } @@ -283,13 +296,24 @@ impl App { } else if let InputEvent::State(polling_state) = ev { self.external_editor_popup.hide(); if let InputState::Paused = polling_state { - let result = match self.file_to_open.take() { - Some(path) => { - ExternalEditorComponent::open_file_in_editor( - Path::new(&path), - ) + let result = if let Some(ee) = &self.external_editor { + match &ee.0 { + Some(path) => { + ExternalEditorComponent::open_file_in_editor( + Path::new(&path) + ) + }, + None => match ee.1 { + EditorSource::Commit => { + self.commit.show_editor() + } + EditorSource::Reword => { + self.reword_popup.show_editor() + } + }, } - None => self.commit.show_editor(), + } else { + Err(anyhow!("There was no editor path or return path selected, the app external editor was set to null, put in a bug report at https://github.com/extrawurst/gitui and detail what you tried to do, this is most likely an error")) }; if let Err(e) = result { @@ -393,6 +417,7 @@ impl App { push_tags_popup, pull_popup, tag_commit_popup, + reword_popup, create_branch_popup, rename_branch_popup, select_branch_popup, @@ -528,7 +553,11 @@ impl App { .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } InternalEvent::Update(u) => flags.insert(u), - InternalEvent::OpenCommit => self.commit.show()?, + InternalEvent::OpenCommit => { + self.external_editor = + Some((None, EditorSource::Commit)); + self.commit.show()?; + } InternalEvent::PopupStashing(opts) => { self.stashmsg_popup.options(opts); self.stashmsg_popup.show()? @@ -536,6 +565,11 @@ impl App { InternalEvent::TagCommit(id) => { self.tag_commit_popup.open(id)?; } + InternalEvent::RewordCommit(id) => { + self.external_editor = + Some((None, EditorSource::Reword)); + self.reword_popup.open(id)?; + } InternalEvent::BlameFile(path) => { self.blame_file_popup.open(&path)?; flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) @@ -555,10 +589,12 @@ impl App { self.inspect_commit_popup.open(id, tags)?; flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) } - InternalEvent::OpenExternalEditor(path) => { + InternalEvent::OpenExternalEditor( + _path, + _editor_source, + ) => { self.input.set_polling(false); self.external_editor_popup.show()?; - self.file_to_open = path; flags.insert(NeedsUpdate::COMMANDS) } InternalEvent::Push(branch, force) => { @@ -697,6 +733,7 @@ impl App { || self.pull_popup.is_visible() || self.select_branch_popup.is_visible() || self.rename_branch_popup.is_visible() + || self.reword_popup.is_visible() || self.revision_files_popup.is_visible() } @@ -723,6 +760,7 @@ impl App { self.external_editor_popup.draw(f, size)?; self.tag_commit_popup.draw(f, size)?; self.select_branch_popup.draw(f, size)?; + self.reword_popup.draw(f, size)?; self.create_branch_popup.draw(f, size)?; self.rename_branch_popup.draw(f, size)?; self.revision_files_popup.draw(f, size)?; diff --git a/src/components/commit.rs b/src/components/commit.rs index a6d1648e4f..6d3929d9ae 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -1,9 +1,10 @@ use super::{ - textinput::TextInputComponent, visibility_blocking, - CommandBlocking, CommandInfo, Component, DrawableComponent, - EventState, ExternalEditorComponent, + externaleditor::show_editor, textinput::TextInputComponent, + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, }; use crate::{ + app::EditorSource, keys::SharedKeyConfig, queue::{InternalEvent, NeedsUpdate, Queue}, strings, @@ -20,10 +21,7 @@ use asyncgit::{ }; use crossterm::event::Event; use easy_cast::Cast; -use std::{ - fs::{read_to_string, File}, - io::{Read, Write}, -}; +use std::fs::read_to_string; use tui::{ backend::Backend, layout::{Alignment, Rect}, @@ -113,7 +111,10 @@ impl Component for CommitComponent { self.amend()?; } else if e == self.key_config.open_commit_editor { self.queue.borrow_mut().push_back( - InternalEvent::OpenExternalEditor(None), + InternalEvent::OpenExternalEditor( + None, + EditorSource::Commit, + ), ); self.hide(); } else { @@ -249,42 +250,11 @@ impl CommitComponent { } } + /// Open external editor pub fn show_editor(&mut self) -> Result<()> { - let file_path = sync::repo_dir(CWD)?.join("COMMIT_EDITMSG"); - - { - let mut file = File::create(&file_path)?; - file.write_fmt(format_args!( - "{}\n", - self.input.get_text() - ))?; - file.write_all( - strings::commit_editor_msg(&self.key_config) - .as_bytes(), - )?; - } - - ExternalEditorComponent::open_file_in_editor(&file_path)?; - - let mut message = String::new(); - - let mut file = File::open(&file_path)?; - file.read_to_string(&mut message)?; - drop(file); - std::fs::remove_file(&file_path)?; - - let message: String = message - .lines() - .flat_map(|l| { - if l.starts_with('#') { - vec![] - } else { - vec![l, "\n"] - } - }) - .collect(); - - let message = message.trim().to_string(); + let message = show_editor(Some(self.input.get_text()))? + .trim() + .to_string(); self.input.set_text(message); self.input.show()?; diff --git a/src/components/externaleditor.rs b/src/components/externaleditor.rs index 58a112b2e7..5ab3feea7d 100644 --- a/src/components/externaleditor.rs +++ b/src/components/externaleditor.rs @@ -18,7 +18,13 @@ use crossterm::{ }; use scopeguard::defer; use std::ffi::OsStr; -use std::{env, io, path::Path, process::Command}; +use std::{ + env, + io::{self, Read, Write}, + path::Path, + process::Command, +}; +use tempfile::NamedTempFile; use tui::{ backend::Backend, layout::Rect, @@ -187,3 +193,34 @@ impl Component for ExternalEditorComponent { Ok(()) } } + +pub fn show_editor(with_text: Option<&String>) -> Result { + let temp_file = NamedTempFile::new()?; + { + let mut file = temp_file.reopen()?; + if let Some(text) = with_text { + file.write_fmt(format_args!("{}\n", text))?; + } + file.write_all(strings::commit_editor_msg().as_bytes())?; + } + + ExternalEditorComponent::open_file_in_editor(temp_file.path())?; + + let mut message = String::new(); + + let mut file = temp_file.reopen()?; + file.read_to_string(&mut message)?; + + let message: String = message + .lines() + .flat_map(|l| { + if l.starts_with('#') { + vec![] + } else { + vec![l, "\n"] + } + }) + .collect(); + + Ok(message.trim().to_string()) +} diff --git a/src/components/mod.rs b/src/components/mod.rs index a48994e8d7..b713783dc6 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -18,6 +18,7 @@ mod push; mod push_tags; mod rename_branch; mod reset; +mod reword; mod revision_files; mod stashmsg; mod syntax_text; @@ -44,6 +45,7 @@ pub use push::PushComponent; pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ResetComponent; +pub use reword::RewordComponent; pub use revision_files::RevisionFilesComponent; pub use stashmsg::StashMsgComponent; pub use syntax_text::SyntaxTextComponent; diff --git a/src/components/reword.rs b/src/components/reword.rs new file mode 100644 index 0000000000..e826cf85b9 --- /dev/null +++ b/src/components/reword.rs @@ -0,0 +1,185 @@ +use super::{ + externaleditor::show_editor, textinput::TextInputComponent, + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, +}; +use crate::{ + app::EditorSource, + keys::SharedKeyConfig, + queue::{InternalEvent, NeedsUpdate, Queue}, + strings, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, CommitId}, + CWD, +}; +use crossterm::event::Event; +use tui::{backend::Backend, layout::Rect, Frame}; + +pub struct RewordComponent { + input: TextInputComponent, + commit_id: Option, + queue: Queue, + key_config: SharedKeyConfig, +} + +impl DrawableComponent for RewordComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + self.input.draw(f, rect)?; + + Ok(()) + } +} + +impl Component for RewordComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + self.input.commands(out, force_all); + + out.push(CommandInfo::new( + strings::commands::reword_commit_confirm_msg( + &self.key_config, + ), + true, + true, + )); + + out.push(CommandInfo::new( + strings::commands::commit_open_editor( + &self.key_config, + ), + true, + true, + )); + } + + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.is_visible() { + if let Ok(EventState::Consumed) = self.input.event(ev) { + return Ok(EventState::Consumed); + } + + if let Event::Key(e) = ev { + if e == self.key_config.enter { + self.reword() + } else if e == self.key_config.open_commit_editor { + self.queue.borrow_mut().push_back( + InternalEvent::OpenExternalEditor( + None, + EditorSource::Reword, + ), + ); + self.hide(); + } + + return Ok(EventState::Consumed); + } + } + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.input.is_visible() + } + + fn hide(&mut self) { + self.input.hide() + } + + fn show(&mut self) -> Result<()> { + self.input.show()?; + + Ok(()) + } +} + +impl RewordComponent { + /// + pub fn new( + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + queue, + input: TextInputComponent::new( + theme, + key_config.clone(), + &strings::reword_popup_title(&key_config), + &strings::reword_popup_msg(&key_config), + true, + ), + commit_id: None, + key_config, + } + } + + /// + pub fn open(&mut self, id: CommitId) -> Result<()> { + self.commit_id = Some(id); + if let Some(commit_msg) = + sync::get_commit_details(CWD, id)?.message + { + self.input.set_text(commit_msg.combine()); + } + self.show()?; + + Ok(()) + } + + /// Open external editor + pub fn show_editor(&mut self) -> Result<()> { + let message = show_editor(Some(self.input.get_text()))? + .trim() + .to_string(); + + self.input.set_text(message); + self.input.show()?; + + Ok(()) + } + + /// + pub fn reword(&mut self) { + if let Some(commit_id) = self.commit_id { + match sync::reword( + CWD, + commit_id.into(), + self.input.get_text(), + ) { + Ok(_) => { + self.input.clear(); + self.hide(); + + self.queue.borrow_mut().push_back( + InternalEvent::Update(NeedsUpdate::ALL), + ); + } + Err(e) => { + self.input.clear(); + self.hide(); + log::error!("e: {}", e,); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "reword error:\n{}", + e, + )), + ); + } + } + } + } +} diff --git a/src/keys.rs b/src/keys.rs index aa22380951..de5b6529ad 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -50,6 +50,7 @@ pub struct KeyConfig { pub shift_up: KeyEvent, pub shift_down: KeyEvent, pub enter: KeyEvent, + pub reword: KeyEvent, pub blame: KeyEvent, pub edit_file: KeyEvent, pub status_stage_all: KeyEvent, @@ -113,6 +114,7 @@ impl Default for KeyConfig { shift_down: KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::SHIFT}, enter: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()}, blame: KeyEvent { code: KeyCode::Char('B'), modifiers: KeyModifiers::SHIFT}, + reword: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty() }, edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()}, status_stage_all: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::empty()}, status_reset_item: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, diff --git a/src/queue.rs b/src/queue.rs index c761598ae7..91d8cc0016 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,3 +1,4 @@ +use crate::app::EditorSource; use crate::tabs::StashingOptions; use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags}; use bitflags::bitflags; @@ -61,6 +62,8 @@ pub enum InternalEvent { /// TagCommit(CommitId), /// + RewordCommit(CommitId), + /// BlameFile(String), /// CreateBranch, @@ -69,7 +72,7 @@ pub enum InternalEvent { /// SelectBranch, /// - OpenExternalEditor(Option), + OpenExternalEditor(Option, EditorSource), /// Push(String, bool), /// diff --git a/src/strings.rs b/src/strings.rs index 7359bb6383..162efd4dee 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -77,7 +77,7 @@ pub fn commit_msg(_key_config: &SharedKeyConfig) -> String { pub fn commit_first_line_warning(count: usize) -> String { format!("[subject length: {}]", count) } -pub fn commit_editor_msg(_key_config: &SharedKeyConfig) -> String { +pub fn commit_editor_msg() -> String { r##" # Edit your commit message # Lines starting with '#' will be ignored"## @@ -193,6 +193,12 @@ pub fn tag_commit_popup_title( pub fn tag_commit_popup_msg(_key_config: &SharedKeyConfig) -> String { "type tag".to_string() } +pub fn reword_popup_title(_key_config: &SharedKeyConfig) -> String { + "reword".to_string() +} +pub fn reword_popup_msg(_key_config: &SharedKeyConfig) -> String { + "new message".to_string() +} pub fn stashlist_title(_key_config: &SharedKeyConfig) -> String { "Stashes".to_string() } @@ -888,6 +894,30 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn reword_commit_confirm_msg( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Reword [{}]", + key_config.get_hint(key_config.enter), + ), + "reword commit", + CMD_GROUP_LOG, + ) + } + pub fn reword_commit( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Reword [{}]", + key_config.get_hint(key_config.reword), + ), + "reword commit", + CMD_GROUP_LOG, + ) + } pub fn create_branch_confirm_msg( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 616bbcdf9e..b76c6e2b52 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -227,6 +227,16 @@ impl Component for Revlog { Ok(EventState::Consumed) }, ); + } else if k == self.key_config.reword { + return self.selected_commit().map_or( + Ok(EventState::NotConsumed), + |id| { + self.queue.borrow_mut().push_back( + InternalEvent::RewordCommit(id), + ); + Ok(EventState::Consumed) + }, + ); } else if k == self.key_config.focus_right && self.commit_details.is_visible() { @@ -295,7 +305,13 @@ impl Component for Revlog { )); out.push(CommandInfo::new( - strings::commands::open_branch_select_popup( + strings::commands::reword_commit(&self.key_config), + true, + self.visible || force_all, + )); + + out.push(CommandInfo::new( + strings::commands::open_branch_create_popup( &self.key_config, ), true, diff --git a/src/tabs/status.rs b/src/tabs/status.rs index baa31bdf1d..f01ec6fa98 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -1,5 +1,6 @@ use crate::{ accessors, + app::EditorSource, components::{ command_pump, event_pump, visibility_blocking, ChangesComponent, CommandBlocking, CommandInfo, Component, @@ -635,9 +636,10 @@ impl Component for Status { { if let Some((path, _)) = self.selected_path() { self.queue.borrow_mut().push_back( - InternalEvent::OpenExternalEditor(Some( - path, - )), + InternalEvent::OpenExternalEditor( + Some(path), + EditorSource::Commit, + ), ); } Ok(EventState::Consumed) diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index 544ea88927..dc0e4c5687 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -80,6 +80,7 @@ abort_merge: ( code: Char('M'), modifiers: ( bits: 1,),), push: ( code: Char('p'), modifiers: ( bits: 0,),), + reword: ( code: Char('r'), modifiers: ( bits: 0,),), force_push: ( code: Char('P'), modifiers: ( bits: 1,),), pull: ( code: Char('f'), modifiers: ( bits: 0,),),