Skip to content

Support git commit signing using OpenPGP #1544

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

Merged
merged 14 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Collaborator

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)?;

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading