Skip to content

Commit 7e55067

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

File tree

12 files changed

+203
-24
lines changed

12 files changed

+203
-24
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;
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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use tracing::instrument;
2+
3+
use super::{repo::Result, Repo, RepoError};
4+
5+
/// Get commit signer configured from CLI arguments and repository configurations.
6+
#[instrument]
7+
pub fn get_signer(
8+
repo: &Repo,
9+
gpg_sign: &Option<String>,
10+
no_gpg_sign: bool,
11+
) -> Result<Option<Box<dyn git2_ext::ops::Sign>>> {
12+
if no_gpg_sign {
13+
return Ok(None);
14+
}
15+
let config = repo.inner.config().map_err(RepoError::ReadConfig)?;
16+
let sign_config = config.get_bool("commit.gpgsign").ok();
17+
let signer = match (sign_config, gpg_sign.as_deref()) {
18+
(Some(false) | None, None) => return Ok(None),
19+
(_, Some("") | None) => {
20+
let signer = git2_ext::ops::UserSign::from_config(&repo.inner, &config)
21+
.map_err(RepoError::ReadConfig)?;
22+
Box::new(signer) as Box<dyn git2_ext::ops::Sign>
23+
}
24+
(_, Some(keyid)) => {
25+
let format = config
26+
.get_string("gpg.format")
27+
.unwrap_or_else(|_| "openpgp".to_owned());
28+
match format.as_str() {
29+
"openpgp" => {
30+
let program = config
31+
.get_string("gpg.openpgp.program")
32+
.or_else(|_| config.get_string("gpg.program"))
33+
.unwrap_or_else(|_| "gpg".to_owned());
34+
35+
Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string()))
36+
as Box<dyn git2_ext::ops::Sign>
37+
}
38+
"x509" => {
39+
let program = config
40+
.get_string("gpg.x509.program")
41+
.unwrap_or_else(|_| "gpgsm".to_owned());
42+
43+
Box::new(git2_ext::ops::GpgSign::new(program, keyid.to_string()))
44+
as Box<dyn git2_ext::ops::Sign>
45+
}
46+
"ssh" => {
47+
let program = config
48+
.get_string("gpg.ssh.program")
49+
.unwrap_or_else(|_| "ssh-keygen".to_owned());
50+
51+
Box::new(git2_ext::ops::SshSign::new(program, keyid.to_string()))
52+
as Box<dyn git2_ext::ops::Sign>
53+
}
54+
format => {
55+
return Err(RepoError::ReadConfig(git2::Error::new(
56+
git2::ErrorCode::Invalid,
57+
git2::ErrorClass::Config,
58+
format!("invalid value for gpg.format: {}", format),
59+
)))
60+
}
61+
}
62+
}
63+
};
64+
Ok(Some(signer))
65+
}

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
}

git-branchless-opts/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ pub struct SwitchOptions {
178178
pub target: Option<String>,
179179
}
180180

181+
/// Options for signing commits
182+
#[derive(Args, Debug)]
183+
pub struct SignOptions {
184+
/// GPG-sign commits. The `keyid` argument is optional and defaults to the committer
185+
/// identity.
186+
#[clap(
187+
short = 'S',
188+
long = "gpg-sign",
189+
value_name = "keyid",
190+
conflicts_with = "no_gpg_sign",
191+
num_args(0..=1),
192+
default_missing_value(""),
193+
)]
194+
pub gpg_sign: Option<String>,
195+
196+
/// Countermand `commit.gpgSign` configuration variable.
197+
#[clap(long = "no-gpg-sign", conflicts_with = "gpg_sign")]
198+
pub no_gpg_sign: bool,
199+
}
200+
181201
/// Internal use.
182202
#[derive(Debug, Parser)]
183203
pub enum HookSubcommand {
@@ -420,6 +440,10 @@ pub enum Command {
420440
/// formatting or refactoring changes.
421441
#[clap(long)]
422442
reparent: bool,
443+
444+
/// Options for signing commits.
445+
#[clap(flatten)]
446+
sign_options: SignOptions,
423447
},
424448

425449
/// Gather information about recent operations to upload as part of a bug

git-branchless-test/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1969,12 +1969,12 @@ fn apply_fixes(
19691969
.try_collect()?;
19701970
let fixed_tree = repo.find_tree_or_fail(fixed_tree_oid)?;
19711971
let fixed_commit_oid = repo.create_commit(
1972-
None,
19731972
&original_commit.get_author(),
19741973
&original_commit.get_committer(),
19751974
commit_message,
19761975
&fixed_tree,
19771976
parents.iter().collect(),
1977+
None,
19781978
)?;
19791979
if original_commit_oid == fixed_commit_oid {
19801980
continue;

git-branchless/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ git-branchless-submit = { version = "0.7.0", path = "../git-branchless-submit" }
4040
git-branchless-test = { version = "0.7.0", path = "../git-branchless-test" }
4141
git-branchless-undo = { version = "0.7.0", path = "../git-branchless-undo" }
4242
git-record = { version = "0.3", path = "../git-record" }
43+
git2-ext = "0.6.0"
4344
itertools = "0.10.5"
4445
lazy_static = "1.4.0"
4546
lib = { package = "git-branchless-lib", version = "0.7.0", path = "../git-branchless-lib" }

0 commit comments

Comments
 (0)