Skip to content

Commit 8699d0f

Browse files
authored
Merge pull request #2208 from cruessler/add-commit-signing
Add `gix commit sign`
2 parents 12b3257 + 82f788e commit 8699d0f

File tree

4 files changed

+75
-5
lines changed

4 files changed

+75
-5
lines changed

gitoxide-core/src/repository/commit.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
use std::{io::Write, process::Stdio};
1+
use std::{
2+
borrow::Cow,
3+
io::{Read, Write},
4+
process::Stdio,
5+
};
26

37
use anyhow::{anyhow, bail, Context, Result};
8+
use gix::{
9+
bstr::{BStr, BString},
10+
objs::commit::SIGNATURE_FIELD_NAME,
11+
};
412

513
/// Note that this is a quick implementation of commit signature verification that ignores a lot of what
614
/// git does and can do, while focussing on the gist of it.
@@ -39,6 +47,51 @@ pub fn verify(repo: gix::Repository, rev_spec: Option<&str>) -> Result<()> {
3947
Ok(())
4048
}
4149

50+
/// Note that this is a quick first prototype that lacks some of the features provided by `git verify-commit`.
51+
pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write) -> Result<()> {
52+
let rev_spec = rev_spec.unwrap_or("HEAD");
53+
let object = repo
54+
.rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
55+
.object()?;
56+
let mut commit_ref = object.to_commit_ref();
57+
if commit_ref.extra_headers().pgp_signature().is_some() {
58+
gix::trace::info!("The commit {id} is already signed, did nothing", id = object.id);
59+
writeln!(out, "{id}", id = object.id)?;
60+
return Ok(());
61+
}
62+
63+
let mut cmd: std::process::Command = gix::command::prepare("gpg").into();
64+
cmd.args([
65+
"--keyid-format=long",
66+
"--status-fd=2",
67+
"--detach-sign",
68+
"--sign",
69+
"--armor",
70+
])
71+
.stdin(Stdio::piped())
72+
.stdout(Stdio::piped());
73+
74+
gix::trace::debug!("About to execute {cmd:?}");
75+
let mut child = cmd.spawn()?;
76+
child.stdin.take().expect("to be present").write_all(&object.data)?;
77+
78+
if !child.wait()?.success() {
79+
bail!("Command {cmd:?} failed");
80+
}
81+
82+
let mut signed_data = Vec::new();
83+
child.stdout.expect("to be present").read_to_end(&mut signed_data)?;
84+
85+
commit_ref
86+
.extra_headers
87+
.push((BStr::new(SIGNATURE_FIELD_NAME), Cow::Owned(BString::new(signed_data))));
88+
89+
let signed_id = repo.write_object(&commit_ref)?;
90+
writeln!(&mut out, "{signed_id}")?;
91+
92+
Ok(())
93+
}
94+
4295
pub fn describe(
4396
mut repo: gix::Repository,
4497
rev_spec: Option<&str>,

gix-object/src/commit/ref_iter.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ use winnow::{
1111

1212
use crate::{
1313
bstr::ByteSlice,
14-
commit::{decode, SignedData},
15-
parse,
16-
parse::NL,
14+
commit::{decode, SignedData, SIGNATURE_FIELD_NAME},
15+
parse::{self, NL},
1716
CommitRefIter,
1817
};
1918

@@ -65,7 +64,7 @@ impl<'a> CommitRefIter<'a> {
6564
for token in raw_tokens {
6665
let token = token?;
6766
if let Token::ExtraHeader((name, value)) = &token.token {
68-
if *name == "gpgsig" {
67+
if *name == SIGNATURE_FIELD_NAME {
6968
// keep track of the signature range alongside the signature data,
7069
// because all but the signature is the signed data.
7170
signature_and_range = Some((value.clone(), token.token_range));

src/plumbing/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,17 @@ pub fn main() -> Result<()> {
12871287
core::repository::commit::verify(repository(Mode::Lenient)?, rev_spec.as_deref())
12881288
},
12891289
),
1290+
commit::Subcommands::Sign { rev_spec } => prepare_and_run(
1291+
"commit-sign",
1292+
trace,
1293+
auto_verbose,
1294+
progress,
1295+
progress_keep_open,
1296+
None,
1297+
move |_progress, out, _err| {
1298+
core::repository::commit::sign(repository(Mode::Lenient)?, rev_spec.as_deref(), out)
1299+
},
1300+
),
12901301
commit::Subcommands::Describe {
12911302
annotated_tags,
12921303
all_refs,

src/plumbing/options/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,13 @@ pub mod commit {
912912
/// A specification of the revision to verify, or the current `HEAD` if unset.
913913
rev_spec: Option<String>,
914914
},
915+
/// Sign a commit and print the signed commit's id to stdout.
916+
///
917+
/// This command does not change symbolic refs.
918+
Sign {
919+
/// A specification of the revision to sign, or the current `HEAD` if unset.
920+
rev_spec: Option<String>,
921+
},
915922
/// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
916923
Describe {
917924
/// Use annotated tag references only, not all tags.

0 commit comments

Comments
 (0)