gitoxide_core/repository/
commit.rs

1use std::{io::Write, process::Stdio};
2
3use anyhow::{anyhow, bail, Context, Result};
4
5/// Note that this is a quick implementation of commit signature verification that ignores a lot of what
6/// git does and can do, while focussing on the gist of it.
7/// For this to go into `gix`, one will have to implement many more options and various validation programs.
8pub fn verify(repo: gix::Repository, rev_spec: Option<&str>) -> Result<()> {
9    let rev_spec = rev_spec.unwrap_or("HEAD");
10    let commit = repo
11        .rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
12        .object()?
13        .into_commit();
14    let (signature, signed_data) = commit
15        .signature()
16        .context("Could not parse commit to obtain signature")?
17        .ok_or_else(|| anyhow!("Commit at {rev_spec} is not signed"))?;
18
19    let mut signature_storage = tempfile::NamedTempFile::new()?;
20    signature_storage.write_all(signature.as_ref())?;
21    let signed_storage = signature_storage.into_temp_path();
22
23    let mut cmd: std::process::Command = gix::command::prepare("gpg").into();
24    cmd.args(["--keyid-format=long", "--status-fd=1", "--verify"])
25        .arg(&signed_storage)
26        .arg("-")
27        .stdin(Stdio::piped());
28    gix::trace::debug!("About to execute {cmd:?}");
29    let mut child = cmd.spawn()?;
30    child
31        .stdin
32        .take()
33        .expect("configured")
34        .write_all(signed_data.to_bstring().as_ref())?;
35
36    if !child.wait()?.success() {
37        bail!("Command {cmd:?} failed");
38    }
39    Ok(())
40}
41
42pub fn describe(
43    mut repo: gix::Repository,
44    rev_spec: Option<&str>,
45    mut out: impl std::io::Write,
46    mut err: impl std::io::Write,
47    describe::Options {
48        all_tags,
49        all_refs,
50        first_parent,
51        always,
52        statistics,
53        max_candidates,
54        long_format,
55        dirty_suffix,
56    }: describe::Options,
57) -> Result<()> {
58    repo.object_cache_size_if_unset(4 * 1024 * 1024);
59    let commit = match rev_spec {
60        Some(spec) => repo.rev_parse_single(spec)?.object()?.try_into_commit()?,
61        None => repo.head_commit()?,
62    };
63    use gix::commit::describe::SelectRef::*;
64    let select_ref = if all_refs {
65        AllRefs
66    } else if all_tags {
67        AllTags
68    } else {
69        Default::default()
70    };
71    let resolution = commit
72        .describe()
73        .names(select_ref)
74        .traverse_first_parent(first_parent)
75        .id_as_fallback(always)
76        .max_candidates(max_candidates)
77        .try_resolve()?
78        .with_context(|| format!("Did not find a single candidate ref for naming id '{}'", commit.id))?;
79
80    if statistics {
81        writeln!(err, "traversed {} commits", resolution.outcome.commits_seen)?;
82    }
83
84    let mut describe_id = resolution.format_with_dirty_suffix(dirty_suffix)?;
85    describe_id.long(long_format);
86
87    writeln!(out, "{describe_id}")?;
88    Ok(())
89}
90
91pub mod describe {
92    #[derive(Debug, Clone)]
93    pub struct Options {
94        pub all_tags: bool,
95        pub all_refs: bool,
96        pub first_parent: bool,
97        pub always: bool,
98        pub long_format: bool,
99        pub statistics: bool,
100        pub max_candidates: usize,
101        pub dirty_suffix: Option<String>,
102    }
103}