gitoxide_core/repository/
commit.rs

1use std::{
2    borrow::Cow,
3    io::{Read, Write},
4    process::Stdio,
5};
6
7use anyhow::{anyhow, bail, Context, Result};
8use gix::{
9    bstr::{BStr, BString},
10    objs::commit::SIGNATURE_FIELD_NAME,
11};
12
13/// Note that this is a quick implementation of commit signature verification that ignores a lot of what
14/// git does and can do, while focussing on the gist of it.
15/// For this to go into `gix`, one will have to implement many more options and various validation programs.
16pub fn verify(repo: gix::Repository, rev_spec: Option<&str>) -> Result<()> {
17    let rev_spec = rev_spec.unwrap_or("HEAD");
18    let commit = repo
19        .rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
20        .object()?
21        .into_commit();
22    let (signature, signed_data) = commit
23        .signature()
24        .context("Could not parse commit to obtain signature")?
25        .ok_or_else(|| anyhow!("Commit at {rev_spec} is not signed"))?;
26
27    let mut signature_storage = tempfile::NamedTempFile::new()?;
28    signature_storage.write_all(signature.as_ref())?;
29    let signed_storage = signature_storage.into_temp_path();
30
31    let mut cmd: std::process::Command = gix::command::prepare("gpg").into();
32    cmd.args(["--keyid-format=long", "--status-fd=1", "--verify"])
33        .arg(&signed_storage)
34        .arg("-")
35        .stdin(Stdio::piped());
36    gix::trace::debug!("About to execute {cmd:?}");
37    let mut child = cmd.spawn()?;
38    child
39        .stdin
40        .take()
41        .expect("configured")
42        .write_all(signed_data.to_bstring().as_ref())?;
43
44    if !child.wait()?.success() {
45        bail!("Command {cmd:?} failed");
46    }
47    Ok(())
48}
49
50/// Note that this is a quick first prototype that lacks some of the features provided by `git verify-commit`.
51pub 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
95pub fn describe(
96    mut repo: gix::Repository,
97    rev_spec: Option<&str>,
98    mut out: impl std::io::Write,
99    mut err: impl std::io::Write,
100    describe::Options {
101        all_tags,
102        all_refs,
103        first_parent,
104        always,
105        statistics,
106        max_candidates,
107        long_format,
108        dirty_suffix,
109    }: describe::Options,
110) -> Result<()> {
111    repo.object_cache_size_if_unset(4 * 1024 * 1024);
112    let commit = match rev_spec {
113        Some(spec) => repo.rev_parse_single(spec)?.object()?.try_into_commit()?,
114        None => repo.head_commit()?,
115    };
116    use gix::commit::describe::SelectRef::*;
117    let select_ref = if all_refs {
118        AllRefs
119    } else if all_tags {
120        AllTags
121    } else {
122        Default::default()
123    };
124    let resolution = commit
125        .describe()
126        .names(select_ref)
127        .traverse_first_parent(first_parent)
128        .id_as_fallback(always)
129        .max_candidates(max_candidates)
130        .try_resolve()?
131        .with_context(|| format!("Did not find a single candidate ref for naming id '{}'", commit.id))?;
132
133    if statistics {
134        writeln!(err, "traversed {} commits", resolution.outcome.commits_seen)?;
135    }
136
137    let mut describe_id = resolution.format_with_dirty_suffix(dirty_suffix)?;
138    describe_id.long(long_format);
139
140    writeln!(out, "{describe_id}")?;
141    Ok(())
142}
143
144pub mod describe {
145    #[derive(Debug, Clone)]
146    pub struct Options {
147        pub all_tags: bool,
148        pub all_refs: bool,
149        pub first_parent: bool,
150        pub always: bool,
151        pub long_format: bool,
152        pub statistics: bool,
153        pub max_candidates: usize,
154        pub dirty_suffix: Option<String>,
155    }
156}