153 lines · 5.6 KB · Rust
RawDownload
use std::{ffi::OsString, path::PathBuf, process::Stdio}; use crate::{    bstr::{BString, ByteSlice},    config::tree::{Gpg, Key, User, gpg},}; use gix_object::signature::sign::is_literal_ssh_key;pub use gix_object::signature::{Format, sign::Options}; /// Errors encountered when applying resolved signing options to a commit.#[derive(Debug, thiserror::Error)]#[expect(missing_docs)]pub enum Error {    #[error(transparent)]    Options(#[from] options::Error),    #[error(transparent)]    Decode(#[from] gix_object::decode::Error),    #[error(transparent)]    Sign(#[from] gix_object::signature::sign::Error),    #[error(transparent)]    WriteObject(#[from] crate::object::write::Error),    #[error(transparent)]    FindCommit(#[from] crate::object::find::existing::with_conversion::Error),} /// Errors encountered when resolving commit-signing options.pub mod options {    use std::ffi::OsString;     use crate::bstr::BString;     /// The error returned by [`crate::Repository::commit_signing_options()`].    #[derive(Debug, thiserror::Error)]    #[expect(missing_docs)]    pub enum Error {        #[error(transparent)]        ConfigBoolean(#[from] crate::config::boolean::Error),        #[error(transparent)]        ParseTime(#[from] crate::config::time::Error),        #[error("Unsupported value for gpg.format: {0:?}")]        UnsupportedFormat(BString),        #[error("Committer identity is not configured and user.signingKey is unset")]        MissingCommitter,        #[error("user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key")]        MissingSshSigningKey,        #[error("Could not interpolate a configured commit-signing path")]        ConfiguredPath(#[from] gix_config::path::interpolate::Error),        #[error("Could not execute gpg.ssh.defaultKeyCommand {program:?}")]        DefaultKeyCommand {            program: OsString,            #[source]            source: std::io::Error,        },        #[error("gpg.ssh.defaultKeyCommand failed: {0:?}")]        DefaultKeyCommandFailed(BString),        #[error("gpg.ssh.defaultKeyCommand returned an invalid key: {0:?}")]        InvalidDefaultKey(BString),    }} pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result<crate::Commit<'repo>, Error> {    let options = commit.repo.commit_signing_options()?;    let signed = commit.decode()?.sign(options)?;    let id = commit.repo.write_object(&signed)?;    Ok(commit.repo.find_commit(id)?)} pub(crate) fn signing_options(repo: &crate::Repository) -> Result<Options, options::Error> {    use options::Error;     let config = repo.config_snapshot();    let format = config        .string(Gpg::FORMAT)        .unwrap_or_else(|| Gpg::FORMAT.default_value_or_panic().into());    let format = parse_format(format.trim()).ok_or(Error::UnsupportedFormat(format))?;    let program = super::signature_program(&config, format)?;    let signing_key = match config        .plumbing()        .string_filter(User::SIGNING_KEY, &mut repo.filter_config_section())    {        Some(key) if !key.is_empty() && format == Format::Ssh && is_literal_ssh_key(&key).is_none() => config            .trusted_path(User::SIGNING_KEY)?            .map_or_else(|| gix_path::from_bstring(key).into_os_string(), PathBuf::into_os_string),        Some(key) if !key.is_empty() => gix_path::from_bstring(key).into_os_string(),        _ if format == Format::Ssh => default_ssh_key(&config)?.ok_or(Error::MissingSshSigningKey)?,        _ => {            let committer = repo.committer().ok_or(Error::MissingCommitter)??;            let mut identity = committer.name.to_owned();            identity.extend_from_slice(b" <");            identity.extend_from_slice(committer.email);            identity.extend_from_slice(b">");            gix_path::from_bstring(identity).into_os_string()        }    };    Ok(Options {        format,        program,        program_arguments: Vec::new(),        signing_key,        environment: Vec::new(),    })} pub(crate) fn signing_options_if_enabled(repo: &crate::Repository) -> Result<Option<Options>, options::Error> {    repo.config        .may_sign_commits()?        .then(|| signing_options(repo))        .transpose()} fn parse_format(value: &[u8]) -> Option<Format> {    if value.eq_ignore_ascii_case(b"openpgp") {        Some(Format::OpenPgp)    } else if value.eq_ignore_ascii_case(b"x509") {        Some(Format::X509)    } else if value.eq_ignore_ascii_case(b"ssh") {        Some(Format::Ssh)    } else {        None    }} fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result<Option<OsString>, options::Error> {    use options::Error;     let Some(program) = config.trusted_program(gpg::Ssh::DEFAULT_KEY_COMMAND) else {        return Ok(None);    };    let output = gix_command::prepare(&program)        .command_may_be_shell_script()        .stdin(Stdio::null())        .stdout(Stdio::piped())        .stderr(Stdio::piped())        .spawn()        .map_err(|source| Error::DefaultKeyCommand {            program: program.clone(),            source,        })?        .wait_with_output()        .map_err(|source| Error::DefaultKeyCommand {            program: program.clone(),            source,        })?;    if !output.status.success() {        return Err(Error::DefaultKeyCommandFailed(output.stderr.into()));    }    let key = output.stdout.as_bstr().lines().next().unwrap_or_default().trim();    if is_literal_ssh_key(key).is_none() {        return Err(Error::InvalidDefaultKey(BString::from(key)));    }    Ok(Some(gix_path::from_bstr(key.as_bstr()).into_owned().into_os_string()))}