Skip to content

Commit 9c338ae

Browse files
committed
feat(amend): support GPG-signing
1 parent 367205d commit 9c338ae

File tree

12 files changed

+237
-23
lines changed

12 files changed

+237
-23
lines changed

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

git-branchless-lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ eyre = "0.6.8"
5757
futures = "0.3.28"
5858
git-record = { version = "0.3", path = "../git-record" }
5959
git2 = { version = "0.17.2", default-features = false }
60+
git2-ext = "0.6.0"
6061
indicatif = { version = "0.17.5", features = ["improved_unicode"] }
6162
itertools = "0.10.3"
6263
lazy_static = "1.4.0"

git-branchless-lib/src/core/rewrite/execute.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,12 +628,12 @@ mod in_memory {
628628
};
629629
let rebased_commit_oid = repo
630630
.create_commit(
631-
None,
632631
&commit_to_apply.get_author(),
633632
&committer_signature,
634633
commit_message,
635634
&commit_tree,
636635
vec![&current_commit],
636+
None,
637637
)
638638
.wrap_err("Applying rebased commit")?;
639639

@@ -748,12 +748,12 @@ mod in_memory {
748748
};
749749
let rebased_commit_oid = repo
750750
.create_commit(
751-
None,
752751
&replacement_commit.get_author(),
753752
&committer_signature,
754753
replacement_commit_message,
755754
&replacement_tree,
756755
parents.iter().collect(),
756+
None,
757757
)
758758
.wrap_err("Applying rebased commit")?;
759759

