diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs index a84ce79b58..0d6a40fdb9 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs @@ -9,6 +9,7 @@ use gitbutler_oxidize::gix_to_git2_oid; use gitbutler_oxidize::{git2_to_gix_object_id, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, ReferenceName, Refname}; +use gitbutler_repo::committing::RepositoryExt as _; use gitbutler_repo::RepositoryExt; use gitbutler_repo::SignaturePurpose; use gitbutler_repo_actions::RepoActionsExt; diff --git a/crates/gitbutler-branch-actions/src/integration.rs b/crates/gitbutler-branch-actions/src/integration.rs index 37d3197090..abaff49904 100644 --- a/crates/gitbutler-branch-actions/src/integration.rs +++ b/crates/gitbutler-branch-actions/src/integration.rs @@ -11,8 +11,8 @@ use gitbutler_error::error::Marker; use gitbutler_operating_modes::OPEN_WORKSPACE_REFS; use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; +use gitbutler_repo::committing::RepositoryExt as _; use gitbutler_repo::logging::{LogUntil, RepositoryExt as _}; -use gitbutler_repo::RepositoryExt; use gitbutler_repo::SignaturePurpose; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use tracing::instrument; diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 62b3762c2f..05dcdfdaf1 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -28,6 +28,7 @@ use gitbutler_oxidize::{ use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::{ + committing::RepositoryExt as _, logging::{LogUntil, RepositoryExt as _}, rebase::{cherry_rebase, cherry_rebase_group}, RepositoryExt, diff --git a/crates/gitbutler-branch-actions/tests/extra/mod.rs b/crates/gitbutler-branch-actions/tests/extra/mod.rs index 89ca39b90e..d42ed29323 100644 --- a/crates/gitbutler-branch-actions/tests/extra/mod.rs +++ b/crates/gitbutler-branch-actions/tests/extra/mod.rs @@ -20,7 +20,7 @@ use gitbutler_branch_actions::{ }; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::CommitHeadersV2}; use gitbutler_reference::{Refname, RemoteRefname}; -use gitbutler_repo::RepositoryExt; +use gitbutler_repo::committing::RepositoryExt as _; use gitbutler_stack::{BranchOwnershipClaims, Target, VirtualBranchesHandle}; use gitbutler_testsupport::{commit_all, virtual_branches::set_test_target, Case, Suite}; use pretty_assertions::assert_eq; diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index 72eee0caaa..54e29645ae 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -21,6 +21,8 @@ use gitbutler_operating_modes::{ use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_index, GixRepositoryExt}; use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission}; use gitbutler_reference::{ReferenceName, Refname}; +use gitbutler_repo::committing::RepositoryExt as _; +use gitbutler_repo::identity::RepositoryExt as _; use gitbutler_repo::{rebase::cherry_rebase, RepositoryExt}; use gitbutler_repo::{signature, SignaturePurpose}; use gitbutler_stack::{Stack, VirtualBranchesHandle}; diff --git a/crates/gitbutler-repo-actions/src/repository.rs b/crates/gitbutler-repo-actions/src/repository.rs index 00b0a15fa6..e08e6b188c 100644 --- a/crates/gitbutler-repo-actions/src/repository.rs +++ b/crates/gitbutler-repo-actions/src/repository.rs @@ -10,7 +10,9 @@ use gitbutler_stack::{Stack, StackId}; use crate::askpass; use gitbutler_repo::{ + committing::RepositoryExt as _, credentials, + identity::RepositoryExt as _, logging::{LogUntil, RepositoryExt as _}, RepositoryExt, }; diff --git a/crates/gitbutler-repo/src/commands.rs b/crates/gitbutler-repo/src/commands.rs index 1a99111725..a215f675bd 100644 --- a/crates/gitbutler-repo/src/commands.rs +++ b/crates/gitbutler-repo/src/commands.rs @@ -1,4 +1,4 @@ -use crate::{remote::GitRemote, Config, RepositoryExt}; +use crate::{remote::GitRemote, sigining::sign_buffer, Config, RepositoryExt}; use anyhow::{bail, Result}; use base64::engine::Engine as _; use git2::Oid; @@ -138,7 +138,7 @@ impl RepoCommands for Project { fn check_signing_settings(&self) -> Result { let ctx = CommandContext::open(self)?; - let signed = ctx.repo().sign_buffer(b"test"); + let signed = sign_buffer(ctx.repo(), b"test"); match signed { Ok(_) => Ok(true), Err(e) => Err(e), diff --git a/crates/gitbutler-repo/src/committing.rs b/crates/gitbutler-repo/src/committing.rs new file mode 100644 index 0000000000..fb93c98c32 --- /dev/null +++ b/crates/gitbutler-repo/src/committing.rs @@ -0,0 +1,80 @@ +use anyhow::{anyhow, Result}; +use gitbutler_commit::commit_headers::CommitHeadersV2; +use gitbutler_config::git::{GbConfig, GitConfig as _}; +use gitbutler_error::error::Code; +use gitbutler_oxidize::{git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid}; +use gitbutler_reference::Refname; +use gix::objs::WriteTo as _; + +use crate::sigining::sign_buffer; + +pub trait RepositoryExt { + #[allow(clippy::too_many_arguments)] + fn commit_with_signature( + &self, + update_ref: Option<&Refname>, + author: &git2::Signature<'_>, + committer: &git2::Signature<'_>, + message: &str, + tree: &git2::Tree<'_>, + parents: &[&git2::Commit<'_>], + commit_headers: Option, + ) -> Result; +} + +impl RepositoryExt for git2::Repository { + #[allow(clippy::too_many_arguments)] + fn commit_with_signature( + &self, + update_ref: Option<&Refname>, + author: &git2::Signature<'_>, + committer: &git2::Signature<'_>, + message: &str, + tree: &git2::Tree<'_>, + parents: &[&git2::Commit<'_>], + commit_headers: Option, + ) -> Result { + let repo = gix::open(self.path())?; + let mut commit = gix::objs::Commit { + message: message.into(), + tree: git2_to_gix_object_id(tree.id()), + author: git2_signature_to_gix_signature(author), + committer: git2_signature_to_gix_signature(committer), + encoding: None, + parents: parents + .iter() + .map(|commit| git2_to_gix_object_id(commit.id())) + .collect(), + extra_headers: commit_headers.unwrap_or_default().into(), + }; + + if self.gb_config()?.sign_commits.unwrap_or(false) { + let mut buf = Vec::new(); + commit.write_to(&mut buf)?; + let signature = sign_buffer(self, &buf); + match signature { + Ok(signature) => { + commit.extra_headers.push(("gpgsig".into(), signature)); + } + Err(e) => { + // If signing fails, set the "gitbutler.signCommits" config to false before erroring out + self.set_gb_config(GbConfig { + sign_commits: Some(false), + ..GbConfig::default() + })?; + return Err( + anyhow!("Failed to sign commit: {}", e).context(Code::CommitSigningFailed) + ); + } + } + } + // TODO: extra-headers should be supported in `gix` directly. + let oid = gix_to_git2_oid(repo.write_object(&commit)?); + + // update reference + if let Some(refname) = update_ref { + self.reference(&refname.to_string(), oid, true, message)?; + } + Ok(oid) + } +} diff --git a/crates/gitbutler-repo/src/identity.rs b/crates/gitbutler-repo/src/identity.rs new file mode 100644 index 0000000000..095606b34c --- /dev/null +++ b/crates/gitbutler-repo/src/identity.rs @@ -0,0 +1,35 @@ +use anyhow::{Context as _, Result}; +use gitbutler_error::error::Code; +use gitbutler_oxidize::gix_to_git2_signature; + +use crate::{Config, SignaturePurpose}; + +pub trait RepositoryExt { + fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>; +} + +impl RepositoryExt for git2::Repository { + fn signatures(&self) -> Result<(git2::Signature, git2::Signature)> { + let repo = gix::open(self.path())?; + + let author = repo + .author() + .transpose()? + .map(gix_to_git2_signature) + .transpose()? + .context("No author is configured in Git") + .context(Code::AuthorMissing)?; + + let config: Config = self.into(); + let committer = if config.user_real_comitter()? { + repo.committer() + .transpose()? + .map(gix_to_git2_signature) + .unwrap_or_else(|| crate::signature(SignaturePurpose::Committer)) + } else { + crate::signature(SignaturePurpose::Committer) + }?; + + Ok((author, committer)) + } +} diff --git a/crates/gitbutler-repo/src/lib.rs b/crates/gitbutler-repo/src/lib.rs index 3825920e5a..5fb2bd6515 100644 --- a/crates/gitbutler-repo/src/lib.rs +++ b/crates/gitbutler-repo/src/lib.rs @@ -16,7 +16,10 @@ pub use config::Config; pub mod temporary_workdir; +pub mod committing; +pub mod identity; pub mod logging; +mod sigining; use gitbutler_oxidize::gix_to_git2_signature; pub const GITBUTLER_COMMIT_AUTHOR_NAME: &str = "GitButler"; diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs index f9707d9c97..c859c67c19 100644 --- a/crates/gitbutler-repo/src/rebase.rs +++ b/crates/gitbutler-repo/src/rebase.rs @@ -1,8 +1,9 @@ use std::{collections::HashSet, path::PathBuf}; use crate::{ + committing::RepositoryExt as _, + identity::RepositoryExt as _, logging::{LogUntil, RepositoryExt as _}, - RepositoryExt as _, }; use anyhow::{Context, Result}; use bstr::ByteSlice; @@ -130,17 +131,17 @@ fn commit_unconflicted_cherry_result<'repository>( let (_, committer) = repository.signatures()?; - let commit_oid = crate::RepositoryExt::commit_with_signature( - repository, - None, - &to_rebase.author(), - &committer, - &to_rebase.message_bstr().to_str_lossy(), - &merge_tree, - &[&head], - commit_headers, - ) - .context("failed to create commit")?; + let commit_oid = repository + .commit_with_signature( + None, + &to_rebase.author(), + &committer, + &to_rebase.message_bstr().to_str_lossy(), + &merge_tree, + &[&head], + commit_headers, + ) + .context("failed to create commit")?; repository .find_commit(commit_oid) @@ -211,19 +212,19 @@ fn commit_conflicted_cherry_result<'repository>( let (_, committer) = repository.signatures()?; - let commit_oid = crate::RepositoryExt::commit_with_signature( - repository, - None, - &to_rebase.author(), - &committer, - &to_rebase.message_bstr().to_str_lossy(), - &repository - .find_tree(tree_oid) - .context("failed to find tree")?, - &[&head], - commit_headers, - ) - .context("failed to create commit")?; + let commit_oid = repository + .commit_with_signature( + None, + &to_rebase.author(), + &committer, + &to_rebase.message_bstr().to_str_lossy(), + &repository + .find_tree(tree_oid) + .context("failed to find tree")?, + &[&head], + commit_headers, + ) + .context("failed to create commit")?; repository .find_commit(commit_oid) @@ -371,22 +372,22 @@ pub fn gitbutler_merge_commits<'repository>( }; let (author, committer) = repository.signatures()?; - let commit_oid = crate::RepositoryExt::commit_with_signature( - repository, - None, - &author, - &committer, - &format!( - "Merge `{}` into `{}`", - incoming_branch_name, target_branch_name - ), - &repository - .find_tree(tree_oid) - .context("failed to find tree")?, - &[&target_commit, &incoming_commit], - Some(commit_headers), - ) - .context("failed to create commit")?; + let commit_oid = repository + .commit_with_signature( + None, + &author, + &committer, + &format!( + "Merge `{}` into `{}`", + incoming_branch_name, target_branch_name + ), + &repository + .find_tree(tree_oid) + .context("failed to find tree")?, + &[&target_commit, &incoming_commit], + Some(commit_headers), + ) + .context("failed to create commit")?; Ok(repository.find_commit(commit_oid)?) } diff --git a/crates/gitbutler-repo/src/repository_ext.rs b/crates/gitbutler-repo/src/repository_ext.rs index d897314358..562393ce4b 100644 --- a/crates/gitbutler-repo/src/repository_ext.rs +++ b/crates/gitbutler-repo/src/repository_ext.rs @@ -1,24 +1,12 @@ -use crate::Config; -use crate::SignaturePurpose; use anyhow::{anyhow, bail, Context, Result}; -use bstr::BString; use git2::{StatusOptions, Tree}; -use gitbutler_commit::commit_headers::CommitHeadersV2; -use gitbutler_config::git::{GbConfig, GitConfig}; -use gitbutler_error::error::Code; -use gitbutler_oxidize::{ - git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid, gix_to_git2_signature, -}; use gitbutler_reference::{Refname, RemoteRefname}; use gix::filter::plumbing::pipeline::convert::ToGitOutcome; use gix::fs::is_executable; -use gix::objs::WriteTo; use std::io; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; #[cfg(windows)] use std::os::windows::process::CommandExt; -use std::{io::Write, path::Path, process::Stdio, str}; +use std::{path::Path, str}; use tracing::instrument; /// Extension trait for `git2::Repository`. @@ -34,13 +22,9 @@ pub trait RepositoryExt { /// conflict with the libgit2 binding I upstreamed when it eventually /// gets merged. fn merge_base_octopussy(&self, ids: &[git2::Oid]) -> Result; - fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>; fn remote_branches(&self) -> Result>; fn remotes_as_string(&self) -> Result>; - /// `buffer` is the commit object to sign, but in theory could be anything to compute the signature for. - /// Returns the computed signature. - fn sign_buffer(&self, buffer: &[u8]) -> Result; fn checkout_tree_builder<'a>(&'a self, tree: &'a git2::Tree<'a>) -> CheckoutTreeBuidler<'a>; fn maybe_find_branch_by_refname(&self, name: &Refname) -> Result>; /// Based on the index, add all data similar to `git add .` and create a tree from it, which is returned. @@ -52,18 +36,6 @@ pub trait RepositoryExt { /// /// This is for safety to assure the repository actually is in 'gitbutler mode'. fn workspace_ref_from_head(&self) -> Result>; - - #[allow(clippy::too_many_arguments)] - fn commit_with_signature( - &self, - update_ref: Option<&Refname>, - author: &git2::Signature<'_>, - committer: &git2::Signature<'_>, - message: &str, - tree: &git2::Tree<'_>, - parents: &[&git2::Commit<'_>], - commit_headers: Option, - ) -> Result; } impl RepositoryExt for git2::Repository { @@ -220,207 +192,6 @@ impl RepositoryExt for git2::Repository { } } - #[allow(clippy::too_many_arguments)] - fn commit_with_signature( - &self, - update_ref: Option<&Refname>, - author: &git2::Signature<'_>, - committer: &git2::Signature<'_>, - message: &str, - tree: &git2::Tree<'_>, - parents: &[&git2::Commit<'_>], - commit_headers: Option, - ) -> Result { - let repo = gix::open(self.path())?; - let mut commit = gix::objs::Commit { - message: message.into(), - tree: git2_to_gix_object_id(tree.id()), - author: git2_signature_to_gix_signature(author), - committer: git2_signature_to_gix_signature(committer), - encoding: None, - parents: parents - .iter() - .map(|commit| git2_to_gix_object_id(commit.id())) - .collect(), - extra_headers: commit_headers.unwrap_or_default().into(), - }; - - if self.gb_config()?.sign_commits.unwrap_or(false) { - let mut buf = Vec::new(); - commit.write_to(&mut buf)?; - let signature = self.sign_buffer(&buf); - match signature { - Ok(signature) => { - commit.extra_headers.push(("gpgsig".into(), signature)); - } - Err(e) => { - // If signing fails, set the "gitbutler.signCommits" config to false before erroring out - self.set_gb_config(GbConfig { - sign_commits: Some(false), - ..GbConfig::default() - })?; - return Err( - anyhow!("Failed to sign commit: {}", e).context(Code::CommitSigningFailed) - ); - } - } - } - // TODO: extra-headers should be supported in `gix` directly. - let oid = gix_to_git2_oid(repo.write_object(&commit)?); - - // update reference - if let Some(refname) = update_ref { - self.reference(&refname.to_string(), oid, true, message)?; - } - Ok(oid) - } - - fn sign_buffer(&self, buffer: &[u8]) -> Result { - // check git config for gpg.signingkey - // TODO: support gpg.ssh.defaultKeyCommand to get the signing key if this value doesn't exist - let signing_key = self.config()?.get_string("user.signingkey"); - if let Ok(signing_key) = signing_key { - let sign_format = self.config()?.get_string("gpg.format"); - let is_ssh = if let Ok(sign_format) = sign_format { - sign_format == "ssh" - } else { - false - }; - - if is_ssh { - // write commit data to a temp file so we can sign it - let mut signature_storage = tempfile::NamedTempFile::new()?; - signature_storage.write_all(buffer)?; - let buffer_file_to_sign_path = signature_storage.into_temp_path(); - - let gpg_program = self.config()?.get_string("gpg.ssh.program"); - let mut gpg_program = gpg_program.unwrap_or("ssh-keygen".to_string()); - // if cmd is "", use gpg - if gpg_program.is_empty() { - gpg_program = "ssh-keygen".to_string(); - } - - let mut cmd_string = format!("{} -Y sign -n git -f ", gpg_program); - - let buffer_file_to_sign_path_str = buffer_file_to_sign_path - .to_str() - .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))? - .to_string(); - - let output; - // support literal ssh key - if let (true, signing_key) = is_literal_ssh_key(&signing_key) { - // write the key to a temp file - let mut key_storage = tempfile::NamedTempFile::new()?; - key_storage.write_all(signing_key.as_bytes())?; - - // if on unix - #[cfg(unix)] - { - // make sure the tempfile permissions are acceptable for a private ssh key - let mut permissions = key_storage.as_file().metadata()?.permissions(); - permissions.set_mode(0o600); - key_storage.as_file().set_permissions(permissions)?; - } - - let key_file_path = key_storage.into_temp_path(); - - let args = format!( - "{} -U {}", - key_file_path.to_string_lossy(), - buffer_file_to_sign_path.to_string_lossy() - ); - cmd_string += &args; - - let mut signing_cmd: std::process::Command = - gix::command::prepare(cmd_string).with_shell().into(); - - #[cfg(windows)] - signing_cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW - - signing_cmd.stderr(Stdio::piped()); - signing_cmd.stdout(Stdio::piped()); - signing_cmd.stdin(Stdio::null()); - - let child = signing_cmd.spawn()?; - output = child.wait_with_output()?; - } else { - let args = format!("{} {}", signing_key, buffer_file_to_sign_path_str); - cmd_string += &args; - - let mut signing_cmd: std::process::Command = - gix::command::prepare(cmd_string).with_shell().into(); - - #[cfg(windows)] - signing_cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW - - signing_cmd.stderr(Stdio::piped()); - signing_cmd.stdout(Stdio::piped()); - signing_cmd.stdin(Stdio::null()); - - let child = signing_cmd.spawn()?; - output = child.wait_with_output()?; - } - - if output.status.success() { - // read signed_storage path plus .sig - let signature_path = buffer_file_to_sign_path.with_extension("sig"); - let sig_data = std::fs::read(signature_path)?; - let signature = BString::new(sig_data); - return Ok(signature); - } else { - let stderr = BString::new(output.stderr); - let stdout = BString::new(output.stdout); - let std_both = format!("{} {}", stdout, stderr); - bail!("Failed to sign SSH: {}", std_both); - } - } else { - let gpg_program = self - .config()? - .get_path("gpg.program") - .ok() - .filter(|gpg| !gpg.as_os_str().is_empty()) - .unwrap_or_else(|| "gpg".into()); - - let mut cmd = std::process::Command::new(&gpg_program); - - cmd.args(["--status-fd=2", "-bsau", &signing_key]) - .arg("-") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()); - - #[cfg(windows)] - cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW - - let mut child = match cmd.spawn() { - Ok(child) => child, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - bail!("Could not find '{}'. Please make sure it is in your `PATH` or configure the full path using `gpg.program` in the Git configuration", gpg_program.display()) - } - Err(err) => { - return Err(err) - .context(format!("Could not execute GPG program using {:?}", cmd)) - } - }; - child.stdin.take().expect("configured").write_all(buffer)?; - - let output = child.wait_with_output()?; - if output.status.success() { - // read stdout - let signature = BString::new(output.stdout); - return Ok(signature); - } else { - let stderr = BString::new(output.stderr); - let stdout = BString::new(output.stdout); - let std_both = format!("{} {}", stdout, stderr); - bail!("Failed to sign GPG: {}", std_both); - } - } - } - Err(anyhow::anyhow!("No signing key found")) - } - fn remotes_as_string(&self) -> Result> { Ok(self.remotes().map(|string_array| { string_array @@ -439,30 +210,6 @@ impl RepositoryExt for git2::Repository { .collect::>>() } - fn signatures(&self) -> Result<(git2::Signature, git2::Signature)> { - let repo = gix::open(self.path())?; - - let author = repo - .author() - .transpose()? - .map(gix_to_git2_signature) - .transpose()? - .context("No author is configured in Git") - .context(Code::AuthorMissing)?; - - let config: Config = self.into(); - let committer = if config.user_real_comitter()? { - repo.committer() - .transpose()? - .map(gix_to_git2_signature) - .unwrap_or_else(|| crate::signature(SignaturePurpose::Committer)) - } else { - crate::signature(SignaturePurpose::Committer) - }?; - - Ok((author, committer)) - } - fn merge_base_octopussy(&self, ids: &[git2::Oid]) -> Result { if ids.len() < 2 { bail!("Merge base octopussy requires at least two commit ids to operate on"); diff --git a/crates/gitbutler-repo/src/sigining.rs b/crates/gitbutler-repo/src/sigining.rs new file mode 100644 index 0000000000..f01f546eb8 --- /dev/null +++ b/crates/gitbutler-repo/src/sigining.rs @@ -0,0 +1,156 @@ +use std::{io::Write as _, process::Stdio}; + +use anyhow::{bail, Context as _, Result}; +use bstr::BString; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt as _; + +use crate::repository_ext::is_literal_ssh_key; + +/// `buffer` is the commit object to sign, but in theory could be anything to compute the signature for. +/// Returns the computed signature. +pub(crate) fn sign_buffer(repository: &git2::Repository, buffer: &[u8]) -> Result { + // check git config for gpg.signingkey + // TODO: support gpg.ssh.defaultKeyCommand to get the signing key if this value doesn't exist + let signing_key = repository.config()?.get_string("user.signingkey"); + if let Ok(signing_key) = signing_key { + let sign_format = repository.config()?.get_string("gpg.format"); + let is_ssh = if let Ok(sign_format) = sign_format { + sign_format == "ssh" + } else { + false + }; + + if is_ssh { + // write commit data to a temp file so we can sign it + let mut signature_storage = tempfile::NamedTempFile::new()?; + signature_storage.write_all(buffer)?; + let buffer_file_to_sign_path = signature_storage.into_temp_path(); + + let gpg_program = repository.config()?.get_string("gpg.ssh.program"); + let mut gpg_program = gpg_program.unwrap_or("ssh-keygen".to_string()); + // if cmd is "", use gpg + if gpg_program.is_empty() { + gpg_program = "ssh-keygen".to_string(); + } + + let mut cmd_string = format!("{} -Y sign -n git -f ", gpg_program); + + let buffer_file_to_sign_path_str = buffer_file_to_sign_path + .to_str() + .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))? + .to_string(); + + let output; + // support literal ssh key + if let (true, signing_key) = is_literal_ssh_key(&signing_key) { + // write the key to a temp file + let mut key_storage = tempfile::NamedTempFile::new()?; + key_storage.write_all(signing_key.as_bytes())?; + + // if on unix + #[cfg(unix)] + { + // make sure the tempfile permissions are acceptable for a private ssh key + let mut permissions = key_storage.as_file().metadata()?.permissions(); + permissions.set_mode(0o600); + key_storage.as_file().set_permissions(permissions)?; + } + + let key_file_path = key_storage.into_temp_path(); + + let args = format!( + "{} -U {}", + key_file_path.to_string_lossy(), + buffer_file_to_sign_path.to_string_lossy() + ); + cmd_string += &args; + + let mut signing_cmd: std::process::Command = + gix::command::prepare(cmd_string).with_shell().into(); + + #[cfg(windows)] + signing_cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + + signing_cmd.stderr(Stdio::piped()); + signing_cmd.stdout(Stdio::piped()); + signing_cmd.stdin(Stdio::null()); + + let child = signing_cmd.spawn()?; + output = child.wait_with_output()?; + } else { + let args = format!("{} {}", signing_key, buffer_file_to_sign_path_str); + cmd_string += &args; + + let mut signing_cmd: std::process::Command = + gix::command::prepare(cmd_string).with_shell().into(); + + #[cfg(windows)] + signing_cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + + signing_cmd.stderr(Stdio::piped()); + signing_cmd.stdout(Stdio::piped()); + signing_cmd.stdin(Stdio::null()); + + let child = signing_cmd.spawn()?; + output = child.wait_with_output()?; + } + + if output.status.success() { + // read signed_storage path plus .sig + let signature_path = buffer_file_to_sign_path.with_extension("sig"); + let sig_data = std::fs::read(signature_path)?; + let signature = BString::new(sig_data); + return Ok(signature); + } else { + let stderr = BString::new(output.stderr); + let stdout = BString::new(output.stdout); + let std_both = format!("{} {}", stdout, stderr); + bail!("Failed to sign SSH: {}", std_both); + } + } else { + let gpg_program = repository + .config()? + .get_path("gpg.program") + .ok() + .filter(|gpg| !gpg.as_os_str().is_empty()) + .unwrap_or_else(|| "gpg".into()); + + let mut cmd = std::process::Command::new(&gpg_program); + + cmd.args(["--status-fd=2", "-bsau", &signing_key]) + .arg("-") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()); + + #[cfg(windows)] + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + + let mut child = match cmd.spawn() { + Ok(child) => child, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + bail!("Could not find '{}'. Please make sure it is in your `PATH` or configure the full path using `gpg.program` in the Git configuration", gpg_program.display()) + } + Err(err) => { + return Err(err) + .context(format!("Could not execute GPG program using {:?}", cmd)) + } + }; + child.stdin.take().expect("configured").write_all(buffer)?; + + let output = child.wait_with_output()?; + if output.status.success() { + // read stdout + let signature = BString::new(output.stdout); + return Ok(signature); + } else { + let stderr = BString::new(output.stderr); + let stdout = BString::new(output.stdout); + let std_both = format!("{} {}", stdout, stderr); + bail!("Failed to sign GPG: {}", std_both); + } + } + } + Err(anyhow::anyhow!("No signing key found")) +} diff --git a/crates/gitbutler-stack/src/stack.rs b/crates/gitbutler-stack/src/stack.rs index 32eb40da92..4c96bbdbbe 100644 --- a/crates/gitbutler-stack/src/stack.rs +++ b/crates/gitbutler-stack/src/stack.rs @@ -11,9 +11,9 @@ use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt; use gitbutler_id::id::Id; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname, VirtualRefname}; +use gitbutler_repo::identity::RepositoryExt as _; use gitbutler_repo::logging::LogUntil; use gitbutler_repo::logging::RepositoryExt as _; -use gitbutler_repo::RepositoryExt; use gix::validate::reference::name_partial; use gix_utils::str::decompose; use itertools::Itertools; diff --git a/crates/gitbutler-testsupport/src/suite.rs b/crates/gitbutler-testsupport/src/suite.rs index 1667b25dee..2794524058 100644 --- a/crates/gitbutler-testsupport/src/suite.rs +++ b/crates/gitbutler-testsupport/src/suite.rs @@ -5,7 +5,7 @@ use std::{ }; use gitbutler_command_context::CommandContext; -use gitbutler_repo::RepositoryExt; +use gitbutler_repo::committing::RepositoryExt as _; use tempfile::{tempdir, TempDir}; use crate::test_project::setup_config; diff --git a/crates/gitbutler-testsupport/src/test_project.rs b/crates/gitbutler-testsupport/src/test_project.rs index 7b4cb3e004..b15df05889 100644 --- a/crates/gitbutler-testsupport/src/test_project.rs +++ b/crates/gitbutler-testsupport/src/test_project.rs @@ -1,7 +1,7 @@ use crate::{init_opts, VAR_NO_CLEANUP}; use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_reference::{LocalRefname, Refname}; -use gitbutler_repo::RepositoryExt; +use gitbutler_repo::{committing::RepositoryExt as _, RepositoryExt}; use std::{fs, path, path::PathBuf}; use tempfile::TempDir; diff --git a/crates/gitbutler-testsupport/src/testing_repository.rs b/crates/gitbutler-testsupport/src/testing_repository.rs index 7c962ccc2d..5f6408aa28 100644 --- a/crates/gitbutler-testsupport/src/testing_repository.rs +++ b/crates/gitbutler-testsupport/src/testing_repository.rs @@ -3,7 +3,7 @@ use std::fs; use crate::init_opts; use gitbutler_commit::commit_headers::CommitHeadersV2; use gitbutler_oxidize::git2_to_gix_object_id; -use gitbutler_repo::RepositoryExt; +use gitbutler_repo::committing::RepositoryExt as _; use gix_testtools::bstr::ByteSlice as _; use tempfile::{tempdir, TempDir}; use uuid::Uuid; diff --git a/crates/gitbutler-workspace/src/branch_trees.rs b/crates/gitbutler-workspace/src/branch_trees.rs index aabe46b850..7e8cfe2ea7 100644 --- a/crates/gitbutler-workspace/src/branch_trees.rs +++ b/crates/gitbutler-workspace/src/branch_trees.rs @@ -4,8 +4,9 @@ use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt as _; use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; -use gitbutler_repo::rebase::cherry_rebase_group; +use gitbutler_repo::committing::RepositoryExt as _; use gitbutler_repo::RepositoryExt as _; +use gitbutler_repo::{identity::RepositoryExt as _, rebase::cherry_rebase_group}; use gitbutler_stack::{Stack, VirtualBranchesHandle}; use tracing::instrument;