git-ents.gitmain
⌘K
foforge
commit.rs109 lines · 3.5 KB · rusthistorycomment on this file
1//! A commit builder that signs the way git's SSH signing does.
2
3use gix_hash::ObjectId;
4use gix_object::{Commit, Kind, Write, WriteTo as _};
5
6use crate::keys::Keypair;
7
8/// The inputs for one fixture commit; see [`write_commit`].
9///
10/// Author and committer are fixed fixture identities — only the timestamp
11/// varies, which keeps fixture object ids deterministic and lets tests
12/// stage commits that *claim* earlier authorship (the gate must ignore
13/// such claims: admission is judged at acceptance time).
14///
15/// # Examples
16///
17/// ```
18/// use ents_testutil::{CommitSpec, ObjectStore, empty_tree, write_commit};
19///
20/// let objects = ObjectStore::default();
21/// let tree = empty_tree(&objects);
22/// let spec = CommitSpec {
23/// tree,
24/// parents: vec![],
25/// message: "Initial".into(),
26/// seconds: 1_000,
27/// };
28/// let oid = write_commit(&objects, &spec, None);
29/// assert!(objects.get(&oid).is_some());
30/// ```
31#[derive(Debug, Clone)]
32pub struct CommitSpec {
33 /// The tree this commit records.
34 pub tree: ObjectId,
35 /// Parent commits, in order; empty for a root commit.
36 pub parents: Vec<ObjectId>,
37 /// The full commit message, trailers included.
38 pub message: String,
39 /// Author and committer timestamp, in seconds since the Unix epoch.
40 pub seconds: i64,
41}
42
43fn actor(seconds: i64) -> gix::actor::Signature {
44 gix::actor::Signature {
45 name: "Fixture".into(),
46 email: "fixture@ents.test".into(),
47 time: gix::date::Time { seconds, offset: 0 },
48 }
49}
50
51/// Write the commit described by `spec` into `objects`, signing it with
52/// `key` when one is given.
53///
54/// Signing works exactly the way `git commit -S` with an SSH key does: the
55/// SSHSIG (namespace `git`) is computed over the commit object serialized
56/// *without* its `gpgsig` header, then stored as that header's value — so
57/// the signature replicates with the repository and verifies offline in
58/// every clone.
59///
60/// # Examples
61///
62/// ```
63/// use ents_testutil::{CommitSpec, Keypair, ObjectStore, empty_tree, write_commit};
64///
65/// let objects = ObjectStore::default();
66/// let key = Keypair::from_seed(1);
67/// let spec = CommitSpec {
68/// tree: empty_tree(&objects),
69/// parents: vec![],
70/// message: "Signed".into(),
71/// seconds: 1_000,
72/// };
73/// let oid = write_commit(&objects, &spec, Some(&key));
74///
75/// // The stored bytes carry the signature as repository data.
76/// let raw = objects.get(&oid).expect("stored");
77/// # let gix_object::Object::Commit(commit) = raw else { panic!("not a commit") };
78/// assert!(commit.extra_headers.iter().any(|(k, _)| k == "gpgsig"));
79/// ```
80pub fn write_commit(objects: &impl Write, spec: &CommitSpec, key: Option<&Keypair>) -> ObjectId {
81 let mut commit = Commit {
82 tree: spec.tree,
83 parents: spec.parents.clone().into(),
84 author: actor(spec.seconds),
85 committer: actor(spec.seconds),
86 encoding: None,
87 message: spec.message.clone().into(),
88 extra_headers: Vec::new(),
89 };
90
91 if let Some(key) = key {
92 let mut payload = Vec::new();
93 commit
94 .write_to(&mut payload)
95 .expect("serializing a commit to a Vec cannot fail");
96 let pem = key.sign(&payload);
97 commit
98 .extra_headers
99 .push(("gpgsig".into(), pem.trim_end().into()));
100 }
101
102 let mut raw = Vec::new();
103 commit
104 .write_to(&mut raw)
105 .expect("serializing a commit to a Vec cannot fail");
106 objects
107 .write_buf(Kind::Commit, &raw)
108 .expect("in-memory object write cannot fail")
109}