Skip to content

Commit 5b3e2c9

Browse files
Support git commit signing using OpenPGP (#1544)
* Support git commit signing using OpenPGP * workaround for amending signed commits * workaround for rewording signed commits * support signing initial commit * return both signature and signature_field value from sign --------- Co-authored-by: Utkarsh Gupta <[email protected]>
1 parent 5131aba commit 5b3e2c9

File tree

7 files changed

+443
-17
lines changed

7 files changed

+443
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
* sign commits using openpgp; implement `Sign` trait to implement more methods
12+
1013
## [0.25.2] - 2024-03-22
1114

1215
### Fixes

asyncgit/src/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ pub enum Error {
8484
///
8585
#[error("git hook error: {0}")]
8686
Hooks(#[from] git2_hooks::HooksError),
87+
88+
///
89+
#[error("sign builder error: {0}")]
90+
SignBuilder(#[from] crate::sync::sign::SignBuilderError),
91+
92+
///
93+
#[error("sign error: {0}")]
94+
Sign(#[from] crate::sync::sign::SignError),
95+
96+
///
97+
#[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")]
98+
SignAmendNonLastCommit,
99+
100+
///
101+
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording non-last commits")]
102+
SignRewordNonLastCommit,
103+
104+
///
105+
#[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")]
106+
SignRewordLastCommitStaged,
87107
}
88108

89109
///

asyncgit/src/sync/commit.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//! Git Api for Commits
22
use super::{CommitId, RepoPath};
3+
use crate::sync::sign::{SignBuilder, SignError};
34
use crate::{
4-
error::Result,
5+
error::{Error, Result},
56
sync::{repository::repo, utils::get_head_repo},
67
};
78
use git2::{
@@ -18,12 +19,27 @@ pub fn amend(
1819
scope_time!("amend");
1920

2021
let repo = repo(repo_path)?;
22+
let config = repo.config()?;
23+
2124
let commit = repo.find_commit(id.into())?;
2225

2326
let mut index = repo.index()?;
2427
let tree_id = index.write_tree()?;
2528
let tree = repo.find_tree(tree_id)?;
2629

30+
if config.get_bool("commit.gpgsign").unwrap_or(false) {
31+
// HACK: we undo the last commit and create a new one
32+
use crate::sync::utils::undo_last_commit;
33+
34+
let head = get_head_repo(&repo)?;
35+
if head == commit.id().into() {
36+
undo_last_commit(repo_path)?;
37+
return self::commit(repo_path, msg);
38+
}
39+
40+
return Err(Error::SignAmendNonLastCommit);
41+
}
42+
2743
let new_id = commit.amend(
2844
Some("HEAD"),
2945
None,
@@ -68,7 +84,7 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
6884
scope_time!("commit");
6985

7086
let repo = repo(repo_path)?;
71-
87+
let config = repo.config()?;
7288
let signature = signature_allow_undefined_name(&repo)?;
7389
let mut index = repo.index()?;
7490
let tree_id = index.write_tree()?;
@@ -82,16 +98,62 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
8298

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

85-
Ok(repo
86-
.commit(
101+
let commit_id = if config
102+
.get_bool("commit.gpgsign")
103+
.unwrap_or(false)
104+
{
105+
use crate::sync::sign::Sign;
106+
107+
let buffer = repo.commit_create_buffer(
108+
&signature,
109+
&signature,
110+
msg,
111+
&tree,
112+
parents.as_slice(),
113+
)?;
114+
115+
let commit = std::str::from_utf8(&buffer).map_err(|_e| {
116+
SignError::Shellout("utf8 conversion error".to_string())
117+
})?;
118+
119+
let sign = SignBuilder::from_gitconfig(&repo, &config)?;
120+
let (signature, signature_field) = sign.sign(&buffer)?;
121+
let commit_id = repo.commit_signed(
122+
commit,
123+
&signature,
124+
Some(&signature_field),
125+
)?;
126+
127+
// manually advance to the new commit ID
128+
// repo.commit does that on its own, repo.commit_signed does not
129+
// if there is no head, read default branch or defaul to "master"
130+
if let Ok(mut head) = repo.head() {
131+
head.set_target(commit_id, msg)?;
132+
} else {
133+
let default_branch_name = config
134+
.get_str("init.defaultBranch")
135+
.unwrap_or("master");
136+
repo.reference(
137+
&format!("refs/heads/{default_branch_name}"),
138+
commit_id,
139+
true,
140+
msg,
141+
)?;
142+
}
143+
144+
commit_id
145+
} else {
146+
repo.commit(
87147
Some("HEAD"),
88148
&signature,
89149
&signature,
90150
msg,
91151
&tree,
92152
parents.as_slice(),
93153
)?
94-
.into())
154+
};
155+
156+
Ok(commit_id.into())
95157
}
96158

97159
/// Tag a commit.

asyncgit/src/sync/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod remotes;
2525
mod repository;
2626
mod reset;
2727
mod reword;
28+
pub mod sign;
2829
mod staging;
2930
mod stash;
3031
mod state;

asyncgit/src/sync/reword.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use git2::{Oid, RebaseOptions, Repository};
33
use super::{
44
commit::signature_allow_undefined_name,
55
repo,
6-
utils::{bytes2string, get_head_refname},
6+
utils::{bytes2string, get_head_refname, get_head_repo},
77
CommitId, RepoPath,
88
};
99
use crate::error::{Error, Result};
@@ -15,6 +15,32 @@ pub fn reword(
1515
message: &str,
1616
) -> Result<CommitId> {
1717
let repo = repo(repo_path)?;
18+
let config = repo.config()?;
19+
20+
if config.get_bool("commit.gpgsign").unwrap_or(false) {
21+
// HACK: we undo the last commit and create a new one
22+
use crate::sync::utils::undo_last_commit;
23+
24+
let head = get_head_repo(&repo)?;
25+
if head == commit {
26+
// Check if there are any staged changes
27+
let parent = repo.find_commit(head.into())?;
28+
let tree = parent.tree()?;
29+
if repo
30+
.diff_tree_to_index(Some(&tree), None, None)?
31+
.deltas()
32+
.len() == 0
33+
{
34+
undo_last_commit(repo_path)?;
35+
return super::commit(repo_path, message);
36+
}
37+
38+
return Err(Error::SignRewordLastCommitStaged);
39+
}
40+
41+
return Err(Error::SignRewordNonLastCommit);
42+
}
43+
1844
let cur_branch_ref = get_head_refname(&repo)?;
1945

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

0 commit comments

Comments
 (0)