git-branchless-lib/src/git/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod oid;
88
mod reference;
99
mod repo;
1010
mod run;
11+
mod sign;
1112
mod snapshot;
1213
mod status;
1314
mod test;
@@ -27,6 +28,7 @@ pub use repo::{
2728
Time,
2829
};
2930
pub use run::{GitRunInfo, GitRunOpts, GitRunResult};
31+
pub use sign::{get_signer, SignOption};
3032
pub use snapshot::{WorkingCopyChangesType, WorkingCopySnapshot};
3133
pub use status::{FileMode, FileStatus, StatusEntry};
3234
pub use test::{

git-branchless-lib/src/git/repo.rs

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use chrono::NaiveDateTime;
2424
use cursive::theme::BaseColor;
2525
use cursive::utils::markup::StyledString;
2626
use git2::DiffOptions;
27+
use git2_ext::ops::Sign;
2728
use itertools::Itertools;
2829
use thiserror::Error;
2930
use tracing::{instrument, warn};
@@ -1152,31 +1153,81 @@ impl Repo {
11521153
}
11531154

11541155
/// Create a new commit.
1155-
#[instrument]
1156+
#[instrument(skip(signer))]
11561157
pub fn create_commit(
11571158
&self,
1158-
update_ref: Option<&str>,
11591159
author: &Signature,
11601160
committer: &Signature,
11611161
message: &str,
11621162
tree: &Tree,
11631163
parents: Vec<&Commit>,
1164+
signer: Option<&dyn Sign>,
11641165
) -> Result<NonZeroOid> {
11651166
let parents = parents
11661167
.iter()
11671168
.map(|commit| &commit.inner)
11681169
.collect::<Vec<_>>();
1169-
let oid = self
1170-
.inner
1171-
.commit(
1172-
update_ref,
1173-
&author.inner,
1174-
&committer.inner,
1175-
message,
1176-
&tree.inner,
1177-
parents.as_slice(),
1178-
)
1179-
.map_err(Error::CreateCommit)?;
1170+
let oid = git2_ext::ops::commit(
1171+
&self.inner,
1172+
&author.inner,
1173+
&committer.inner,
1174+
message,
1175+
&tree.inner,
1176+
parents.as_slice(),
1177+
signer,
1178+
)
1179+
.map_err(Error::CreateCommit)?;
1180+
Ok(make_non_zero_oid(oid))
1181+
}
1182+
1183+
/// Amend a commit with all non-`None` values
1184+
#[instrument(skip(signer))]
1185+
pub fn amend_commit(
1186+
&self,
1187+
commit_to_amend: &Commit,
1188+
author: Option<&Signature>,
1189+
committer: Option<&Signature>,
1190+
message: Option<&str>,
1191+
tree: Option<&Tree>,
1192+
signer: Option<&dyn Sign>,
1193+
) -> Result<NonZeroOid> {
1194+
macro_rules! owning_unwrap_or {
1195+
($name:ident, $value_source:expr) => {
1196+
let owned_value;
1197+
let $name = if let Some(value) = $name {
1198+
value
1199+
} else {
1200+
owned_value = $value_source;
1201+
&owned_value
1202+
};
1203+
};
1204+
}
1205+
owning_unwrap_or!(author, commit_to_amend.get_author());
1206+
owning_unwrap_or!(committer, commit_to_amend.get_committer());
1207+
owning_unwrap_or!(
1208+
message,
1209+
commit_to_amend
1210+
.inner
1211+
.message_raw()
1212+
.ok_or(Error::DecodeUtf8 {
1213+
item: "raw message",
1214+
})?
1215+
);
1216+
owning_unwrap_or!(tree, commit_to_amend.get_tree()?);
1217+
1218+
let parents = commit_to_amend.get_parents();
1219+
let parents = parents.iter().map(|parent| &parent.inner).collect_vec();
1220+
1221+
let oid = git2_ext::ops::commit(
1222+
&self.inner,
1223+
&author.inner,
1224+
&committer.inner,
1225+
message,
1226+
&tree.inner,
1227+
parents.as_slice(),
1228+
signer,
1229+
)
1230+
.map_err(Error::Amend)?;
11801231
Ok(make_non_zero_oid(oid))
11811232
}
11821233

@@ -1365,12 +1416,12 @@ impl Repo {
13651416
vec![]
13661417
};
13671418
let dehydrated_commit_oid = self.create_commit(
1368-
None,
13691419
&signature,
13701420
&signature,
13711421
&message,
13721422
&dehydrated_tree,
13731423
parents.iter().collect_vec(),
1424+
None,
13741425
)?;
13751426
let dehydrated_commit = self.find_commit_or_fail(dehydrated_commit_oid)?;
13761427
Ok(dehydrated_commit)

git-branchless-lib/src/git/sign.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use tracing::instrument;
2+
3+
use super::{repo::Result, Repo, RepoError};
4+
5+
/// GPG-signing option.
6+
#[derive(Debug, PartialEq, Eq)]
7+
pub enum SignOption {
8+
/// Sign commits conditionally based on the `commit.gpgsign` configuration and
9+
/// and the key `user.signingkey`.
10+
UseConfig,
11+
/// Sign commits using the key from `user.signingkey` configuration.
12+
UseConfigKey,
13+
/// Sign commits using the provided signing key.
14+
KeyOverride(String),
15+
/// Do not sign commits.
16+
Disable,
17+
}
18+
19+
/// Get commit signer configured from CLI arguments and repository configurations.
20+
#[instrument]
21+
pub fn get_signer(
22+
repo: &Repo,
23+
option: &SignOption,
24+
) -> Result<Option<Box<dyn git2_ext::ops::Sign>>> {
25+
match option {
26+
SignOption::UseConfig | SignOption::UseConfigKey => {
27+
let config = repo.inner.config().map_err(RepoError::ReadConfig)?;
28+
if *option == SignOption::UseConfig {
29+
if config.get_bool("commit.gpgsign").ok() == Some(false) {
30+
return Ok(None);
31+
}
32+
}
33+
let signer = git2_ext::ops::UserSign::from_config(&repo.inner, &config)
34+
.map_err(RepoError::ReadConfig)?;
35+
Ok(Some(Box::new(signer) as Box<dyn git2_ext::ops::Sign>))
36+
}
37+
SignOption::KeyOverride(keyid) => {
38+
let config = repo.inner.config().map_err(RepoError::ReadConfig)?;
39+
let format = config
40+
.get_string("gpg.format")
41+
.unwrap_or_else(|_| "openpgp".to_owned());
42+
let signer = match format.as_str() {
43+
"openpgp" => {
44+
let program = config
45+
.get_string("gpg.openpgp.program")
46+
.or_else(|_| config.get_string("gpg.program"))
47+
.unwrap_or_else(|_| "gpg".to_owned());
48+
49+
Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string()))
50+
as Box<dyn git2_ext::ops::Sign>
51+
}
52+
"x509" => {
53+
let program = config
54+
.get_string("gpg.x509.program")
55+
.unwrap_or_else(|_| "gpgsm".to_owned());
56+
57+
Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string()))
58+
as Box<dyn git2_ext::ops::Sign>
59+
}
60+
"ssh" => {
61+
let program = config
62+
.get_string("gpg.ssh.program")
63+
.unwrap_or_else(|_| "ssh-keygen".to_owned());
64+
65+
Box::new(git2_ext::ops::SshSign::new(program, keyid.to_string()))
66+
as Box<dyn git2_ext::ops::Sign>
67+
}
68+
format => {
69+
return Err(RepoError::ReadConfig(git2::Error::new(
70+
git2::ErrorCode::Invalid,
71+
git2::ErrorClass::Config,
72+
format!("invalid value for gpg.format: {}", format),
73+
)))
74+
}
75+
};
76+
Ok(Some(signer))
77+
}
78+
SignOption::Disable => Ok(None),
79+
}
80+
}

git-branchless-lib/src/git/snapshot.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ branchless: automated working copy snapshot
213213
parents
214214
};
215215
let commit_oid =
216-
repo.create_commit(None, &signature, &signature, &message, &tree, parents)?;
216+
repo.create_commit(&signature, &signature, &message, &tree, parents, None)?;
217217

218218
Ok(WorkingCopySnapshot {
219219
base_commit: repo.find_commit_or_fail(commit_oid)?,
@@ -365,12 +365,12 @@ branchless: automated working copy snapshot
365365
}
366366
);
367367
let commit = repo.create_commit(
368-
None,
369368
&signature,
370369
&signature,
371370
&message,
372371
&tree_unstaged,
373372
Vec::from_iter(head_commit),
373+
None,
374374
)?;
375375
Ok(commit)
376376
}
@@ -452,7 +452,6 @@ branchless: automated working copy snapshot
452452
}
453453
);
454454
let commit_oid = repo.create_commit(
455-
None,
456455
&signature,
457456
&signature,
458457
&message,
@@ -461,6 +460,7 @@ branchless: automated working copy snapshot
461460
Some(parent_commit) => vec![parent_commit],
462461
None => vec![],
463462
},
463+
None,
464464
)?;
465465
Ok(commit_oid)
466466
}

0 commit comments

Comments
 (0)