1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2
3use sley_core::{GitError, ObjectFormat, ObjectId, Result, Signature};
19use std::str::FromStr;
20
21pub use sley_core::BString;
22
23mod commit_create;
24mod identity;
25
26pub use commit_create::*;
27pub use identity::*;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum ObjectType {
31 Blob,
32 Tree,
33 Commit,
34 Tag,
35}
36
37impl ObjectType {
38 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::Blob => "blob",
41 Self::Tree => "tree",
42 Self::Commit => "commit",
43 Self::Tag => "tag",
44 }
45 }
46}
47
48impl FromStr for ObjectType {
49 type Err = GitError;
50
51 fn from_str(value: &str) -> Result<Self> {
52 match value {
53 "blob" => Ok(Self::Blob),
54 "tree" => Ok(Self::Tree),
55 "commit" => Ok(Self::Commit),
56 "tag" => Ok(Self::Tag),
57 other => Err(GitError::InvalidObject(format!(
58 "unknown object type {other}"
59 ))),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct EncodedObject {
66 pub object_type: ObjectType,
67 pub body: Vec<u8>,
68}
69
70impl EncodedObject {
71 pub fn new(object_type: ObjectType, body: impl Into<Vec<u8>>) -> Self {
78 Self {
79 object_type,
80 body: body.into(),
81 }
82 }
83
84 pub fn framed_bytes(&self) -> Vec<u8> {
86 let mut out = Vec::with_capacity(self.body.len() + 32);
87 out.extend_from_slice(self.object_type.as_str().as_bytes());
88 out.push(b' ');
89 out.extend_from_slice(self.body.len().to_string().as_bytes());
90 out.push(0);
91 out.extend_from_slice(&self.body);
92 out
93 }
94
95 pub fn object_id(&self, format: ObjectFormat) -> Result<ObjectId> {
97 sley_core::object_id_for_bytes(format, self.object_type.as_str(), &self.body)
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct Tree {
103 pub entries: Vec<TreeEntry>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct TreeEntry {
108 pub mode: u32,
109 pub name: BString,
110 pub oid: ObjectId,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TreeEntryRef<'a> {
120 pub mode: u32,
121 pub name: &'a [u8],
122 pub oid: ObjectId,
123}
124
125#[derive(Debug, Clone)]
127pub struct TreeEntries<'a> {
128 format: ObjectFormat,
129 bytes: &'a [u8],
130 offset: usize,
131}
132
133impl<'a> TreeEntries<'a> {
134 pub const fn new(format: ObjectFormat, bytes: &'a [u8]) -> Self {
135 Self {
136 format,
137 bytes,
138 offset: 0,
139 }
140 }
141}
142
143impl<'a> Iterator for TreeEntries<'a> {
144 type Item = Result<TreeEntryRef<'a>>;
145
146 fn next(&mut self) -> Option<Self::Item> {
147 if self.offset >= self.bytes.len() {
148 return None;
149 }
150 match parse_tree_entry_ref(self.format, self.bytes, self.offset) {
151 Ok((entry, next_offset)) => {
152 self.offset = next_offset;
153 Some(Ok(entry))
154 }
155 Err(err) => {
156 self.offset = self.bytes.len();
157 Some(Err(err))
158 }
159 }
160 }
161}
162
163impl<'a> From<TreeEntryRef<'a>> for TreeEntry {
164 fn from(entry: TreeEntryRef<'a>) -> Self {
165 Self {
166 mode: entry.mode,
167 name: entry.name.into(),
168 oid: entry.oid,
169 }
170 }
171}
172
173impl Tree {
174 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
175 let entries = TreeEntries::new(format, bytes)
176 .map(|entry| entry.map(TreeEntry::from))
177 .collect::<Result<Vec<_>>>()?;
178 Ok(Self { entries })
179 }
180
181 pub fn write(&self) -> Vec<u8> {
182 let mut out = Vec::new();
183 for entry in &self.entries {
184 out.extend_from_slice(format!("{:o}", entry.mode).as_bytes());
185 out.push(b' ');
186 out.extend_from_slice(entry.name.as_bytes());
187 out.push(0);
188 out.extend_from_slice(entry.oid.as_bytes());
189 }
190 out
191 }
192}
193
194fn parse_tree_entry_ref<'a>(
195 format: ObjectFormat,
196 bytes: &'a [u8],
197 offset: usize,
198) -> Result<(TreeEntryRef<'a>, usize)> {
199 let mode_end = bytes[offset..]
200 .iter()
201 .position(|byte| *byte == b' ')
202 .map(|relative| offset + relative)
203 .ok_or_else(|| GitError::InvalidFormat("unterminated tree mode".into()))?;
204 let mode_text = std::str::from_utf8(&bytes[offset..mode_end])
205 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
206 let mode = u32::from_str_radix(mode_text, 8)
207 .map_err(|_| GitError::InvalidFormat("invalid tree mode".into()))?;
208
209 let name_start = mode_end + 1;
210 let name_end = bytes[name_start..]
211 .iter()
212 .position(|byte| *byte == 0)
213 .map(|relative| name_start + relative)
214 .ok_or_else(|| GitError::InvalidFormat("unterminated tree path".into()))?;
215 if name_end == name_start {
216 return Err(GitError::InvalidFormat("empty tree path".into()));
217 }
218
219 let oid_start = name_end + 1;
220 let oid_end = oid_start
221 .checked_add(format.raw_len())
222 .ok_or_else(|| GitError::InvalidFormat("tree oid overflow".into()))?;
223 if oid_end > bytes.len() {
224 return Err(GitError::InvalidFormat("truncated tree object id".into()));
225 }
226
227 Ok((
228 TreeEntryRef {
229 mode,
230 name: &bytes[name_start..name_end],
231 oid: ObjectId::from_raw(format, &bytes[oid_start..oid_end])?,
232 },
233 oid_end,
234 ))
235}
236
237pub fn tree_entry_object_type(mode: u32) -> ObjectType {
238 match mode {
239 0o040000 => ObjectType::Tree,
240 0o160000 => ObjectType::Commit,
241 _ => ObjectType::Blob,
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252pub enum EntryKind {
253 Tree,
255 Blob,
257 BlobExecutable,
259 Symlink,
262 Commit,
264}
265
266impl EntryKind {
267 pub const fn mode(self) -> u32 {
269 match self {
270 Self::Tree => 0o040000,
271 Self::Blob => 0o100644,
272 Self::BlobExecutable => 0o100755,
273 Self::Symlink => 0o120000,
274 Self::Commit => 0o160000,
275 }
276 }
277
278 pub const fn from_mode(mode: u32) -> Option<Self> {
281 match mode {
282 0o040000 => Some(Self::Tree),
283 0o100644 => Some(Self::Blob),
284 0o100755 => Some(Self::BlobExecutable),
285 0o120000 => Some(Self::Symlink),
286 0o160000 => Some(Self::Commit),
287 _ => None,
288 }
289 }
290
291 pub const fn object_type(self) -> ObjectType {
294 match self {
295 Self::Tree => ObjectType::Tree,
296 Self::Commit => ObjectType::Commit,
297 _ => ObjectType::Blob,
298 }
299 }
300}
301
302impl From<EntryKind> for u32 {
303 fn from(kind: EntryKind) -> Self {
304 kind.mode()
305 }
306}
307
308impl TreeEntry {
309 pub fn kind(&self) -> Option<EntryKind> {
311 EntryKind::from_mode(self.mode)
312 }
313
314 pub fn is_tree(&self) -> bool {
315 self.mode == EntryKind::Tree.mode()
316 }
317
318 pub fn is_symlink(&self) -> bool {
319 self.mode == EntryKind::Symlink.mode()
320 }
321
322 pub fn is_gitlink(&self) -> bool {
323 self.mode == EntryKind::Commit.mode()
324 }
325
326 pub fn is_executable(&self) -> bool {
327 self.mode == EntryKind::BlobExecutable.mode()
328 }
329}
330
331impl TreeEntryRef<'_> {
332 pub fn kind(&self) -> Option<EntryKind> {
334 EntryKind::from_mode(self.mode)
335 }
336
337 pub fn is_tree(&self) -> bool {
338 self.mode == EntryKind::Tree.mode()
339 }
340
341 pub fn is_symlink(&self) -> bool {
342 self.mode == EntryKind::Symlink.mode()
343 }
344
345 pub fn is_gitlink(&self) -> bool {
346 self.mode == EntryKind::Commit.mode()
347 }
348
349 pub fn is_executable(&self) -> bool {
350 self.mode == EntryKind::BlobExecutable.mode()
351 }
352
353 pub fn to_owned(&self) -> TreeEntry {
354 TreeEntry {
355 mode: self.mode,
356 name: self.name.into(),
357 oid: self.oid,
358 }
359 }
360}
361
362pub fn tree_entry_cmp(
366 left_name: &[u8],
367 left_mode: u32,
368 right_name: &[u8],
369 right_mode: u32,
370) -> std::cmp::Ordering {
371 use std::cmp::Ordering;
372 let shared = left_name.len().min(right_name.len());
373 let name_order = left_name[..shared].cmp(&right_name[..shared]);
374 if name_order != Ordering::Equal {
375 return name_order;
376 }
377 let left_end = left_name.len() == shared;
378 let right_end = right_name.len() == shared;
379 match (left_end, right_end) {
380 (true, true) => Ordering::Equal,
381 (true, false) => tree_name_terminator(left_mode).cmp(&right_name[shared]),
382 (false, true) => left_name[shared].cmp(&tree_name_terminator(right_mode)),
383 (false, false) => Ordering::Equal,
384 }
385}
386
387fn tree_name_terminator(mode: u32) -> u8 {
388 if mode == 0o040000 { b'/' } else { 0 }
389}
390
391#[derive(Debug, Clone, Default)]
398pub struct TreeBuilder {
399 entries: Vec<TreeEntry>,
400}
401
402impl TreeBuilder {
403 pub fn new() -> Self {
404 Self {
405 entries: Vec::new(),
406 }
407 }
408
409 pub fn from_tree(tree: Tree) -> Self {
411 Self {
412 entries: tree.entries,
413 }
414 }
415
416 pub fn upsert(&mut self, name: impl Into<BString>, kind: EntryKind, oid: ObjectId) {
419 self.upsert_raw(name, kind.mode(), oid);
420 }
421
422 pub fn upsert_raw(&mut self, name: impl Into<BString>, mode: u32, oid: ObjectId) {
425 let name = name.into();
426 if let Some(entry) = self
427 .entries
428 .iter_mut()
429 .find(|entry| entry.name == name.as_bytes())
430 {
431 entry.mode = mode;
432 entry.oid = oid;
433 } else {
434 self.entries.push(TreeEntry { mode, name, oid });
435 }
436 }
437
438 pub fn remove(&mut self, name: &[u8]) -> bool {
440 if let Some(position) = self.entries.iter().position(|entry| entry.name == name) {
441 self.entries.swap_remove(position);
442 true
443 } else {
444 false
445 }
446 }
447
448 pub fn is_empty(&self) -> bool {
449 self.entries.is_empty()
450 }
451
452 pub fn len(&self) -> usize {
453 self.entries.len()
454 }
455
456 pub fn build(self) -> Tree {
458 let mut entries = self.entries;
459 entries.sort_by(|left, right| {
460 tree_entry_cmp(
461 left.name.as_bytes(),
462 left.mode,
463 right.name.as_bytes(),
464 right.mode,
465 )
466 });
467 Tree { entries }
468 }
469
470 pub fn write(self) -> Vec<u8> {
472 self.build().write()
473 }
474
475 pub fn object_id(self, format: ObjectFormat) -> Result<ObjectId> {
477 EncodedObject::new(ObjectType::Tree, self.write()).object_id(format)
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct Commit {
491 pub tree: ObjectId,
492 pub parents: Vec<ObjectId>,
493 pub author: Vec<u8>,
494 pub committer: Vec<u8>,
495 pub encoding: Option<Vec<u8>>,
496 pub message: Vec<u8>,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
507pub struct CommitRef<'a> {
508 pub tree: ObjectId,
509 pub parents: Vec<ObjectId>,
510 pub author: &'a [u8],
511 pub committer: &'a [u8],
512 pub encoding: Option<&'a [u8]>,
513 pub message: &'a [u8],
514}
515
516impl Commit {
517 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
522 Ok(Self::parse_ref(format, bytes)?.into())
523 }
524
525 pub fn parse_ref<'a>(format: ObjectFormat, bytes: &'a [u8]) -> Result<CommitRef<'a>> {
526 CommitRef::parse(format, bytes)
527 }
528
529 pub fn write(&self) -> Vec<u8> {
535 let mut out = Vec::new();
536 out.extend_from_slice(format!("tree {}\n", self.tree).as_bytes());
537 for parent in &self.parents {
538 out.extend_from_slice(format!("parent {parent}\n").as_bytes());
539 }
540 out.extend_from_slice(b"author ");
541 out.extend_from_slice(&self.author);
542 out.push(b'\n');
543 out.extend_from_slice(b"committer ");
544 out.extend_from_slice(&self.committer);
545 if let Some(encoding) = &self.encoding {
546 out.extend_from_slice(b"\nencoding ");
547 out.extend_from_slice(encoding);
548 }
549 out.extend_from_slice(b"\n\n");
550 out.extend_from_slice(&self.message);
551 out
552 }
553
554 pub fn author_signature(&self) -> Option<Signature> {
563 Signature::from_ident_line(&self.author)
564 }
565
566 pub fn committer_signature(&self) -> Option<Signature> {
571 Signature::from_ident_line(&self.committer)
572 }
573}
574
575impl<'a> CommitRef<'a> {
576 pub fn parse(format: ObjectFormat, bytes: &'a [u8]) -> Result<Self> {
577 let split = bytes
578 .windows(2)
579 .position(|window| window == b"\n\n")
580 .ok_or_else(|| GitError::InvalidObject("commit missing message separator".into()))?;
581 let mut tree = None;
582 let mut parents = Vec::new();
583 let mut author = None;
584 let mut committer = None;
585 let mut encoding = None;
586 for line in bytes[..split].split(|byte| *byte == b'\n') {
587 if let Some(value) = line.strip_prefix(b"tree ") {
588 tree = Some(ObjectId::from_hex(format, ascii_header_value(value)?)?);
589 } else if let Some(value) = line.strip_prefix(b"parent ") {
590 parents.push(ObjectId::from_hex(format, ascii_header_value(value)?)?);
591 } else if let Some(value) = line.strip_prefix(b"author ") {
592 author = Some(value);
593 } else if let Some(value) = line.strip_prefix(b"committer ") {
594 committer = Some(value);
595 } else if let Some(value) = line.strip_prefix(b"encoding ") {
596 encoding = Some(value);
597 }
598 }
599 Ok(Self {
600 tree: tree.ok_or_else(|| GitError::InvalidObject("commit missing tree".into()))?,
601 parents,
602 author: author
603 .ok_or_else(|| GitError::InvalidObject("commit missing author".into()))?,
604 committer: committer
605 .ok_or_else(|| GitError::InvalidObject("commit missing committer".into()))?,
606 encoding,
607 message: &bytes[split + 2..],
608 })
609 }
610
611 pub fn to_owned(&self) -> Commit {
612 Commit {
613 tree: self.tree,
614 parents: self.parents.clone(),
615 author: self.author.to_vec(),
616 committer: self.committer.to_vec(),
617 encoding: self.encoding.map(<[u8]>::to_vec),
618 message: self.message.to_vec(),
619 }
620 }
621
622 pub fn author_signature(&self) -> Option<Signature> {
631 Signature::from_ident_line(self.author)
632 }
633
634 pub fn committer_signature(&self) -> Option<Signature> {
639 Signature::from_ident_line(self.committer)
640 }
641}
642
643impl<'a> From<CommitRef<'a>> for Commit {
644 fn from(commit: CommitRef<'a>) -> Self {
645 Self {
646 tree: commit.tree,
647 parents: commit.parents,
648 author: commit.author.to_vec(),
649 committer: commit.committer.to_vec(),
650 encoding: commit.encoding.map(<[u8]>::to_vec),
651 message: commit.message.to_vec(),
652 }
653 }
654}
655
656#[derive(Debug, Clone, Eq)]
663pub struct Tag {
664 pub object: ObjectId,
665 pub object_type: ObjectType,
666 pub name: Vec<u8>,
667 pub tagger: Option<Vec<u8>>,
668 pub message: Vec<u8>,
669 pub raw_body: Option<Vec<u8>>,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
680pub struct TagRef<'a> {
681 pub object: ObjectId,
682 pub object_type: ObjectType,
683 pub name: &'a [u8],
684 pub tagger: Option<&'a [u8]>,
685 pub message: &'a [u8],
686 pub raw_body: Option<&'a [u8]>,
687}
688
689impl PartialEq for Tag {
690 fn eq(&self, other: &Self) -> bool {
691 self.object == other.object
692 && self.object_type == other.object_type
693 && self.name == other.name
694 && self.tagger == other.tagger
695 && self.message == other.message
696 }
697}
698
699impl Tag {
700 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
705 Ok(Self::parse_ref(format, bytes)?.into())
706 }
707
708 pub fn parse_ref<'a>(format: ObjectFormat, bytes: &'a [u8]) -> Result<TagRef<'a>> {
709 TagRef::parse(format, bytes)
710 }
711
712 pub fn write(&self) -> Vec<u8> {
718 if let Some(raw) = &self.raw_body {
719 return raw.clone();
720 }
721 let mut out = Vec::new();
722 out.extend_from_slice(format!("object {}\n", self.object).as_bytes());
723 out.extend_from_slice(format!("type {}\n", self.object_type.as_str()).as_bytes());
724 out.extend_from_slice(b"tag ");
725 out.extend_from_slice(&self.name);
726 out.push(b'\n');
727 if let Some(tagger) = &self.tagger {
728 out.extend_from_slice(b"tagger ");
729 out.extend_from_slice(tagger);
730 out.push(b'\n');
731 }
732 out.push(b'\n');
733 out.extend_from_slice(&self.message);
734 out
735 }
736
737 pub fn tagger_signature(&self) -> Option<Signature> {
747 Signature::from_ident_line(self.tagger.as_deref()?)
748 }
749}
750
751impl<'a> TagRef<'a> {
752 pub fn parse(format: ObjectFormat, bytes: &'a [u8]) -> Result<Self> {
753 let split = bytes.windows(2).position(|window| window == b"\n\n");
754 let (headers, message) = match split {
755 Some(split) => (&bytes[..split], &bytes[split + 2..]),
756 None => (bytes, &bytes[bytes.len()..]),
757 };
758 let mut object = None;
759 let mut object_type = None;
760 let mut name = None;
761 let mut tagger = None;
762 for line in headers.split(|byte| *byte == b'\n') {
763 if let Some(value) = line.strip_prefix(b"object ") {
764 object = Some(ObjectId::from_hex(format, ascii_header_value(value)?)?);
765 } else if let Some(value) = line.strip_prefix(b"type ") {
766 object_type = Some(ascii_header_value(value)?.parse()?);
767 } else if let Some(value) = line.strip_prefix(b"tag ") {
768 name = Some(value);
769 } else if let Some(value) = line.strip_prefix(b"tagger ") {
770 tagger = Some(value);
771 }
772 }
773 Ok(Self {
774 object: object.ok_or_else(|| GitError::InvalidObject("tag missing object".into()))?,
775 object_type: object_type
776 .ok_or_else(|| GitError::InvalidObject("tag missing type".into()))?,
777 name: name.ok_or_else(|| GitError::InvalidObject("tag missing name".into()))?,
778 tagger,
779 message,
780 raw_body: Some(bytes),
781 })
782 }
783
784 pub fn to_owned(&self) -> Tag {
785 Tag {
786 object: self.object,
787 object_type: self.object_type,
788 name: self.name.to_vec(),
789 tagger: self.tagger.map(<[u8]>::to_vec),
790 message: self.message.to_vec(),
791 raw_body: self.raw_body.map(<[u8]>::to_vec),
792 }
793 }
794
795 pub fn tagger_signature(&self) -> Option<Signature> {
805 Signature::from_ident_line(self.tagger?)
806 }
807}
808
809impl<'a> From<TagRef<'a>> for Tag {
810 fn from(tag: TagRef<'a>) -> Self {
811 Self {
812 object: tag.object,
813 object_type: tag.object_type,
814 name: tag.name.to_vec(),
815 tagger: tag.tagger.map(<[u8]>::to_vec),
816 message: tag.message.to_vec(),
817 raw_body: tag.raw_body.map(<[u8]>::to_vec),
818 }
819 }
820}
821
822fn ascii_header_value(value: &[u8]) -> Result<&str> {
823 std::str::from_utf8(value).map_err(|err| GitError::InvalidObject(err.to_string()))
824}
825
826pub fn parse_framed_object(bytes: &[u8]) -> Result<EncodedObject> {
827 let nul = bytes
828 .iter()
829 .position(|byte| *byte == 0)
830 .ok_or_else(|| GitError::InvalidObject("missing object header terminator".into()))?;
831 let header = std::str::from_utf8(&bytes[..nul])
832 .map_err(|err| GitError::InvalidObject(err.to_string()))?;
833 let (kind, size) = header
834 .split_once(' ')
835 .ok_or_else(|| GitError::InvalidObject("missing object size".into()))?;
836 let size: usize = size
837 .parse()
838 .map_err(|_| GitError::InvalidObject("invalid object size".into()))?;
839 let body = &bytes[nul + 1..];
840 if body.len() != size {
841 return Err(GitError::InvalidObject(format!(
842 "object declared {size} bytes, found {}",
843 body.len()
844 )));
845 }
846 Ok(EncodedObject::new(kind.parse()?, body.to_vec()))
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn tree_builder_sorts_canonically_and_dedups() {
855 let format = ObjectFormat::Sha1;
856 let blob = ObjectId::empty_blob(format);
857 let subtree = ObjectId::empty_tree(format);
858 assert_eq!(subtree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
860 assert_eq!(blob.to_hex(), "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
861
862 let mut builder = TreeBuilder::new();
863 builder.upsert("foo", EntryKind::Tree, subtree);
867 builder.upsert("a.txt", EntryKind::Blob, blob.clone());
868 builder.upsert("foo.txt", EntryKind::Blob, blob.clone());
869 builder.upsert("a.txt", EntryKind::BlobExecutable, blob);
871
872 let tree = builder.build();
873 let names: Vec<&[u8]> = tree.entries.iter().map(|e| e.name.as_bytes()).collect();
874 assert_eq!(names, vec![&b"a.txt"[..], &b"foo.txt"[..], &b"foo"[..]]);
875 assert_eq!(tree.entries[0].mode, EntryKind::BlobExecutable.mode());
876 assert!(tree.entries[2].is_tree());
877 }
878
879 #[test]
880 fn entry_kind_round_trips_modes() {
881 for kind in [
882 EntryKind::Tree,
883 EntryKind::Blob,
884 EntryKind::BlobExecutable,
885 EntryKind::Symlink,
886 EntryKind::Commit,
887 ] {
888 assert_eq!(EntryKind::from_mode(kind.mode()), Some(kind));
889 }
890 assert_eq!(EntryKind::from_mode(0o100600), None);
891 }
892
893 #[test]
894 fn framed_object_round_trips() {
895 let object = EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec());
896 assert_eq!(
897 parse_framed_object(&object.framed_bytes()).expect("test operation should succeed"),
898 object
899 );
900 }
901
902 #[test]
903 fn encoded_raw_commit_with_multiline_gpgsig_preserves_bytes_and_id() {
904 let format = ObjectFormat::Sha1;
905 let tree = ObjectId::empty_tree(format);
906 let body = format!(
907 concat!(
908 "tree {tree}\n",
909 "author Signer <signer@example.invalid> 1700000000 +0000\n",
910 "committer Signer <signer@example.invalid> 1700000000 +0000\n",
911 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
912 " \n",
913 " iQEzBAABCgAdFiEErawcommitbytescontract\n",
914 " =abcd\n",
915 " -----END PGP SIGNATURE-----\n",
916 "\n",
917 "signed commit\n",
918 ),
919 tree = tree,
920 )
921 .into_bytes();
922
923 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Commit, body, format);
924 }
925
926 #[test]
927 fn encoded_raw_commit_with_mergetag_and_custom_headers_preserves_bytes_and_id() {
928 let format = ObjectFormat::Sha1;
929 let tree = ObjectId::empty_tree(format);
930 let parent = ObjectId::empty_blob(format);
931 let body = format!(
932 concat!(
933 "tree {tree}\n",
934 "parent {parent}\n",
935 "author Merger <merger@example.invalid> 1700000000 +0000\n",
936 "committer Merger <merger@example.invalid> 1700000001 +0000\n",
937 "x-review-id 42\n",
938 "mergetag object {parent}\n",
939 " type commit\n",
940 " tag imported-v1\n",
941 " tagger Tagger <tagger@example.invalid> 1699999999 +0000\n",
942 " \n",
943 " imported tag body\n",
944 " gpgsig -----BEGIN PGP SIGNATURE-----\n",
945 " nested-signature-line\n",
946 " -----END PGP SIGNATURE-----\n",
947 "x-sley-extra raw bytes stay here\n",
948 "\n",
949 "merge commit\n",
950 ),
951 tree = tree,
952 parent = parent,
953 )
954 .into_bytes();
955
956 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Commit, body, format);
957 }
958
959 #[test]
960 fn encoded_raw_annotated_tag_with_signature_and_custom_headers_preserves_bytes_and_id() {
961 let format = ObjectFormat::Sha1;
962 let object = ObjectId::empty_blob(format);
963 let body = format!(
964 concat!(
965 "object {object}\n",
966 "type blob\n",
967 "tag signed-v1\n",
968 "tagger Tagger <tagger@example.invalid> 1700000000 -0000\n",
969 "x-release-channel stable\n",
970 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
971 " tag-signature-line-1\n",
972 " tag-signature-line-2\n",
973 " -----END PGP SIGNATURE-----\n",
974 "\n",
975 "release notes\n",
976 ),
977 object = object,
978 )
979 .into_bytes();
980
981 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Tag, body, format);
982 }
983
984 #[test]
985 fn tree_round_trips_entries() {
986 let blob = ObjectId::from_hex(
987 ObjectFormat::Sha1,
988 "ce013625030ba8dba906f756967f9e9ca394464a",
989 )
990 .expect("test operation should succeed");
991 let tree = Tree {
992 entries: vec![TreeEntry {
993 mode: 0o100644,
994 name: BString::from(b"hello.txt"),
995 oid: blob,
996 }],
997 };
998 assert_eq!(
999 Tree::parse(ObjectFormat::Sha1, &tree.write()).expect("test operation should succeed"),
1000 tree
1001 );
1002 }
1003
1004 #[test]
1005 fn tree_entries_iterates_without_name_allocations() {
1006 let format = ObjectFormat::Sha1;
1007 let blob = ObjectId::from_hex(format, "ce013625030ba8dba906f756967f9e9ca394464a")
1008 .expect("test operation should succeed");
1009 let subtree = ObjectId::empty_tree(format);
1010 let mut bytes = Vec::new();
1011
1012 let first_name_start = b"100644 ".len();
1013 write_tree_entry(&mut bytes, EntryKind::Blob.mode(), b"hello.txt", &blob);
1014 let second_name_start = bytes.len() + b"40000 ".len();
1015 write_tree_entry(&mut bytes, EntryKind::Tree.mode(), b"src", &subtree);
1016
1017 let mut entries = TreeEntries::new(format, &bytes);
1018 let first = entries
1019 .next()
1020 .expect("first entry")
1021 .expect("test operation should succeed");
1022 assert_eq!(first.mode, EntryKind::Blob.mode());
1023 assert_eq!(first.name, b"hello.txt");
1024 assert_eq!(first.oid, blob);
1025 assert_eq!(first.kind(), Some(EntryKind::Blob));
1026 assert!(std::ptr::eq(
1027 first.name.as_ptr(),
1028 bytes[first_name_start..].as_ptr()
1029 ));
1030
1031 let second = entries
1032 .next()
1033 .expect("second entry")
1034 .expect("test operation should succeed");
1035 assert_eq!(second.mode, EntryKind::Tree.mode());
1036 assert_eq!(second.name, b"src");
1037 assert_eq!(second.oid, subtree);
1038 assert!(second.is_tree());
1039 assert!(std::ptr::eq(
1040 second.name.as_ptr(),
1041 bytes[second_name_start..].as_ptr()
1042 ));
1043 assert!(entries.next().is_none());
1044
1045 let owned = Tree::parse(format, &bytes).expect("test operation should succeed");
1046 assert_eq!(owned.entries, vec![first.to_owned(), second.to_owned()]);
1047 }
1048
1049 #[test]
1050 fn tree_entries_reports_invalid_mode_path_and_truncated_oid() {
1051 let format = ObjectFormat::Sha1;
1052 let oid = ObjectId::empty_blob(format);
1053
1054 let mut invalid_mode = b"10088 bad\0".to_vec();
1055 invalid_mode.extend_from_slice(oid.as_bytes());
1056 assert_invalid_tree_entry(
1057 TreeEntries::new(format, &invalid_mode)
1058 .next()
1059 .expect("invalid mode result"),
1060 "invalid tree mode",
1061 );
1062
1063 let mut empty_path = b"100644 \0".to_vec();
1064 empty_path.extend_from_slice(oid.as_bytes());
1065 assert_invalid_tree_entry(
1066 TreeEntries::new(format, &empty_path)
1067 .next()
1068 .expect("empty path result"),
1069 "empty tree path",
1070 );
1071
1072 let mut truncated_oid = b"100644 bad\0".to_vec();
1073 truncated_oid.extend_from_slice(&oid.as_bytes()[..format.raw_len() - 1]);
1074 assert_invalid_tree_entry(
1075 TreeEntries::new(format, &truncated_oid)
1076 .next()
1077 .expect("truncated oid result"),
1078 "truncated tree object id",
1079 );
1080 }
1081
1082 #[test]
1083 fn tree_entry_ref_kind_helpers_match_entry_kinds() {
1084 let oid = ObjectId::null(ObjectFormat::Sha1);
1085
1086 let tree = TreeEntryRef {
1087 mode: EntryKind::Tree.mode(),
1088 name: b"dir",
1089 oid,
1090 };
1091 assert_eq!(tree.kind(), Some(EntryKind::Tree));
1092 assert!(tree.is_tree());
1093 assert!(!tree.is_symlink());
1094 assert!(!tree.is_gitlink());
1095 assert!(!tree.is_executable());
1096
1097 let symlink = TreeEntryRef {
1098 mode: EntryKind::Symlink.mode(),
1099 name: b"link",
1100 oid,
1101 };
1102 assert_eq!(symlink.kind(), Some(EntryKind::Symlink));
1103 assert!(symlink.is_symlink());
1104 assert!(!symlink.is_tree());
1105 assert!(!symlink.is_gitlink());
1106 assert!(!symlink.is_executable());
1107
1108 let executable = TreeEntryRef {
1109 mode: EntryKind::BlobExecutable.mode(),
1110 name: b"run",
1111 oid,
1112 };
1113 assert_eq!(executable.kind(), Some(EntryKind::BlobExecutable));
1114 assert!(executable.is_executable());
1115 assert!(!executable.is_tree());
1116 assert!(!executable.is_symlink());
1117 assert!(!executable.is_gitlink());
1118
1119 let gitlink = TreeEntryRef {
1120 mode: EntryKind::Commit.mode(),
1121 name: b"submodule",
1122 oid,
1123 };
1124 assert_eq!(gitlink.kind(), Some(EntryKind::Commit));
1125 assert!(gitlink.is_gitlink());
1126 assert!(!gitlink.is_tree());
1127 assert!(!gitlink.is_symlink());
1128 assert!(!gitlink.is_executable());
1129 }
1130
1131 #[test]
1132 fn commit_round_trips_headers_and_message() {
1133 let tree = ObjectId::from_hex(
1134 ObjectFormat::Sha1,
1135 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1136 )
1137 .expect("test operation should succeed");
1138 let commit = Commit {
1139 tree,
1140 parents: Vec::new(),
1141 author: b"A U Thor <a@example.invalid> 0 +0000".to_vec(),
1142 committer: b"C O Mitter <c@example.invalid> 0 +0000".to_vec(),
1143 encoding: Some(b"ISO-8859-1".to_vec()),
1144 message: b"subject\n\nbody\n".to_vec(),
1145 };
1146 assert_eq!(
1147 Commit::parse(ObjectFormat::Sha1, &commit.write())
1148 .expect("test operation should succeed"),
1149 commit
1150 );
1151 }
1152
1153 #[test]
1154 fn commit_ref_borrows_headers_and_message() {
1155 let format = ObjectFormat::Sha1;
1156 let tree_hex = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1157 let parent_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1158 let body = format!(
1159 "tree {tree_hex}\n\
1160 parent {parent_hex}\n\
1161 author A U Thor <a@example.invalid> 0 +0000\n\
1162 committer C O Mitter <c@example.invalid> 1 -0000\n\
1163 encoding UTF-8\n\
1164 \n\
1165 subject\n\nbody\n"
1166 )
1167 .into_bytes();
1168
1169 let commit = CommitRef::parse(format, &body).expect("test operation should succeed");
1170 assert_eq!(
1171 commit.tree,
1172 ObjectId::from_hex(format, tree_hex).expect("test operation should succeed")
1173 );
1174 assert_eq!(
1175 commit.parents,
1176 vec![ObjectId::from_hex(format, parent_hex).expect("test operation should succeed")]
1177 );
1178 assert_borrows_from(
1179 &body,
1180 commit.author,
1181 b"A U Thor <a@example.invalid> 0 +0000",
1182 );
1183 assert_borrows_from(
1184 &body,
1185 commit.committer,
1186 b"C O Mitter <c@example.invalid> 1 -0000",
1187 );
1188 assert_borrows_from(
1189 &body,
1190 commit.encoding.expect("test operation should succeed"),
1191 b"UTF-8",
1192 );
1193 assert_borrows_from(&body, commit.message, b"subject\n\nbody\n");
1194
1195 assert_eq!(
1196 Commit::parse_ref(format, &body).expect("test operation should succeed"),
1197 commit
1198 );
1199 assert_eq!(
1200 commit.to_owned(),
1201 Commit::parse(format, &body).expect("test operation should succeed")
1202 );
1203 }
1204
1205 #[test]
1206 fn commit_ref_accepts_non_utf8_headers_and_message() {
1207 let format = ObjectFormat::Sha1;
1208 let tree = ObjectId::empty_tree(format);
1209 let mut body = Vec::new();
1210 body.extend_from_slice(format!("tree {tree}\n").as_bytes());
1211 body.extend_from_slice(b"author J\xF6rg <j@example.invalid> 0 +0000\n");
1212 body.extend_from_slice(b"committer M\xFCller <m@example.invalid> 1 +0000\n");
1213 body.extend_from_slice(b"encoding ISO-8859-1\n\n");
1214 body.extend_from_slice(b"caf\xE9\n");
1215
1216 let commit = CommitRef::parse(format, &body).expect("non-utf8 commit parses");
1217 assert_eq!(commit.tree, tree);
1218 assert_borrows_from(&body, commit.author, b"J\xF6rg <j@example.invalid> 0 +0000");
1219 assert_borrows_from(
1220 &body,
1221 commit.committer,
1222 b"M\xFCller <m@example.invalid> 1 +0000",
1223 );
1224 assert_borrows_from(&body, commit.encoding.expect("encoding"), b"ISO-8859-1");
1225 assert_borrows_from(&body, commit.message, b"caf\xE9\n");
1226 assert_eq!(commit.to_owned().write(), body);
1227 }
1228
1229 #[test]
1230 fn commit_ref_rejects_missing_or_malformed_required_headers() {
1231 let format = ObjectFormat::Sha1;
1232 let valid_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1233 let valid_idents =
1234 b"author A U Thor <a@example.invalid> 0 +0000\ncommitter C O Mitter <c@example.invalid> 0 +0000\n\nmessage\n";
1235 let mut missing_tree = Vec::new();
1236 missing_tree.extend_from_slice(valid_idents);
1237 assert_invalid_object(
1238 CommitRef::parse(format, &missing_tree),
1239 "commit missing tree",
1240 );
1241
1242 let malformed_tree = b"tree not-an-object-id\nauthor A U Thor <a@example.invalid> 0 +0000\ncommitter C O Mitter <c@example.invalid> 0 +0000\n\nmessage\n";
1243 assert!(matches!(
1244 CommitRef::parse(format, malformed_tree),
1245 Err(GitError::InvalidObjectId(_))
1246 ));
1247
1248 let missing_committer =
1249 format!("tree {valid_tree}\nauthor A U Thor <a@example.invalid> 0 +0000\n\nmessage\n")
1250 .into_bytes();
1251 assert_invalid_object(
1252 CommitRef::parse(format, &missing_committer),
1253 "commit missing committer",
1254 );
1255 }
1256
1257 #[test]
1258 fn tag_round_trips_headers_and_message() {
1259 let object = ObjectId::from_hex(
1260 ObjectFormat::Sha1,
1261 "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15",
1262 )
1263 .expect("test operation should succeed");
1264 let tag = Tag {
1265 object,
1266 object_type: ObjectType::Commit,
1267 name: b"v1.0".to_vec(),
1268 tagger: Some(b"Example User <example@example.invalid> 0 +0000".to_vec()),
1269 message: b"release\n".to_vec(),
1270 raw_body: None,
1271 };
1272 assert_eq!(
1273 Tag::parse(ObjectFormat::Sha1, &tag.write()).expect("test operation should succeed"),
1274 tag
1275 );
1276 }
1277
1278 #[test]
1279 fn tag_ref_accepts_non_utf8_tagger_and_message() {
1280 let format = ObjectFormat::Sha1;
1281 let object = ObjectId::empty_blob(format);
1282 let mut body = Vec::new();
1283 body.extend_from_slice(format!("object {object}\n").as_bytes());
1284 body.extend_from_slice(b"type blob\n");
1285 body.extend_from_slice(b"tag v1.0\n");
1286 body.extend_from_slice(b"tagger J\xF6rg <j@example.invalid> 0 +0000\n\n");
1287 body.extend_from_slice(b"caf\xE9\n");
1288
1289 let tag = TagRef::parse(format, &body).expect("non-utf8 tag parses");
1290 assert_eq!(tag.object, object);
1291 assert_eq!(tag.object_type, ObjectType::Blob);
1292 assert_borrows_from(&body, tag.name, b"v1.0");
1293 assert_borrows_from(
1294 &body,
1295 tag.tagger.expect("tagger"),
1296 b"J\xF6rg <j@example.invalid> 0 +0000",
1297 );
1298 assert_borrows_from(&body, tag.message, b"caf\xE9\n");
1299 assert_eq!(tag.to_owned().write(), body);
1300 }
1301
1302 #[test]
1303 fn typed_commit_canonicalizes_but_tag_write_preserves_raw_body() {
1304 let format = ObjectFormat::Sha1;
1305 let tree = ObjectId::empty_tree(format);
1306 let raw_commit = format!(
1307 concat!(
1308 "tree {tree}\n",
1309 "author A U Thor <a@example.invalid> 0 +0000\n",
1310 "x-hidden keep only in raw encoded object\n",
1311 "committer C O Mitter <c@example.invalid> 0 +0000\n",
1312 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1313 " typed-parser-accepts-this\n",
1314 " -----END PGP SIGNATURE-----\n",
1315 "\n",
1316 "subject\n",
1317 ),
1318 tree = tree,
1319 )
1320 .into_bytes();
1321
1322 let commit = Commit::parse(format, &raw_commit).expect("test operation should succeed");
1323 assert_eq!(commit.tree, tree);
1324 assert_eq!(commit.author, b"A U Thor <a@example.invalid> 0 +0000");
1325 assert_eq!(commit.committer, b"C O Mitter <c@example.invalid> 0 +0000");
1326 assert_eq!(commit.message, b"subject\n");
1327
1328 let written_commit = commit.write();
1329 assert_ne!(written_commit, raw_commit);
1330 assert_bytes_not_contains(&written_commit, b"x-hidden");
1331 assert_bytes_not_contains(&written_commit, b"gpgsig");
1332
1333 let object = ObjectId::empty_blob(format);
1334 let raw_tag = format!(
1335 concat!(
1336 "object {object}\n",
1337 "type blob\n",
1338 "tag v1.0\n",
1339 "x-hidden keep only in raw encoded object\n",
1340 "tagger Example User <example@example.invalid> 0 +0000\n",
1341 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1342 " typed-parser-accepts-this-too\n",
1343 " -----END PGP SIGNATURE-----\n",
1344 "\n",
1345 "release\n",
1346 ),
1347 object = object,
1348 )
1349 .into_bytes();
1350
1351 let tag = Tag::parse(format, &raw_tag).expect("test operation should succeed");
1352 assert_eq!(tag.object, object);
1353 assert_eq!(tag.object_type, ObjectType::Blob);
1354 assert_eq!(tag.name, b"v1.0");
1355 assert_eq!(
1356 tag.tagger.as_deref(),
1357 Some(&b"Example User <example@example.invalid> 0 +0000"[..])
1358 );
1359 assert_eq!(tag.message, b"release\n");
1360
1361 let written_tag = tag.write();
1362 assert_eq!(written_tag, raw_tag);
1363 let original_oid = EncodedObject::new(ObjectType::Tag, raw_tag).object_id(format);
1364 let written_oid = EncodedObject::new(ObjectType::Tag, written_tag).object_id(format);
1365 assert_eq!(
1366 original_oid.expect("original tag oid"),
1367 written_oid.expect("written tag oid")
1368 );
1369 }
1370
1371 #[test]
1372 fn tag_parse_write_preserves_uppercase_object_and_header_only_body() {
1373 let format = ObjectFormat::Sha1;
1374 let object = ObjectId::empty_blob(format);
1375 let mut raw_tag = Vec::new();
1376 raw_tag.extend_from_slice(
1377 format!("object {}\n", object.to_string().to_uppercase()).as_bytes(),
1378 );
1379 raw_tag.extend_from_slice(b"type blob\n");
1380 raw_tag.extend_from_slice(b"tag v1.0\n");
1381 raw_tag.extend_from_slice(b"tagger Example <example@example.invalid> 0 +0000\n");
1382
1383 let tag = Tag::parse(format, &raw_tag).expect("header-only tag parses");
1384 assert_eq!(tag.object, object);
1385 assert_eq!(tag.message, b"");
1386 assert_eq!(tag.write(), raw_tag);
1387 }
1388
1389 #[test]
1390 fn tag_ref_borrows_name_tagger_and_message() {
1391 let format = ObjectFormat::Sha1;
1392 let object_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1393 let body = format!(
1394 "object {object_hex}\n\
1395 type commit\n\
1396 tag v1.0-borrowed\n\
1397 tagger Example User <example@example.invalid> 0 +0000\n\
1398 \n\
1399 release notes\n"
1400 )
1401 .into_bytes();
1402
1403 let tag = TagRef::parse(format, &body).expect("test operation should succeed");
1404 assert_eq!(
1405 tag.object,
1406 ObjectId::from_hex(format, object_hex).expect("test operation should succeed")
1407 );
1408 assert_eq!(tag.object_type, ObjectType::Commit);
1409 assert_borrows_from(&body, tag.name, b"v1.0-borrowed");
1410 assert_borrows_from(
1411 &body,
1412 tag.tagger.expect("test operation should succeed"),
1413 b"Example User <example@example.invalid> 0 +0000",
1414 );
1415 assert_borrows_from(&body, tag.message, b"release notes\n");
1416
1417 assert_eq!(
1418 Tag::parse_ref(format, &body).expect("test operation should succeed"),
1419 tag
1420 );
1421 assert_eq!(
1422 tag.to_owned(),
1423 Tag::parse(format, &body).expect("test operation should succeed")
1424 );
1425 }
1426
1427 #[test]
1428 fn tag_ref_rejects_missing_or_malformed_required_headers() {
1429 let format = ObjectFormat::Sha1;
1430 let object_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1431
1432 let missing_name = format!("object {object_hex}\ntype commit\n\nmessage\n").into_bytes();
1433 assert_invalid_object(TagRef::parse(format, &missing_name), "tag missing name");
1434
1435 let malformed_object = b"object not-an-object-id\ntype commit\ntag v1.0\n\nmessage\n";
1436 assert!(matches!(
1437 TagRef::parse(format, malformed_object),
1438 Err(GitError::InvalidObjectId(_))
1439 ));
1440
1441 let malformed_type =
1442 format!("object {object_hex}\ntype mystery\ntag v1.0\n\nmessage\n").into_bytes();
1443 assert_invalid_object(
1444 TagRef::parse(format, &malformed_type),
1445 "unknown object type mystery",
1446 );
1447 }
1448
1449 #[test]
1450 fn commit_signature_accessors_parse_raw_idents_without_changing_storage() {
1451 let tree = ObjectId::from_hex(
1452 ObjectFormat::Sha1,
1453 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1454 )
1455 .expect("test operation should succeed");
1456 let author_raw = b"A U Thor <a@example.invalid> 1700000000 +0530".to_vec();
1457 let committer_raw = b"C O Mitter <c@example.invalid> 1700000001 -0000".to_vec();
1458 let commit = Commit {
1459 tree,
1460 parents: Vec::new(),
1461 author: author_raw.clone(),
1462 committer: committer_raw.clone(),
1463 encoding: None,
1464 message: b"subject\n".to_vec(),
1465 };
1466
1467 let author = commit.author_signature().expect("author parses");
1468 assert_eq!(author.name.as_bytes(), b"A U Thor");
1469 assert_eq!(author.email.as_bytes(), b"a@example.invalid");
1470 assert_eq!(author.time.seconds, 1_700_000_000);
1471 assert_eq!(author.time.timezone_offset_minutes, 330);
1472 assert!(!author.time.negative_utc);
1473 assert_eq!(author.to_ident_bytes(), author_raw);
1475
1476 let committer = commit.committer_signature().expect("committer parses");
1477 assert_eq!(committer.time.seconds, 1_700_000_001);
1478 assert!(committer.time.negative_utc);
1480 assert_eq!(committer.to_ident_bytes(), committer_raw);
1481
1482 assert_eq!(commit.author, author_raw);
1485 assert_eq!(commit.committer, committer_raw);
1486 let written = commit.write();
1487 assert_eq!(
1488 Commit::parse(ObjectFormat::Sha1, &written).expect("test operation should succeed"),
1489 commit
1490 );
1491 }
1492
1493 #[test]
1494 fn commit_signature_accessor_is_none_for_malformed_ident() {
1495 let tree = ObjectId::from_hex(
1496 ObjectFormat::Sha1,
1497 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1498 )
1499 .expect("test operation should succeed");
1500 let commit = Commit {
1501 tree,
1502 parents: Vec::new(),
1503 author: b"garbage without an email or time".to_vec(),
1504 committer: b"C O Mitter <c@example.invalid> 0 +0000".to_vec(),
1505 encoding: None,
1506 message: b"x\n".to_vec(),
1507 };
1508 assert!(commit.author_signature().is_none());
1509 assert!(commit.committer_signature().is_some());
1510 }
1511
1512 #[test]
1513 fn tag_signature_accessor_parses_tagger_and_handles_absence() {
1514 let object = ObjectId::from_hex(
1515 ObjectFormat::Sha1,
1516 "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15",
1517 )
1518 .expect("test operation should succeed");
1519 let tagger_raw = b"Example User <example@example.invalid> 1700000000 -0000".to_vec();
1520 let tag = Tag {
1521 object: object.clone(),
1522 object_type: ObjectType::Commit,
1523 name: b"v1.0".to_vec(),
1524 tagger: Some(tagger_raw.clone()),
1525 message: b"release\n".to_vec(),
1526 raw_body: None,
1527 };
1528 let tagger = tag.tagger_signature().expect("tagger parses");
1529 assert_eq!(tagger.name.as_bytes(), b"Example User");
1530 assert!(tagger.time.negative_utc);
1531 assert_eq!(tagger.to_ident_bytes(), tagger_raw);
1532 assert_eq!(tag.tagger.as_deref(), Some(tagger_raw.as_slice()));
1534
1535 let lightweight = Tag {
1537 object,
1538 object_type: ObjectType::Commit,
1539 name: b"v1.0".to_vec(),
1540 tagger: None,
1541 message: b"x\n".to_vec(),
1542 raw_body: None,
1543 };
1544 assert!(lightweight.tagger_signature().is_none());
1545 }
1546
1547 fn write_tree_entry(body: &mut Vec<u8>, mode: u32, name: &[u8], oid: &ObjectId) {
1548 body.extend_from_slice(format!("{:o}", mode).as_bytes());
1549 body.push(b' ');
1550 body.extend_from_slice(name);
1551 body.push(0);
1552 body.extend_from_slice(oid.as_bytes());
1553 }
1554
1555 fn assert_invalid_tree_entry(result: Result<TreeEntryRef<'_>>, expected: &str) {
1556 match result {
1557 Err(GitError::InvalidFormat(message)) => assert_eq!(message, expected),
1558 other => panic!("expected invalid format {expected:?}, got {other:?}"),
1559 }
1560 }
1561
1562 fn assert_invalid_object<T: std::fmt::Debug>(result: Result<T>, expected: &str) {
1563 match result {
1564 Err(GitError::InvalidObject(message)) => assert_eq!(message, expected),
1565 other => panic!("expected invalid object {expected:?}, got {other:?}"),
1566 }
1567 }
1568
1569 fn assert_encoded_preserves_framed_bytes_and_id(
1570 object_type: ObjectType,
1571 body: Vec<u8>,
1572 format: ObjectFormat,
1573 ) {
1574 let object = EncodedObject::new(object_type, body.clone());
1575 let expected_id = object
1576 .object_id(format)
1577 .expect("test operation should succeed");
1578 let framed = object.framed_bytes();
1579
1580 let parsed = parse_framed_object(&framed).expect("test operation should succeed");
1581 assert_eq!(parsed.object_type, object_type);
1582 assert_eq!(parsed.body, body);
1583 assert_eq!(
1584 parsed
1585 .object_id(format)
1586 .expect("test operation should succeed"),
1587 expected_id
1588 );
1589 assert_eq!(parsed.framed_bytes(), framed);
1590 }
1591
1592 fn assert_bytes_not_contains(haystack: &[u8], needle: &[u8]) {
1593 assert!(
1594 !haystack
1595 .windows(needle.len())
1596 .any(|window| window == needle),
1597 "expected bytes not to contain {:?}",
1598 String::from_utf8_lossy(needle)
1599 );
1600 }
1601
1602 fn assert_borrows_from(body: &[u8], slice: &[u8], expected: &[u8]) {
1603 assert_eq!(slice, expected);
1604 let offset = body
1605 .windows(expected.len())
1606 .position(|window| window == expected)
1607 .expect("expected slice appears in body");
1608 assert!(std::ptr::eq(slice.as_ptr(), body[offset..].as_ptr()));
1609 }
1610}