This categorizes them semantically without using the overloaded names of owning and borrowing, which mean different things in Rust.
Showing the changes to git-object/src/mutable (9 of 34 files). Show all
Unified · Split
| @@ -0,0 +1,60 @@ | ||
| 1 | +use crate::{ | |
| 2 | + commit, | |
| 3 | + mutable::{self, ser, NL}, | |
| 4 | +}; | |
| 5 | +use bstr::{BStr, BString, ByteSlice}; | |
| 6 | +use smallvec::SmallVec; | |
| 7 | +use std::io; | |
| 8 | + | |
| 9 | +/// A mutable git commit, representing an annotated state of a working tree along with a reference to its historical commits. | |
| 10 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 11 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 12 | +pub struct Commit { | |
| 13 | + /// The hash of recorded working tree state. | |
| 14 | + pub tree: git_hash::ObjectId, | |
| 15 | + /// Hash of each parent commit. Empty for the first commit in repository. | |
| 16 | + pub parents: SmallVec<[git_hash::ObjectId; 1]>, | |
| 17 | + /// Who wrote this commit. | |
| 18 | + pub author: mutable::Signature, | |
| 19 | + /// Who committed this commit. | |
| 20 | + /// | |
| 21 | + /// This may be different from the `author` in case the author couldn't write to the repository themselves and | |
| 22 | + /// is commonly encountered with contributed commits. | |
| 23 | + pub committer: mutable::Signature, | |
| 24 | + /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493). | |
| 25 | + pub encoding: Option<BString>, | |
| 26 | + /// The commit message documenting the change. | |
| 27 | + pub message: BString, | |
| 28 | + /// Extra header fields, in order of them being encountered, made accessible with the iterator returned | |
| 29 | + /// by [`extra_headers()`][Commit::extra_headers()]. | |
| 30 | + pub extra_headers: Vec<(BString, BString)>, | |
| 31 | +} | |
| 32 | + | |
| 33 | +impl Commit { | |
| 34 | + /// Returns a convenient iterator over all extra headers. | |
| 35 | + pub fn extra_headers(&self) -> commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> { | |
| 36 | + commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (k.as_bstr(), v.as_bstr()))) | |
| 37 | + } | |
| 38 | + /// Serializes this instance to `out` in the git serialization format. | |
| 39 | + pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { | |
| 40 | + ser::trusted_header_id(b"tree", &self.tree, &mut out)?; | |
| 41 | + for parent in &self.parents { | |
| 42 | + ser::trusted_header_id(b"parent", parent, &mut out)?; | |
| 43 | + } | |
| 44 | + ser::trusted_header_signature(b"author", &self.author, &mut out)?; | |
| 45 | + ser::trusted_header_signature(b"committer", &self.committer, &mut out)?; | |
| 46 | + if let Some(encoding) = self.encoding.as_ref() { | |
| 47 | + ser::header_field(b"encoding", encoding, &mut out)?; | |
| 48 | + } | |
| 49 | + for (name, value) in &self.extra_headers { | |
| 50 | + let has_newline = value.find_byte(b'\n').is_some(); | |
| 51 | + if has_newline { | |
| 52 | + ser::header_field_multi_line(name, value, &mut out)?; | |
| 53 | + } else { | |
| 54 | + ser::trusted_header_field(name, value, &mut out)?; | |
| 55 | + } | |
| 56 | + } | |
| 57 | + out.write_all(NL)?; | |
| 58 | + out.write_all(&self.message) | |
| 59 | + } | |
| 60 | +} | |
| @@ -0,0 +1,101 @@ | ||
| 1 | +use crate::{immutable, mutable}; | |
| 2 | + | |
| 3 | +impl From<immutable::Signature<'_>> for mutable::Signature { | |
| 4 | + fn from(other: immutable::Signature<'_>) -> mutable::Signature { | |
| 5 | + let immutable::Signature { name, email, time } = other; | |
| 6 | + mutable::Signature { | |
| 7 | + name: name.to_owned(), | |
| 8 | + email: email.to_owned(), | |
| 9 | + time, | |
| 10 | + } | |
| 11 | + } | |
| 12 | +} | |
| 13 | + | |
| 14 | +impl From<immutable::Tag<'_>> for mutable::Tag { | |
| 15 | + fn from(other: immutable::Tag<'_>) -> mutable::Tag { | |
| 16 | + let immutable::Tag { | |
| 17 | + target, | |
| 18 | + name, | |
| 19 | + target_kind, | |
| 20 | + message, | |
| 21 | + signature, | |
| 22 | + pgp_signature, | |
| 23 | + } = other; | |
| 24 | + mutable::Tag { | |
| 25 | + target: git_hash::ObjectId::from_hex(&target).expect("40 bytes hex sha1"), | |
| 26 | + name: name.to_owned(), | |
| 27 | + target_kind, | |
| 28 | + message: message.to_owned(), | |
| 29 | + signature: signature.map(Into::into), | |
| 30 | + pgp_signature: pgp_signature.map(ToOwned::to_owned), | |
| 31 | + } | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 35 | +impl From<immutable::Commit<'_>> for mutable::Commit { | |
| 36 | + fn from(other: immutable::Commit<'_>) -> mutable::Commit { | |
| 37 | + let immutable::Commit { | |
| 38 | + tree, | |
| 39 | + parents, | |
| 40 | + author, | |
| 41 | + committer, | |
| 42 | + encoding, | |
| 43 | + message, | |
| 44 | + extra_headers, | |
| 45 | + } = other; | |
| 46 | + mutable::Commit { | |
| 47 | + tree: git_hash::ObjectId::from_hex(&tree).expect("40 bytes hex sha1"), | |
| 48 | + parents: parents | |
| 49 | + .iter() | |
| 50 | + .map(|parent| git_hash::ObjectId::from_hex(parent).expect("40 bytes hex sha1")) | |
| 51 | + .collect(), | |
| 52 | + author: author.into(), | |
| 53 | + committer: committer.into(), | |
| 54 | + encoding: encoding.map(ToOwned::to_owned), | |
| 55 | + message: message.to_owned(), | |
| 56 | + extra_headers: extra_headers | |
| 57 | + .into_iter() | |
| 58 | + .map(|(k, v)| (k.into(), v.into_owned())) | |
| 59 | + .collect(), | |
| 60 | + } | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +impl<'a> From<immutable::Blob<'a>> for mutable::Blob { | |
| 65 | + fn from(v: immutable::Blob<'a>) -> Self { | |
| 66 | + mutable::Blob { | |
| 67 | + data: v.data.to_owned(), | |
| 68 | + } | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +impl From<immutable::Tree<'_>> for mutable::Tree { | |
| 73 | + fn from(other: immutable::Tree<'_>) -> mutable::Tree { | |
| 74 | + let immutable::Tree { entries } = other; | |
| 75 | + mutable::Tree { | |
| 76 | + entries: entries.into_iter().map(Into::into).collect(), | |
| 77 | + } | |
| 78 | + } | |
| 79 | +} | |
| 80 | + | |
| 81 | +impl From<immutable::tree::Entry<'_>> for mutable::tree::Entry { | |
| 82 | + fn from(other: immutable::tree::Entry<'_>) -> mutable::tree::Entry { | |
| 83 | + let immutable::tree::Entry { mode, filename, oid } = other; | |
| 84 | + mutable::tree::Entry { | |
| 85 | + mode, | |
| 86 | + filename: filename.to_owned(), | |
| 87 | + oid: oid.into(), | |
| 88 | + } | |
| 89 | + } | |
| 90 | +} | |
| 91 | + | |
| 92 | +impl<'a> From<immutable::Object<'a>> for mutable::Object { | |
| 93 | + fn from(v: immutable::Object<'_>) -> Self { | |
| 94 | + match v { | |
| 95 | + immutable::Object::Tree(v) => mutable::Object::Tree(v.into()), | |
| 96 | + immutable::Object::Blob(v) => mutable::Object::Blob(v.into()), | |
| 97 | + immutable::Object::Commit(v) => mutable::Object::Commit(v.into()), | |
| 98 | + immutable::Object::Tag(v) => mutable::Object::Tag(v.into()), | |
| 99 | + } | |
| 100 | + } | |
| 101 | +} | |
| @@ -0,0 +1,49 @@ | ||
| 1 | +//! Mutable objects with each field being separately allocated and changeable. | |
| 2 | +//! | |
| 3 | +//! Mutable objects are Commits, Trees, Blobs and Tags that can be changed and serialized. | |
| 4 | +//! They either created using object [construction][Object] or by [deserializing existing objects][crate::immutable::Object::from_bytes()] | |
| 5 | +//! and converting these [into mutable copies][crate::immutable::Object::into_mutable()] for adjustments. | |
| 6 | + | |
| 7 | +pub(crate) const NL: &[u8; 1] = b"\n"; | |
| 8 | +pub(crate) const SPACE: &[u8; 1] = b" "; | |
| 9 | + | |
| 10 | +mod convert; | |
| 11 | +mod ser; | |
| 12 | + | |
| 13 | +mod tag; | |
| 14 | +pub use tag::Tag; | |
| 15 | + | |
| 16 | +/// | |
| 17 | +pub mod tree; | |
| 18 | +pub use tree::Tree; | |
| 19 | + | |
| 20 | +mod commit; | |
| 21 | +pub use commit::Commit; | |
| 22 | + | |
| 23 | +mod blob { | |
| 24 | + use std::io; | |
| 25 | + | |
| 26 | + /// A mutable chunk of any [`data`][Blob::data]. | |
| 27 | + #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 28 | + #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 29 | + pub struct Blob { | |
| 30 | + /// The data itself. | |
| 31 | + pub data: Vec<u8>, | |
| 32 | + } | |
| 33 | + | |
| 34 | + impl Blob { | |
| 35 | + /// Write the blobs data to `out` verbatim. | |
| 36 | + pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { | |
| 37 | + out.write_all(&self.data) | |
| 38 | + } | |
| 39 | + } | |
| 40 | +} | |
| 41 | +pub use blob::Blob; | |
| 42 | + | |
| 43 | +/// | |
| 44 | +pub mod signature; | |
| 45 | +#[doc(inline)] | |
| 46 | +pub use signature::Signature; | |
| 47 | + | |
| 48 | +mod object; | |
| 49 | +pub use object::Object; | |
| @@ -0,0 +1,141 @@ | ||
| 1 | +use crate::mutable; | |
| 2 | +use std::io; | |
| 3 | + | |
| 4 | +/// A mutable object representing [`Trees`][mutable::Tree], [`Blobs`][mutable::Blob], [`Commits`][mutable::Commit] or [`Tags`][mutable::Tag]. | |
| 5 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 6 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 7 | +#[allow(clippy::large_enum_variant, missing_docs)] | |
| 8 | +pub enum Object { | |
| 9 | + Tree(mutable::Tree), | |
| 10 | + Blob(mutable::Blob), | |
| 11 | + Commit(mutable::Commit), | |
| 12 | + Tag(mutable::Tag), | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// Convenient extraction of typed object. | |
| 16 | +impl Object { | |
| 17 | + /// Returns a [`Blob`][mutable::Blob] if it is one. | |
| 18 | + pub fn as_blob(&self) -> Option<&mutable::Blob> { | |
| 19 | + match self { | |
| 20 | + Object::Blob(v) => Some(v), | |
| 21 | + _ => None, | |
| 22 | + } | |
| 23 | + } | |
| 24 | + /// Returns a [`Commit`][mutable::Commit] if it is one. | |
| 25 | + pub fn as_commit(&self) -> Option<&mutable::Commit> { | |
| 26 | + match self { | |
| 27 | + Object::Commit(v) => Some(v), | |
| 28 | + _ => None, | |
| 29 | + } | |
| 30 | + } | |
| 31 | + /// Returns a [`Tree`][mutable::Tree] if it is one. | |
| 32 | + pub fn as_tree(&self) -> Option<&mutable::Tree> { | |
| 33 | + match self { | |
| 34 | + Object::Tree(v) => Some(v), | |
| 35 | + _ => None, | |
| 36 | + } | |
| 37 | + } | |
| 38 | + /// Returns a [`Tag`][mutable::Tag] if it is one. | |
| 39 | + pub fn as_tag(&self) -> Option<&mutable::Tag> { | |
| 40 | + match self { | |
| 41 | + Object::Tag(v) => Some(v), | |
| 42 | + _ => None, | |
| 43 | + } | |
| 44 | + } | |
| 45 | + /// Returns the kind of object stored in this instance. | |
| 46 | + pub fn kind(&self) -> crate::Kind { | |
| 47 | + match self { | |
| 48 | + Object::Tree(_) => crate::Kind::Tree, | |
| 49 | + Object::Blob(_) => crate::Kind::Blob, | |
| 50 | + Object::Commit(_) => crate::Kind::Commit, | |
| 51 | + Object::Tag(_) => crate::Kind::Tag, | |
| 52 | + } | |
| 53 | + } | |
| 54 | +} | |
| 55 | + | |
| 56 | +/// Serialization | |
| 57 | +impl Object { | |
| 58 | + /// Write the contained object to `out` in the git serialization format. | |
| 59 | + pub fn write_to(&self, out: impl io::Write) -> io::Result<()> { | |
| 60 | + use Object::*; | |
| 61 | + match self { | |
| 62 | + Tree(v) => v.write_to(out), | |
| 63 | + Blob(v) => v.write_to(out), | |
| 64 | + Commit(v) => v.write_to(out), | |
| 65 | + Tag(v) => v.write_to(out), | |
| 66 | + } | |
| 67 | + } | |
| 68 | +} | |
| 69 | + | |
| 70 | +mod convert { | |
| 71 | + use crate::mutable::{Blob, Commit, Object, Tag, Tree}; | |
| 72 | + use std::convert::TryFrom; | |
| 73 | + | |
| 74 | + impl From<Tag> for Object { | |
| 75 | + fn from(v: Tag) -> Self { | |
| 76 | + Object::Tag(v) | |
| 77 | + } | |
| 78 | + } | |
| 79 | + | |
| 80 | + impl From<Commit> for Object { | |
| 81 | + fn from(v: Commit) -> Self { | |
| 82 | + Object::Commit(v) | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + impl From<Tree> for Object { | |
| 87 | + fn from(v: Tree) -> Self { | |
| 88 | + Object::Tree(v) | |
| 89 | + } | |
| 90 | + } | |
| 91 | + | |
| 92 | + impl From<Blob> for Object { | |
| 93 | + fn from(v: Blob) -> Self { | |
| 94 | + Object::Blob(v) | |
| 95 | + } | |
| 96 | + } | |
| 97 | + | |
| 98 | + impl TryFrom<Object> for Tag { | |
| 99 | + type Error = Object; | |
| 100 | + | |
| 101 | + fn try_from(value: Object) -> Result<Self, Self::Error> { | |
| 102 | + Ok(match value { | |
| 103 | + Object::Tag(v) => v, | |
| 104 | + _ => return Err(value), | |
| 105 | + }) | |
| 106 | + } | |
| 107 | + } | |
| 108 | + | |
| 109 | + impl TryFrom<Object> for Commit { | |
| 110 | + type Error = Object; | |
| 111 | + | |
| 112 | + fn try_from(value: Object) -> Result<Self, Self::Error> { | |
| 113 | + Ok(match value { | |
| 114 | + Object::Commit(v) => v, | |
| 115 | + _ => return Err(value), | |
| 116 | + }) | |
| 117 | + } | |
| 118 | + } | |
| 119 | + | |
| 120 | + impl TryFrom<Object> for Tree { | |
| 121 | + type Error = Object; | |
| 122 | + | |
| 123 | + fn try_from(value: Object) -> Result<Self, Self::Error> { | |
| 124 | + Ok(match value { | |
| 125 | + Object::Tree(v) => v, | |
| 126 | + _ => return Err(value), | |
| 127 | + }) | |
| 128 | + } | |
| 129 | + } | |
| 130 | + | |
| 131 | + impl TryFrom<Object> for Blob { | |
| 132 | + type Error = Object; | |
| 133 | + | |
| 134 | + fn try_from(value: Object) -> Result<Self, Self::Error> { | |
| 135 | + Ok(match value { | |
| 136 | + Object::Blob(v) => v, | |
| 137 | + _ => return Err(value), | |
| 138 | + }) | |
| 139 | + } | |
| 140 | + } | |
| 141 | +} | |
| @@ -0,0 +1,71 @@ | ||
| 1 | +use crate::mutable::{self, NL, SPACE}; | |
| 2 | +use bstr::{BString, ByteSlice}; | |
| 3 | +use quick_error::quick_error; | |
| 4 | +use std::io; | |
| 5 | + | |
| 6 | +quick_error! { | |
| 7 | + #[derive(Debug)] | |
| 8 | + enum Error { | |
| 9 | + NewlineInHeaderValue(value: BString) { | |
| 10 | + display("Newlines are not allowed in header values: {:?}", value) | |
| 11 | + } | |
| 12 | + EmptyValue { | |
| 13 | + display("Header values must not be empty") | |
| 14 | + } | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +impl From<Error> for io::Error { | |
| 19 | + fn from(other: Error) -> io::Error { | |
| 20 | + io::Error::new(io::ErrorKind::Other, other) | |
| 21 | + } | |
| 22 | +} | |
| 23 | + | |
| 24 | +pub fn header_field_multi_line(name: &[u8], value: &[u8], mut out: impl io::Write) -> io::Result<()> { | |
| 25 | + let mut lines = value.as_bstr().lines(); | |
| 26 | + trusted_header_field(name, lines.next().expect("non-empty value"), &mut out)?; | |
| 27 | + for line in lines { | |
| 28 | + out.write_all(SPACE)?; | |
| 29 | + out.write_all(line)?; | |
| 30 | + out.write_all(NL)?; | |
| 31 | + } | |
| 32 | + // If the last line ended on a newline, we have to re-add it by force | |
| 33 | + if let Some(b) = value.last() { | |
| 34 | + if *b == b'\n' { | |
| 35 | + out.write_all(SPACE)?; | |
| 36 | + out.write_all(NL)?; | |
| 37 | + } | |
| 38 | + } | |
| 39 | + Ok(()) | |
| 40 | +} | |
| 41 | + | |
| 42 | +pub fn trusted_header_field(name: &[u8], value: &[u8], mut out: impl io::Write) -> io::Result<()> { | |
| 43 | + out.write_all(name)?; | |
| 44 | + out.write_all(SPACE)?; | |
| 45 | + out.write_all(value)?; | |
| 46 | + out.write_all(NL) | |
| 47 | +} | |
| 48 | + | |
| 49 | +pub fn trusted_header_signature(name: &[u8], value: &mutable::Signature, mut out: impl io::Write) -> io::Result<()> { | |
| 50 | + out.write_all(name)?; | |
| 51 | + out.write_all(SPACE)?; | |
| 52 | + value.write_to(&mut out)?; | |
| 53 | + out.write_all(NL) | |
| 54 | +} | |
| 55 | + | |
| 56 | +pub fn trusted_header_id(name: &[u8], value: &git_hash::ObjectId, mut out: impl io::Write) -> io::Result<()> { | |
| 57 | + out.write_all(name)?; | |
| 58 | + out.write_all(&SPACE[..])?; | |
| 59 | + value.write_hex_to(&mut out)?; | |
| 60 | + out.write_all(&NL[..]) | |
| 61 | +} | |
| 62 | + | |
| 63 | +pub fn header_field(name: &[u8], value: &[u8], out: impl io::Write) -> io::Result<()> { | |
| 64 | + if value.is_empty() { | |
| 65 | + return Err(Error::EmptyValue.into()); | |
| 66 | + } | |
| 67 | + if value.find(NL).is_some() { | |
| 68 | + return Err(Error::NewlineInHeaderValue(value.into()).into()); | |
| 69 | + } | |
| 70 | + trusted_header_field(name, value, out) | |
| 71 | +} | |
| @@ -0,0 +1,55 @@ | ||
| 1 | +use crate::{mutable::SPACE, Time}; | |
| 2 | +use bstr::{BStr, BString, ByteSlice}; | |
| 3 | +use quick_error::quick_error; | |
| 4 | +use std::io; | |
| 5 | + | |
| 6 | +/// A mutable signature is created by an actor at a certain time. | |
| 7 | +/// | |
| 8 | +/// Note that this is not a cryptographical signature. | |
| 9 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 10 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 11 | +pub struct Signature { | |
| 12 | + /// The actors name. | |
| 13 | + pub name: BString, | |
| 14 | + /// The actor's email. | |
| 15 | + pub email: BString, | |
| 16 | + /// The time stamp at which the signature is performed. | |
| 17 | + pub time: Time, | |
| 18 | +} | |
| 19 | + | |
| 20 | +quick_error! { | |
| 21 | + /// The Error produced in by [`Signature::write_to()`]. | |
| 22 | + #[derive(Debug)] | |
| 23 | + #[allow(missing_docs)] | |
| 24 | + pub enum Error { | |
| 25 | + IllegalCharacter { | |
| 26 | + display("Signature name or email must not contain '<', '>' or \\n") | |
| 27 | + } | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +impl From<Error> for io::Error { | |
| 32 | + fn from(err: Error) -> Self { | |
| 33 | + io::Error::new(io::ErrorKind::Other, err) | |
| 34 | + } | |
| 35 | +} | |
| 36 | + | |
| 37 | +impl Signature { | |
| 38 | + /// Serialize this instance to `out` in the git serialization format for actors. | |
| 39 | + pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { | |
| 40 | + out.write_all(validated_token(self.name.as_bstr())?)?; | |
| 41 | + out.write_all(SPACE)?; | |
| 42 | + out.write_all(&b"<"[..])?; | |
| 43 | + out.write_all(validated_token(self.email.as_bstr())?)?; | |
| 44 | + out.write_all(&b"> "[..])?; | |
| 45 | + self.time.write_to(out)?; | |
| 46 | + Ok(()) | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +fn validated_token(name: &BStr) -> Result<&BStr, Error> { | |
| 51 | + if name.find_byteset(b"<>\n").is_some() { | |
| 52 | + return Err(Error::IllegalCharacter); | |
| 53 | + } | |
| 54 | + Ok(name) | |
| 55 | +} | |
| @@ -0,0 +1,76 @@ | ||
| 1 | +use crate::mutable::{self, ser, NL}; | |
| 2 | +use bstr::{BStr, BString}; | |
| 3 | +use quick_error::quick_error; | |
| 4 | +use std::io; | |
| 5 | + | |
| 6 | +quick_error! { | |
| 7 | + /// An Error used in [`Tag::write_to()`]. | |
| 8 | + #[derive(Debug)] | |
| 9 | + pub enum Error { | |
| 10 | + StartsWithDash { | |
| 11 | + display("Tags must not start with a dash: '-'") | |
| 12 | + } | |
| 13 | + InvalidRefName(err: git_ref::validated::NameError) { | |
| 14 | + display("The tag name was no valid reference name") | |
| 15 | + from() | |
| 16 | + source(err) | |
| 17 | + } | |
| 18 | + } | |
| 19 | +} | |
| 20 | + | |
| 21 | +impl From<Error> for io::Error { | |
| 22 | + fn from(err: Error) -> Self { | |
| 23 | + io::Error::new(io::ErrorKind::Other, err) | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +/// A mutable git tag. | |
| 28 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 29 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 30 | +pub struct Tag { | |
| 31 | + /// The hash this tag is pointing to. | |
| 32 | + pub target: git_hash::ObjectId, | |
| 33 | + /// The kind of object this tag is pointing to. | |
| 34 | + pub target_kind: crate::Kind, | |
| 35 | + /// The name of the tag, e.g. "v1.0". | |
| 36 | + pub name: BString, | |
| 37 | + /// The message describing the tag. | |
| 38 | + pub message: BString, | |
| 39 | + /// The tags author. | |
| 40 | + pub signature: Option<mutable::Signature>, | |
| 41 | + /// A pgp signature over all bytes of the encoded tag, excluding the pgp signature itself. | |
| 42 | + pub pgp_signature: Option<BString>, | |
| 43 | +} | |
| 44 | + | |
| 45 | +impl Tag { | |
| 46 | + /// Writes the encoded tag to `out`. | |
| 47 | + pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { | |
| 48 | + ser::trusted_header_id(b"object", &self.target, &mut out)?; | |
| 49 | + ser::trusted_header_field(b"type", self.target_kind.to_bytes(), &mut out)?; | |
| 50 | + ser::header_field(b"tag", validated_name(self.name.as_ref())?, &mut out)?; | |
| 51 | + if let Some(tagger) = &self.signature { | |
| 52 | + ser::trusted_header_signature(b"tagger", tagger, &mut out)?; | |
| 53 | + } | |
| 54 | + | |
| 55 | + if !self.message.is_empty() { | |
| 56 | + out.write_all(NL)?; | |
| 57 | + out.write_all(&self.message)?; | |
| 58 | + } | |
| 59 | + if let Some(ref message) = self.pgp_signature { | |
| 60 | + out.write_all(NL)?; | |
| 61 | + out.write_all(&message)?; | |
| 62 | + } | |
| 63 | + Ok(()) | |
| 64 | + } | |
| 65 | +} | |
| 66 | + | |
| 67 | +fn validated_name(name: &BStr) -> Result<&BStr, Error> { | |
| 68 | + git_ref::validated::name(name)?; | |
| 69 | + if name[0] == b'-' { | |
| 70 | + return Err(Error::StartsWithDash); | |
| 71 | + } | |
| 72 | + Ok(name) | |
| 73 | +} | |
| 74 | + | |
| 75 | +#[cfg(test)] | |
| 76 | +mod tests; | |
| @@ -0,0 +1,27 @@ | ||
| 1 | +mod validated_name { | |
| 2 | + mod invalid { | |
| 3 | + use super::super::super::*; | |
| 4 | + use bstr::ByteSlice; | |
| 5 | + | |
| 6 | + #[test] | |
| 7 | + fn only_dash() { | |
| 8 | + assert!(validated_name(b"-".as_bstr()).is_err()) | |
| 9 | + } | |
| 10 | + #[test] | |
| 11 | + fn leading_dash() { | |
| 12 | + assert!(validated_name(b"-hello".as_bstr()).is_err()) | |
| 13 | + } | |
| 14 | + } | |
| 15 | + | |
| 16 | + mod valid { | |
| 17 | + use super::super::super::*; | |
| 18 | + use bstr::ByteSlice; | |
| 19 | + | |
| 20 | + #[test] | |
| 21 | + fn version() { | |
| 22 | + for version in &["v1.0.0", "0.2.1", "0-alpha1"] { | |
| 23 | + assert!(validated_name(version.as_bytes().as_bstr()).is_ok()) | |
| 24 | + } | |
| 25 | + } | |
| 26 | + } | |
| 27 | +} | |
| @@ -0,0 +1,76 @@ | ||
| 1 | +use crate::{mutable::SPACE, tree::Mode}; | |
| 2 | +use bstr::{BString, ByteSlice}; | |
| 3 | +use quick_error::quick_error; | |
| 4 | +use std::io; | |
| 5 | + | |
| 6 | +quick_error! { | |
| 7 | + /// The Error used in [`Tree::write_to()`]. | |
| 8 | + #[derive(Debug)] | |
| 9 | + #[allow(missing_docs)] | |
| 10 | + pub enum Error { | |
| 11 | + NewlineInFilename(name: BString) { | |
| 12 | + display("Newlines are invalid in file paths: {:?}", name) | |
| 13 | + } | |
| 14 | + } | |
| 15 | +} | |
| 16 | + | |
| 17 | +impl From<Error> for io::Error { | |
| 18 | + fn from(err: Error) -> Self { | |
| 19 | + io::Error::new(io::ErrorKind::Other, err) | |
| 20 | + } | |
| 21 | +} | |
| 22 | + | |
| 23 | +/// A mutable Tree, containing other trees, blobs or commits. | |
| 24 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 25 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 26 | +pub struct Tree { | |
| 27 | + /// The directories and files contained in this tree. | |
| 28 | + pub entries: Vec<Entry>, | |
| 29 | +} | |
| 30 | + | |
| 31 | +/// An entry in a [`Tree`], similar to an entry in a directory. | |
| 32 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 33 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 34 | +pub struct Entry { | |
| 35 | + /// The kind of object to which `oid` is pointing to. | |
| 36 | + pub mode: Mode, | |
| 37 | + /// The name of the file in the parent tree. | |
| 38 | + pub filename: BString, | |
| 39 | + /// The id of the object representing the entry. | |
| 40 | + pub oid: git_hash::ObjectId, | |
| 41 | +} | |
| 42 | + | |
| 43 | +/// Serialization | |
| 44 | +impl Mode { | |
| 45 | + /// Return the representation as used in the git internal format. | |
| 46 | + pub fn as_bytes(&self) -> &'static [u8] { | |
| 47 | + use Mode::*; | |
| 48 | + match self { | |
| 49 | + Tree => b"40000", | |
| 50 | + Blob => b"100644", | |
| 51 | + BlobExecutable => b"100755", | |
| 52 | + Link => b"120000", | |
| 53 | + Commit => b"160000", | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +/// Serialization | |
| 59 | +impl Tree { | |
| 60 | + /// Serialize this tree to `out` in the git internal format. | |
| 61 | + pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { | |
| 62 | + for Entry { mode, filename, oid } in &self.entries { | |
| 63 | + out.write_all(mode.as_bytes())?; | |
| 64 | + out.write_all(SPACE)?; | |
| 65 | + | |
| 66 | + if filename.find_byte(b'\n').is_some() { | |
| 67 | + return Err(Error::NewlineInFilename(filename.to_owned()).into()); | |
| 68 | + } | |
| 69 | + out.write_all(&filename)?; | |
| 70 | + out.write_all(&[b'\0'])?; | |
| 71 | + | |
| 72 | + out.write_all(oid.as_bytes())?; | |
| 73 | + } | |
| 74 | + Ok(()) | |
| 75 | + } | |
| 76 | +} | |