diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09b579d4b2..e875ba13c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,23 @@ jobs: profile: minimal components: clippy + - name: Install GnuPG on Ubuntu + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get -qq install libgpg-error-dev libgpgme11-dev + + - name: Install GnuPG on MacOS + if: matrix.os == 'macos-latest' + run: brew install gpgme + + - name: Install GnuPG on Windows + if: matrix.os == 'windows-latest' + run: | + $path = [Environment]::GetEnvironmentVariable("path", "machine") + $newPath = ($path.Split(';') | Where-Object { $_ -eq 'C:\ProgramData\chocolatey\bin' }) -join ';' + [Environment]::SetEnvironmentVariable("path", $newPath, "machine") + choco install gpg4win + refreshenv + - name: Build Debug run: | rustc --version @@ -58,6 +75,9 @@ jobs: profile: minimal target: x86_64-unknown-linux-musl + - name: Install GnuPG + run: sudo apt-get -qq install libgpg-error-dev libgpgme11-dev + - name: Setup MUSL run: | sudo apt-get -qq install musl-tools diff --git a/Cargo.lock b/Cargo.lock index 52a1846123..e1aa8984a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,8 @@ version = "0.9.0" dependencies = [ "crossbeam-channel", "git2", + "gpg-error", + "gpgme", "invalidstring", "log", "rayon-core", @@ -219,6 +221,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", +] + [[package]] name = "cpp_demangle" version = "0.3.0" @@ -312,6 +323,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "cstr-argument" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20bd4e8067c20c7c3a4dea759ef91d4b18418ddb5bd8837ef6e2f2f93ca7ccbb" +dependencies = [ + "cfg-if", + "memchr", +] + +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + [[package]] name = "debugid" version = "0.7.2" @@ -432,6 +459,42 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gpg-error" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eafd8f60a0e9112caa9057f9a5fca2c3b17a9b6f3c035e79c35af9b94905137" +dependencies = [ + "libgpg-error-sys", +] + +[[package]] +name = "gpgme" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528b8b981855eb0c74696a408919b1bf5dbe7f4cd99261bb1b47097d9bf3478f" +dependencies = [ + "bitflags", + "conv", + "cstr-argument", + "gpg-error", + "gpgme-sys", + "libc", + "once_cell", + "smallvec", + "static_assertions", +] + +[[package]] +name = "gpgme-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b293be40a67cf00d4cdbefc8bc87c10d2b9581fcb9ac226084c53a556f2e31f" +dependencies = [ + "libc", + "libgpg-error-sys", +] + [[package]] name = "hermit-abi" version = "0.1.15" @@ -536,6 +599,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libgpg-error-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1088124ffd50d800fa4daa9854a1ce6fbaa4b9cc953ee571c6b3cc27e5928f" +dependencies = [ + "libc", +] + [[package]] name = "libz-sys" version = "1.0.25" @@ -727,6 +799,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + [[package]] name = "parking_lot" version = "0.10.2" @@ -1052,6 +1130,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "str_stack" version = "0.1.0" diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 63be7ff293..ed84abfd87 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -18,6 +18,8 @@ rayon-core = "1.7" crossbeam-channel = "0.4" log = "0.4" thiserror = "1.0" +gpg-error = "0.5.1" +gpgme = "0.9.2" [dev-dependencies] tempfile = "3.1" diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index f00f587b25..eab15fdb3f 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -13,6 +13,9 @@ pub enum Error { #[error("git error:{0}")] Git(#[from] git2::Error), + + #[error("gpg error:#{0}")] + Gpg(#[from] gpg_error::Error), } pub type Result = std::result::Result; diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index b24e7ce5ed..cb9e7761bd 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -1,6 +1,7 @@ use super::{get_head, utils::repo, CommitId}; use crate::error::Result; use git2::{ErrorCode, ObjectType, Repository, Signature}; +use gpgme::{Context, Protocol}; use scopetime::scope_time; /// @@ -54,6 +55,7 @@ pub fn commit(repo_path: &str, msg: &str) -> Result { scope_time!("commit"); let repo = repo(repo_path)?; + let config = repo.config()?; let signature = signature_allow_undefined_name(&repo)?; let mut index = repo.index()?; @@ -68,8 +70,68 @@ pub fn commit(repo_path: &str, msg: &str) -> Result { let parents = parents.iter().collect::>(); - Ok(repo - .commit( + let commit_oid = if config + .get_bool("commit.gpgsign") + .unwrap_or(false) + { + // Generate commit content + let commit_bufffer = repo.commit_create_buffer( + &signature, + &signature, + msg, + &tree, + parents.as_slice(), + )?; + let commit_content = commit_bufffer + .as_str() + .expect("Buffer was not valid UTF-8"); + + // Prepare to sign using the designated key in the user's git config + let mut gpg_ctx = Context::from_protocol(Protocol::OpenPgp)?; + if let Ok(key_id) = config.get_string("user.signingkey") { + let key = gpg_ctx.get_key(key_id)?; + gpg_ctx.add_signer(&key)?; + } + gpg_ctx.set_armor(true); + + // Create GPG signature for commit content + let mut signature_buffer = Vec::new(); + gpg_ctx + .sign_detached(&*commit_bufffer, &mut signature_buffer)?; + let gpg_signature = std::str::from_utf8(&signature_buffer) + .expect("Buffer was not valid UTF-8"); + + let commit_oid = repo.commit_signed( + &commit_content, + &gpg_signature, + None, + )?; + + match repo.head() { + // If HEAD reference is returned, simply update the target. + Ok(mut head) => { + head.set_target(commit_oid, msg)?; + } + // If there is an error getting HEAD, likely it is a new repo + // and a reference to a default branch needs to be created. + Err(_) => { + // Default branch name behavior as of git 2.28. + let default_branch_name = config + .get_str("init.defaultBranch") + .unwrap_or("master"); + + repo.reference( + &format!("refs/heads/{}", default_branch_name), + commit_oid, + true, + msg, + )?; + } + } + + commit_oid + } else { + repo.commit( Some("HEAD"), &signature, &signature, @@ -77,7 +139,9 @@ pub fn commit(repo_path: &str, msg: &str) -> Result { &tree, parents.as_slice(), )? - .into()) + }; + + Ok(commit_oid.into()) } /// Tag a commit.