diff --git a/asyncgit/src/sync/branch/merge_commit.rs b/asyncgit/src/sync/branch/merge_commit.rs index ec1ea498bb..dfdb2b04dc 100644 --- a/asyncgit/src/sync/branch/merge_commit.rs +++ b/asyncgit/src/sync/branch/merge_commit.rs @@ -3,7 +3,10 @@ use super::BranchType; use crate::{ error::{Error, Result}, - sync::{merge_msg, repository::repo, CommitId, RepoPath}, + sync::{ + commit::signature_allow_undefined_name, merge_msg, + repository::repo, sign::SignBuilder, CommitId, RepoPath, + }, }; use git2::Commit; use scopetime::scope_time; @@ -66,8 +69,20 @@ pub(crate) fn commit_merge_with_head( commits: &[Commit], msg: &str, ) -> Result { - let signature = - crate::sync::commit::signature_allow_undefined_name(repo)?; + commit_merge_with_head_with_sign(repo, commits, msg, None) +} + +pub(crate) fn commit_merge_with_head_with_sign( + repo: &git2::Repository, + commits: &[Commit], + msg: &str, + sign_override: Option, +) -> Result { + let config = repo.config()?; + let should_sign = sign_override.unwrap_or_else(|| { + config.get_bool("commit.gpgsign").unwrap_or(false) + }); + let signature = signature_allow_undefined_name(repo)?; let mut index = repo.index()?; let tree_id = index.write_tree()?; let tree = repo.find_tree(tree_id)?; @@ -78,8 +93,46 @@ pub(crate) fn commit_merge_with_head( let mut parents = vec![&head_commit]; parents.extend(commits); - let commit_id = repo - .commit( + let commit_id = if should_sign { + let buffer = repo.commit_create_buffer( + &signature, + &signature, + msg, + &tree, + parents.as_slice(), + )?; + + let commit = std::str::from_utf8(&buffer).map_err(|_e| { + crate::sync::sign::SignError::Shellout( + "utf8 conversion error".to_string(), + ) + })?; + + let signer = SignBuilder::from_gitconfig(repo, &config)?; + let (signature, signature_field) = signer.sign(&buffer)?; + let commit_id = repo.commit_signed( + commit, + &signature, + signature_field.as_deref(), + )?; + + if let Ok(mut head) = repo.head() { + head.set_target(commit_id, msg)?; + } else { + let default_branch_name = config + .get_str("init.defaultBranch") + .unwrap_or("master"); + repo.reference( + &format!("refs/heads/{default_branch_name}"), + commit_id, + true, + msg, + )?; + } + + CommitId::new(commit_id) + } else { + repo.commit( Some("HEAD"), &signature, &signature, @@ -87,7 +140,9 @@ pub(crate) fn commit_merge_with_head( &tree, parents.as_slice(), )? - .into(); + .into() + }; + repo.cleanup_state()?; Ok(commit_id) } diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index b88f055c10..864c2557f7 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -10,16 +10,36 @@ use git2::{ }; use scopetime::scope_time; +fn gpgsign_enabled( + config: &git2::Config, + sign_override: Option, +) -> bool { + sign_override.unwrap_or_else(|| { + config.get_bool("commit.gpgsign").unwrap_or(false) + }) +} + /// pub fn amend( repo_path: &RepoPath, id: CommitId, msg: &str, +) -> Result { + amend_with_sign(repo_path, id, msg, None) +} + +/// +pub fn amend_with_sign( + repo_path: &RepoPath, + id: CommitId, + msg: &str, + sign_override: Option, ) -> Result { scope_time!("amend"); let repo = repo(repo_path)?; let config = repo.config()?; + let should_sign = gpgsign_enabled(&config, sign_override); let commit = repo.find_commit(id.into())?; @@ -27,14 +47,18 @@ pub fn amend( let tree_id = index.write_tree()?; let tree = repo.find_tree(tree_id)?; - if config.get_bool("commit.gpgsign").unwrap_or(false) { + if should_sign { // HACK: we undo the last commit and create a new one use crate::sync::utils::undo_last_commit; let head = get_head_repo(&repo)?; if head == commit.id().into() { undo_last_commit(repo_path)?; - return self::commit(repo_path, msg); + return self::commit_with_sign( + repo_path, + msg, + Some(true), + ); } return Err(Error::SignAmendNonLastCommit); @@ -82,10 +106,20 @@ pub(crate) fn signature_allow_undefined_name( /// this does not run any git hooks, git-hooks have to be executed manually, checkout `hooks_commit_msg` for example pub fn commit(repo_path: &RepoPath, msg: &str) -> Result { + commit_with_sign(repo_path, msg, None) +} + +/// this does not run any git hooks, git-hooks have to be executed manually, checkout `hooks_commit_msg` for example +pub fn commit_with_sign( + repo_path: &RepoPath, + msg: &str, + sign_override: Option, +) -> Result { scope_time!("commit"); let repo = repo(repo_path)?; let config = repo.config()?; + let should_sign = gpgsign_enabled(&config, sign_override); let signature = signature_allow_undefined_name(&repo)?; let mut index = repo.index()?; let tree_id = index.write_tree()?; @@ -99,10 +133,7 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result { let parents = parents.iter().collect::>(); - let commit_id = if config - .get_bool("commit.gpgsign") - .unwrap_or(false) - { + let commit_id = if should_sign { let buffer = repo.commit_create_buffer( &signature, &signature, diff --git a/asyncgit/src/sync/commit_revert.rs b/asyncgit/src/sync/commit_revert.rs index 2d66a2a1d4..b0666c6dd9 100644 --- a/asyncgit/src/sync/commit_revert.rs +++ b/asyncgit/src/sync/commit_revert.rs @@ -40,10 +40,20 @@ pub fn revert_head(repo_path: &RepoPath) -> Result { pub fn commit_revert( repo_path: &RepoPath, msg: &str, +) -> Result { + commit_revert_with_sign(repo_path, msg, None) +} + +/// +pub fn commit_revert_with_sign( + repo_path: &RepoPath, + msg: &str, + sign_override: Option, ) -> Result { scope_time!("commit_revert"); - let id = crate::sync::commit(repo_path, msg)?; + let id = + crate::sync::commit_with_sign(repo_path, msg, sign_override)?; repo(repo_path)?.cleanup_state()?; diff --git a/asyncgit/src/sync/merge.rs b/asyncgit/src/sync/merge.rs index e495100f51..f9e2643088 100644 --- a/asyncgit/src/sync/merge.rs +++ b/asyncgit/src/sync/merge.rs @@ -1,7 +1,9 @@ use crate::{ error::{Error, Result}, sync::{ - branch::merge_commit::commit_merge_with_head, + branch::merge_commit::{ + commit_merge_with_head, commit_merge_with_head_with_sign, + }, rebase::{ abort_rebase, continue_rebase, get_rebase_progress, }, @@ -135,6 +137,16 @@ pub fn merge_commit( repo_path: &RepoPath, msg: &str, ids: &[CommitId], +) -> Result { + merge_commit_with_sign(repo_path, msg, ids, None) +} + +/// +pub fn merge_commit_with_sign( + repo_path: &RepoPath, + msg: &str, + ids: &[CommitId], + sign_override: Option, ) -> Result { scope_time!("merge_commit"); @@ -146,7 +158,16 @@ pub fn merge_commit( commits.push(repo.find_commit((*id).into())?); } - let id = commit_merge_with_head(&repo, &commits, msg)?; + let id = if sign_override.is_some() { + commit_merge_with_head_with_sign( + &repo, + &commits, + msg, + sign_override, + )? + } else { + commit_merge_with_head(&repo, &commits, msg)? + }; Ok(id) } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 2a5f413e8f..44a1800a14 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -45,7 +45,9 @@ pub use branch::{ merge_rebase::merge_upstream_rebase, rename::rename_branch, validate_branch_name, BranchCompare, BranchDetails, BranchInfo, }; -pub use commit::{amend, commit, tag_commit}; +pub use commit::{ + amend, amend_with_sign, commit, commit_with_sign, tag_commit, +}; pub use commit_details::{ get_commit_details, CommitDetails, CommitMessage, CommitSignature, }; @@ -55,7 +57,10 @@ pub use commit_filter::{ LogFilterSearchOptions, SearchFields, SearchOptions, SharedCommitFilterFn, }; -pub use commit_revert::{commit_revert, revert_commit, revert_head}; +pub use commit_revert::{ + commit_revert, commit_revert_with_sign, revert_commit, + revert_head, +}; pub use commits_info::{ get_commit_info, get_commits_info, CommitId, CommitInfo, }; @@ -75,8 +80,9 @@ pub use ignore::add_to_ignore; pub use logwalker::{LogWalker, LogWalkerWithoutFilter}; pub use merge::{ abort_pending_rebase, abort_pending_state, - continue_pending_rebase, merge_branch, merge_commit, merge_msg, - mergehead_ids, rebase_progress, + continue_pending_rebase, merge_branch, merge_commit, + merge_commit_with_sign, merge_msg, mergehead_ids, + rebase_progress, }; pub use rebase::rebase_branch; pub use remotes::{ @@ -88,7 +94,7 @@ pub use remotes::{ pub(crate) use repository::{gix_repo, repo}; pub use repository::{RepoPath, RepoPathRef}; pub use reset::{reset_repo, reset_stage, reset_workdir}; -pub use reword::reword; +pub use reword::{reword, reword_with_sign}; pub use staging::{discard_lines, stage_lines}; pub use stash::{ get_stashes, stash_apply, stash_drop, stash_pop, stash_save, diff --git a/asyncgit/src/sync/reword.rs b/asyncgit/src/sync/reword.rs index a503686321..1efb243ce6 100644 --- a/asyncgit/src/sync/reword.rs +++ b/asyncgit/src/sync/reword.rs @@ -13,11 +13,24 @@ pub fn reword( repo_path: &RepoPath, commit: CommitId, message: &str, +) -> Result { + reword_with_sign(repo_path, commit, message, None) +} + +/// This is the same as reword, but allows overriding signing behavior for this operation +pub fn reword_with_sign( + repo_path: &RepoPath, + commit: CommitId, + message: &str, + sign_override: Option, ) -> Result { let repo = repo(repo_path)?; let config = repo.config()?; + let should_sign = sign_override.unwrap_or_else(|| { + config.get_bool("commit.gpgsign").unwrap_or(false) + }); - if config.get_bool("commit.gpgsign").unwrap_or(false) { + if should_sign { // HACK: we undo the last commit and create a new one use crate::sync::utils::undo_last_commit; @@ -32,7 +45,11 @@ pub fn reword( .len() == 0 { undo_last_commit(repo_path)?; - return super::commit(repo_path, message); + return super::commit_with_sign( + repo_path, + message, + Some(true), + ); } return Err(Error::SignRewordLastCommitStaged); diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 357fef1df1..492f4718bd 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -49,6 +49,7 @@ pub struct TextInputComponent { theme: SharedTheme, key_config: SharedKeyConfig, input_type: InputType, + multiline_popup_height: u16, current_area: Cell, embed: bool, textarea: Option, @@ -72,6 +73,7 @@ impl TextInputComponent { default_msg: default_msg.to_string(), selected: None, input_type: InputType::Multiline, + multiline_popup_height: 20, current_area: Cell::new(Rect::default()), embed: false, textarea: None, @@ -88,6 +90,14 @@ impl TextInputComponent { self } + pub const fn with_multiline_popup_height( + mut self, + height: u16, + ) -> Self { + self.multiline_popup_height = height; + self + } + /// pub fn set_input_type(&mut self, input_type: InputType) { self.clear(); @@ -623,7 +633,11 @@ impl DrawableComponent for TextInputComponent { let area = if self.embed { rect } else if self.input_type == InputType::Multiline { - let area = ui::centered_rect(60, 20, f.area()); + let area = ui::centered_rect( + 60, + self.multiline_popup_height, + f.area(), + ); ui::rect_inside( Size::new(10, 3), f.area().into(), diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 24a9507a49..cbc292d92d 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -91,6 +91,7 @@ pub struct KeysList { pub find_commit_sha: GituiKeyEvent, pub commit_amend: GituiKeyEvent, pub toggle_signoff: GituiKeyEvent, + pub toggle_gpgsign: GituiKeyEvent, pub toggle_verify: GituiKeyEvent, pub copy: GituiKeyEvent, pub create_branch: GituiKeyEvent, @@ -189,6 +190,7 @@ impl Default for KeysList { find_commit_sha: GituiKeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL), commit_amend: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), toggle_signoff: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL), + toggle_gpgsign: GituiKeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL), toggle_verify: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), copy: GituiKeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()), create_branch: GituiKeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()), diff --git a/src/popups/commit.rs b/src/popups/commit.rs index b5dff7677c..b89558ac01 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -63,6 +63,7 @@ pub struct CommitPopup { commit_msg_history_idx: usize, options: SharedOptions, verify: bool, + sign: bool, } const FIRST_LINE_LIMIT: usize = 50; @@ -78,7 +79,8 @@ impl CommitPopup { "", &strings::commit_msg(&env.key_config), true, - ), + ) + .with_multiline_popup_height(64), key_config: env.key_config.clone(), git_branch_name: cached::BranchName::new( env.repo.clone(), @@ -89,6 +91,7 @@ impl CommitPopup { commit_msg_history_idx: 0, options: env.options.clone(), verify: true, + sign: false, } } @@ -279,19 +282,35 @@ impl CommitPopup { fn do_commit(&self, msg: &str) -> Result<()> { match &self.mode { - Mode::Normal => sync::commit(&self.repo.borrow(), msg)?, - Mode::Amend(amend) => { - sync::amend(&self.repo.borrow(), *amend, msg)? - } - Mode::Merge(ids) => { - sync::merge_commit(&self.repo.borrow(), msg, ids)? - } - Mode::Revert => { - sync::commit_revert(&self.repo.borrow(), msg)? - } + Mode::Normal => sync::commit_with_sign( + &self.repo.borrow(), + msg, + Some(self.sign), + )?, + Mode::Amend(amend) => sync::amend_with_sign( + &self.repo.borrow(), + *amend, + msg, + Some(self.sign), + )?, + Mode::Merge(ids) => sync::merge_commit_with_sign( + &self.repo.borrow(), + msg, + ids, + Some(self.sign), + )?, + Mode::Revert => sync::commit_revert_with_sign( + &self.repo.borrow(), + msg, + Some(self.sign), + )?, Mode::Reword(id) => { - let commit = - sync::reword(&self.repo.borrow(), *id, msg)?; + let commit = sync::reword_with_sign( + &self.repo.borrow(), + *id, + msg, + Some(self.sign), + )?; self.queue.push(InternalEvent::TabSwitchStatus); commit @@ -310,6 +329,17 @@ impl CommitPopup { && (self.is_empty() || !self.is_changed()) } + fn can_toggle_sign(&self) -> bool { + matches!( + self.mode, + Mode::Normal + | Mode::Amend(_) + | Mode::Merge(_) + | Mode::Revert + | Mode::Reword(_) + ) + } + fn is_empty(&self) -> bool { self.input.get_text().is_empty() } @@ -349,12 +379,21 @@ impl CommitPopup { self.verify = !self.verify; } + fn toggle_gpgsign(&mut self) { + self.sign = !self.sign; + } + pub fn open(&mut self, reword: Option) -> Result<()> { //only clear text if it was not a normal commit dlg before, so to preserve old commit msg that was edited if !matches!(self.mode, Mode::Normal) { self.input.clear(); } + self.sign = + get_config_string(&self.repo.borrow(), "commit.gpgsign")? + .and_then(|value| value.parse::().ok()) + .unwrap_or(false); + self.mode = Mode::Normal; let repo_state = sync::repo_state(&self.repo.borrow())?; @@ -517,6 +556,15 @@ impl Component for CommitPopup { true, )); + out.push(CommandInfo::new( + strings::commands::toggle_gpgsign( + &self.key_config, + self.sign, + ), + self.can_toggle_sign(), + true, + )); + out.push(CommandInfo::new( strings::commands::commit_amend(&self.key_config), self.can_amend(), @@ -575,6 +623,13 @@ impl Component for CommitPopup { { self.toggle_verify(); true + } else if key_match( + e, + self.key_config.keys.toggle_gpgsign, + ) && self.can_toggle_sign() + { + self.toggle_gpgsign(); + true } else if key_match( e, self.key_config.keys.commit_amend, diff --git a/src/strings.rs b/src/strings.rs index f66a9e93f5..c9d301c1e3 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1193,6 +1193,22 @@ pub mod commands { ) } + pub fn toggle_gpgsign( + key_config: &SharedKeyConfig, + current_signing: bool, + ) -> CommandText { + let verb = if current_signing { "disable" } else { "enable" }; + CommandText::new( + format!( + "{} signing [{}]", + verb, + key_config.get_hint(key_config.keys.toggle_gpgsign), + ), + "toggle GPG/SSH commit signing (-S) for this commit", + CMD_GROUP_COMMIT_POPUP, + ) + } + pub fn commit_amend(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!(