Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support git commit signing using OpenPGP #1544

Merged
merged 14 commits into from Mar 24, 2024
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
* sign commits using openpgp; implement `Sign` trait to implement more methods

## [0.25.2] - 2024-03-22

### Fixes
Expand Down
20 changes: 20 additions & 0 deletions asyncgit/src/error.rs
Expand Up @@ -84,6 +84,26 @@ pub enum Error {
///
#[error("git hook error: {0}")]
Hooks(#[from] git2_hooks::HooksError),

///
#[error("sign builder error: {0}")]
SignBuilder(#[from] crate::sync::sign::SignBuilderError),

///
#[error("sign error: {0}")]
Sign(#[from] crate::sync::sign::SignError),

///
#[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")]
SignAmendNonLastCommit,

///
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording non-last commits")]
SignRewordNonLastCommit,

///
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording commits with staged changes\ntry unstaging or stashing your changes")]
SignRewordLastCommitStaged,
}

///
Expand Down
56 changes: 51 additions & 5 deletions asyncgit/src/sync/commit.rs
@@ -1,7 +1,8 @@
//! Git Api for Commits
use super::{CommitId, RepoPath};
use crate::sync::sign::{SignBuilder, SignError};
use crate::{
error::Result,
error::{Error, Result},
sync::{repository::repo, utils::get_head_repo},
};
use git2::{
Expand All @@ -18,12 +19,27 @@ pub fn amend(
scope_time!("amend");

let repo = repo(repo_path)?;
let config = repo.config()?;

let commit = repo.find_commit(id.into())?;

let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;

if config.get_bool("commit.gpgsign").unwrap_or(false) {
// 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 Err(Error::SignAmendNonLastCommit);
}

let new_id = commit.amend(
Some("HEAD"),
None,
Expand Down Expand Up @@ -68,7 +84,7 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
scope_time!("commit");

let repo = repo(repo_path)?;

let config = repo.config()?;
let signature = signature_allow_undefined_name(&repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
Expand All @@ -82,16 +98,46 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {

let parents = parents.iter().collect::<Vec<_>>();

Ok(repo
.commit(
let commit_id = if config
.get_bool("commit.gpgsign")
.unwrap_or(false)
{
use crate::sync::sign::Sign;

let buffer = repo.commit_create_buffer(
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?;

let commit = std::str::from_utf8(&buffer).map_err(|_e| {
SignError::Shellout("utf8 conversion error".to_string())
})?;

let sign = SignBuilder::from_gitconfig(&repo, &config)?;
let signed_commit = sign.sign(&buffer)?;
let commit_id =
repo.commit_signed(commit, &signed_commit, None)?;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to support more than just gpg the third parameter signature_field needs to take something out of sign which in this case has to be gpgsig but something else for ssh

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to support more than just gpg the third parameter signature_field needs to take something out of sign which in this case has to be gpgsig but something else for ssh

I very much wish for ssh signing. I don't PGP/GPG but i ssh all the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@damccull this one only adds openpgp; there is another PR that wants to add ssh signing. @extrawurst wants them to adopt the Sign trait once this one here is merged though.


// manually advance to the new commit ID
// repo.commit does that on its own, repo.commit_signed does not
repo.head()?.set_target(commit_id, msg)?;
extrawurst marked this conversation as resolved.
Show resolved Hide resolved

commit_id
} else {
repo.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
.into())
};

Ok(commit_id.into())
}

/// Tag a commit.
Expand Down
1 change: 1 addition & 0 deletions asyncgit/src/sync/mod.rs
Expand Up @@ -25,6 +25,7 @@ pub mod remotes;
mod repository;
mod reset;
mod reword;
pub mod sign;
mod staging;
mod stash;
mod state;
Expand Down
28 changes: 27 additions & 1 deletion asyncgit/src/sync/reword.rs
Expand Up @@ -3,7 +3,7 @@ use git2::{Oid, RebaseOptions, Repository};
use super::{
commit::signature_allow_undefined_name,
repo,
utils::{bytes2string, get_head_refname},
utils::{bytes2string, get_head_refname, get_head_repo},
CommitId, RepoPath,
};
use crate::error::{Error, Result};
Expand All @@ -15,6 +15,32 @@ pub fn reword(
message: &str,
) -> Result<CommitId> {
let repo = repo(repo_path)?;
let config = repo.config()?;

if config.get_bool("commit.gpgsign").unwrap_or(false) {
// 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 {
// Check if there are any staged changes
let parent = repo.find_commit(head.into())?;
let tree = parent.tree()?;
if repo
.diff_tree_to_index(Some(&tree), None, None)?
.deltas()
.len() == 0
{
undo_last_commit(repo_path)?;
return super::commit(repo_path, message);
}

return Err(Error::SignRewordLastCommitStaged);
}

return Err(Error::SignRewordNonLastCommit);
}

let cur_branch_ref = get_head_refname(&repo)?;

match reword_internal(&repo, commit.get_oid(), message) {
Expand Down