sley_object/
commit_create.rs1use sley_core::{GitError, ObjectFormat, ObjectId, Result};
10
11use crate::{Commit, EncodedObject, ObjectType};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct CommitCreate {
15 pub tree: ObjectId,
16 pub parents: Vec<ObjectId>,
17 pub author: Vec<u8>,
18 pub committer: Vec<u8>,
19 pub message: Vec<u8>,
20 pub encoding: Option<Vec<u8>>,
22 pub signature: Option<Vec<u8>>,
23}
24
25pub fn encode_commit_object(commit: CommitCreate) -> Result<EncodedObject> {
30 let format = commit.tree.format();
31 for parent in &commit.parents {
32 if parent.format() != format {
33 return Err(GitError::InvalidObjectId(format!(
34 "parent {parent} uses {}, tree uses {}",
35 parent.format().name(),
36 format.name()
37 )));
38 }
39 }
40 let signature = commit.signature;
41 let commit = Commit {
42 tree: commit.tree,
43 parents: commit.parents,
44 author: commit.author,
45 committer: commit.committer,
46 encoding: commit.encoding,
47 message: commit.message,
48 };
49 let mut body = commit.write();
50 if let Some(signature) = signature {
51 body = commit_body_with_signature(format, &body, &signature);
52 }
53 Ok(EncodedObject::new(ObjectType::Commit, body))
54}
55
56fn commit_body_with_signature(format: ObjectFormat, body: &[u8], signature: &[u8]) -> Vec<u8> {
57 let Some(split) = body.windows(2).position(|window| window == b"\n\n") else {
58 return body.to_vec();
59 };
60 let mut out = Vec::with_capacity(body.len() + signature.len() + signature.len() / 70 + 16);
61 out.extend_from_slice(&body[..split]);
62 out.push(b'\n');
63 out.extend_from_slice(match format {
64 ObjectFormat::Sha1 => b"gpgsig ",
65 ObjectFormat::Sha256 => b"gpgsig-sha256 ",
66 });
67 append_folded_signature(&mut out, signature);
68 out.extend_from_slice(&body[split + 1..]);
69 out
70}
71
72fn append_folded_signature(out: &mut Vec<u8>, signature: &[u8]) {
73 let mut first = true;
74 let mut lines = signature.split(|byte| *byte == b'\n').peekable();
75 while let Some(line) = lines.next() {
76 if line.is_empty() && lines.peek().is_none() && signature.ends_with(b"\n") {
77 continue;
78 }
79 if !first {
80 out.push(b' ');
81 }
82 out.extend_from_slice(line);
83 out.push(b'\n');
84 first = false;
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::format_commit_identity;
92
93 fn sha1(hex: &str) -> ObjectId {
94 ObjectId::from_hex(ObjectFormat::Sha1, hex).expect("test operation should succeed")
95 }
96
97 fn sample(signature: Option<Vec<u8>>) -> CommitCreate {
98 let identity =
99 format_commit_identity("Example User", "example@example.invalid", "@0 +0000")
100 .expect("test operation should succeed");
101 CommitCreate {
102 tree: sha1("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
103 parents: Vec::new(),
104 author: identity.clone(),
105 committer: identity,
106 message: b"initial subject\n".to_vec(),
107 encoding: None,
108 signature,
109 }
110 }
111
112 #[test]
113 fn unsigned_encoding_matches_the_known_commit_bytes() {
114 let object = encode_commit_object(sample(None)).expect("test operation should succeed");
115 assert_eq!(object.object_type, ObjectType::Commit);
116 let text = String::from_utf8_lossy(&object.body);
117 assert_eq!(
118 text,
119 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
120 author Example User <example@example.invalid> 0 +0000\n\
121 committer Example User <example@example.invalid> 0 +0000\n\
122 \n\
123 initial subject\n"
124 );
125 }
126
127 #[test]
128 fn signatures_fold_into_continuation_header_lines() {
129 let signature = b"-----BEGIN PGP SIGNATURE-----\nfirst line\nsecond line\n-----END PGP SIGNATURE-----\n".to_vec();
130 let object =
131 encode_commit_object(sample(Some(signature))).expect("test operation should succeed");
132 let text = String::from_utf8_lossy(&object.body);
133 assert!(text.contains(
134 "gpgsig -----BEGIN PGP SIGNATURE-----\n \
135 first line\n \
136 second line\n \
137 -----END PGP SIGNATURE-----\n"
138 ));
139 }
140
141 #[test]
142 fn parent_format_mismatch_is_rejected_before_writing() {
143 let mut commit = sample(None);
144 commit.parents.push(ObjectId::null(ObjectFormat::Sha256));
145 let err = match encode_commit_object(commit) {
146 Err(err) => err,
147 Ok(_) => panic!("expected parent format mismatch to be rejected"),
148 };
149 assert!(err.to_string().contains("uses sha256, tree uses sha1"));
150 }
151}