From e9cd4293a719d524f640f845292d06408ec14297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 7 Oct 2025 19:17:31 +0200 Subject: [PATCH 1/3] Use constant instead of hardcoded value --- gix-object/src/commit/ref_iter.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gix-object/src/commit/ref_iter.rs b/gix-object/src/commit/ref_iter.rs index df953c1da1e..6a7eb2c778a 100644 --- a/gix-object/src/commit/ref_iter.rs +++ b/gix-object/src/commit/ref_iter.rs @@ -11,9 +11,8 @@ use winnow::{ use crate::{ bstr::ByteSlice, - commit::{decode, SignedData}, - parse, - parse::NL, + commit::{decode, SignedData, SIGNATURE_FIELD_NAME}, + parse::{self, NL}, CommitRefIter, }; @@ -65,7 +64,7 @@ impl<'a> CommitRefIter<'a> { for token in raw_tokens { let token = token?; if let Token::ExtraHeader((name, value)) = &token.token { - if *name == "gpgsig" { + if *name == SIGNATURE_FIELD_NAME { // keep track of the signature range alongside the signature data, // because all but the signature is the signed data. signature_and_range = Some((value.clone(), token.token_range)); From aacc326a198e4c78dd546018eebaff192ff5223d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 17 Oct 2025 08:43:41 +0200 Subject: [PATCH 2/3] feat: add a `gix commit sign` prototype --- gitoxide-core/src/repository/commit.rs | 65 +++++++++++++++++++++++++- src/plumbing/main.rs | 11 +++++ src/plumbing/options/mod.rs | 6 +++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/gitoxide-core/src/repository/commit.rs b/gitoxide-core/src/repository/commit.rs index e7b920a357b..fff4fc2a209 100644 --- a/gitoxide-core/src/repository/commit.rs +++ b/gitoxide-core/src/repository/commit.rs @@ -1,6 +1,14 @@ -use std::{io::Write, process::Stdio}; +use std::{ + borrow::Cow, + io::{Read, Write}, + process::Stdio, +}; use anyhow::{anyhow, bail, Context, Result}; +use gix::{ + bstr::{BStr, BString}, + objs::commit::SIGNATURE_FIELD_NAME, +}; /// Note that this is a quick implementation of commit signature verification that ignores a lot of what /// 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<()> { Ok(()) } +/// Note that this is a quick first prototype that lacks some of the features provided by `git +/// verify-commit`. +pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write) -> Result<()> { + let rev_spec = rev_spec.unwrap_or("HEAD"); + let object = repo + .rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())? + .object()?; + let mut commit_ref = object.to_commit_ref(); + + let mut cmd: std::process::Command = gix::command::prepare("gpg").into(); + cmd.args([ + "--keyid-format=long", + "--status-fd=2", + "--detach-sign", + "--sign", + "--armor", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + gix::trace::debug!("About to execute {cmd:?}"); + let mut child = cmd.spawn()?; + child.stdin.take().expect("to be present").write_all(&object.data)?; + + if !child.wait()?.success() { + bail!("Command {cmd:?} failed"); + } + + let mut signed_data = Vec::new(); + child + .stdout + .take() + .expect("to be present") + .read_to_end(&mut signed_data)?; + + let extra_header: Cow<'_, BStr> = Cow::Owned(BString::new(signed_data)); + + assert!( + !commit_ref + .extra_headers + .iter() + .any(|(header_name, _)| *header_name == BStr::new(SIGNATURE_FIELD_NAME)), + "Commit is already signed, doing nothing" + ); + + commit_ref + .extra_headers + .push((BStr::new(SIGNATURE_FIELD_NAME), extra_header)); + + let signed_id = repo.write_object(&commit_ref)?; + + writeln!(&mut out, "{signed_id}")?; + + Ok(()) +} + pub fn describe( mut repo: gix::Repository, rev_spec: Option<&str>, diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 1d0bb96343c..5bc1aacdf70 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -1287,6 +1287,17 @@ pub fn main() -> Result<()> { core::repository::commit::verify(repository(Mode::Lenient)?, rev_spec.as_deref()) }, ), + commit::Subcommands::Sign { rev_spec } => prepare_and_run( + "commit-sign", + trace, + auto_verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::commit::sign(repository(Mode::Lenient)?, rev_spec.as_deref(), out) + }, + ), commit::Subcommands::Describe { annotated_tags, all_refs, diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 597c7b7aaea..7432996d6ea 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -912,6 +912,12 @@ pub mod commit { /// A specification of the revision to verify, or the current `HEAD` if unset. rev_spec: Option, }, + /// Sign a commit and print the signed commit's id to stdout. This command does not change + /// symbolic refs. + Sign { + /// A specification of the revision to sign, or the current `HEAD` if unset. + rev_spec: Option, + }, /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. Describe { /// Use annotated tag references only, not all tags. From 82f788e17aa3a4ad4bf21cc4293ec4f621f227ce Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 17 Oct 2025 08:32:30 +0200 Subject: [PATCH 3/3] refactor --- gitoxide-core/src/repository/commit.rs | 28 +++++++++----------------- src/plumbing/options/mod.rs | 5 +++-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/gitoxide-core/src/repository/commit.rs b/gitoxide-core/src/repository/commit.rs index fff4fc2a209..357323adb19 100644 --- a/gitoxide-core/src/repository/commit.rs +++ b/gitoxide-core/src/repository/commit.rs @@ -47,14 +47,18 @@ pub fn verify(repo: gix::Repository, rev_spec: Option<&str>) -> Result<()> { Ok(()) } -/// Note that this is a quick first prototype that lacks some of the features provided by `git -/// verify-commit`. +/// Note that this is a quick first prototype that lacks some of the features provided by `git verify-commit`. pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write) -> Result<()> { let rev_spec = rev_spec.unwrap_or("HEAD"); let object = repo .rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())? .object()?; let mut commit_ref = object.to_commit_ref(); + if commit_ref.extra_headers().pgp_signature().is_some() { + gix::trace::info!("The commit {id} is already signed, did nothing", id = object.id); + writeln!(out, "{id}", id = object.id)?; + return Ok(()); + } let mut cmd: std::process::Command = gix::command::prepare("gpg").into(); cmd.args([ @@ -66,6 +70,7 @@ pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()); + gix::trace::debug!("About to execute {cmd:?}"); let mut child = cmd.spawn()?; child.stdin.take().expect("to be present").write_all(&object.data)?; @@ -75,28 +80,13 @@ pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io } let mut signed_data = Vec::new(); - child - .stdout - .take() - .expect("to be present") - .read_to_end(&mut signed_data)?; - - let extra_header: Cow<'_, BStr> = Cow::Owned(BString::new(signed_data)); - - assert!( - !commit_ref - .extra_headers - .iter() - .any(|(header_name, _)| *header_name == BStr::new(SIGNATURE_FIELD_NAME)), - "Commit is already signed, doing nothing" - ); + child.stdout.expect("to be present").read_to_end(&mut signed_data)?; commit_ref .extra_headers - .push((BStr::new(SIGNATURE_FIELD_NAME), extra_header)); + .push((BStr::new(SIGNATURE_FIELD_NAME), Cow::Owned(BString::new(signed_data)))); let signed_id = repo.write_object(&commit_ref)?; - writeln!(&mut out, "{signed_id}")?; Ok(()) diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 7432996d6ea..8908244c8f7 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -912,8 +912,9 @@ pub mod commit { /// A specification of the revision to verify, or the current `HEAD` if unset. rev_spec: Option, }, - /// Sign a commit and print the signed commit's id to stdout. This command does not change - /// symbolic refs. + /// Sign a commit and print the signed commit's id to stdout. + /// + /// This command does not change symbolic refs. Sign { /// A specification of the revision to sign, or the current `HEAD` if unset. rev_spec: Option,