Skip to content

Commit aacc326

Browse files
cruesslerByron
authored andcommitted
feat: add a gix commit sign prototype
1 parent e9cd429 commit aacc326

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

gitoxide-core/src/repository/commit.rs

Lines changed: 64 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,61 @@ 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
51+
/// verify-commit`.
52+
pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write) -> Result<()> {
53+
let rev_spec = rev_spec.unwrap_or("HEAD");
54+
let object = repo
55+
.rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
56+
.object()?;
57+
let mut commit_ref = object.to_commit_ref();
58+
59+
let mut cmd: std::process::Command = gix::command::prepare("gpg").into();
60+
cmd.args([
61+
"--keyid-format=long",
62+
"--status-fd=2",
63+
"--detach-sign",
64+
"--sign",
65+
"--armor",
66+
])
67+
.stdin(Stdio::piped())
68+
.stdout(Stdio::piped());
69+
gix::trace::debug!("About to execute {cmd:?}");
70+
let mut child = cmd.spawn()?;
71+
child.stdin.take().expect("to be present").write_all(&object.data)?;
72+
73+
if !child.wait()?.success() {
74+
bail!("Command {cmd:?} failed");
75+
}
76+
77+
let mut signed_data = Vec::new();
78+
child
79+
.stdout
80+
.take()
81+
.expect("to be present")
82+
.read_to_end(&mut signed_data)?;
83+
84+
let extra_header: Cow<'_, BStr> = Cow::Owned(BString::new(signed_data));
85+
86+
assert!(
87+
!commit_ref
88+
.extra_headers
89+
.iter()
90+
.any(|(header_name, _)| *header_name == BStr::new(SIGNATURE_FIELD_NAME)),
91+
"Commit is already signed, doing nothing"
92+
);
93+
94+
commit_ref
95+
.extra_headers
96+
.push((BStr::new(SIGNATURE_FIELD_NAME), extra_header));
97+
98+
let signed_id = repo.write_object(&commit_ref)?;
99+
100+
writeln!(&mut out, "{signed_id}")?;
101+
102+
Ok(())
103+
}
104+
42105
pub fn describe(
43106
mut repo: gix::Repository,
44107
rev_spec: Option<&str>,

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,12 @@ 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. This command does not change
916+
/// symbolic refs.
917+
Sign {
918+
/// A specification of the revision to sign, or the current `HEAD` if unset.
919+
rev_spec: Option<String>,
920+
},
915921
/// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
916922
Describe {
917923
/// Use annotated tag references only, not all tags.

0 commit comments

Comments
 (0)