gitoxide_core/repository/
commit.rs1use 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
13pub 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
50pub 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}