Merge remote-tracking branch 'han/rm-blob-content'

* han/rm-blob-content:
  change BlobContent to BlobRef that borrows its content from git2::Blob

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2023-02-01 10:08:35 +00:00
3 changed files with 130 additions and 58 deletions
+109 -54
View File
@@ -18,8 +18,6 @@
//! Represents git object type 'blob', i.e. actual file contents.
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
use std::str;
use radicle_git_ext::Oid;
#[cfg(feature = "serde")]
use serde::{
@@ -30,45 +28,60 @@ use serde::{
use crate::Commit;
/// Represents a git blob object.
pub struct Blob {
///
/// The type parameter `T` could be [`BlobRef`] or [`BlobVec`].
pub struct Blob<T> {
id: Oid,
content: BlobContent,
is_binary: bool,
commit: Commit,
content: T,
}
impl Blob {
/// Returns the [`Blob`] for a file at `revision` under `path`.
pub(crate) fn new(id: Oid, content: &[u8], commit: Commit) -> Self {
let content = BlobContent::from(content);
Self {
id,
content,
commit,
}
}
/// Indicates if the content of the [`Blob`] is binary.
#[must_use]
pub fn is_binary(&self) -> bool {
matches!(self.content, BlobContent::Binary(_))
}
impl<T> Blob<T> {
pub fn object_id(&self) -> Oid {
self.id
}
pub fn content(&self) -> &BlobContent {
&self.content
pub fn is_binary(&self) -> bool {
self.is_binary
}
/// Returns the commit that created this blob.
pub fn commit(&self) -> &Commit {
&self.commit
}
pub fn content(&self) -> &T {
&self.content
}
}
impl<'a> Blob<BlobRef<'a>> {
/// Returns the [`Blob`] wrapping around an underlying `git2::Blob`.
pub(crate) fn new(id: Oid, git2_blob: git2::Blob<'a>, commit: Commit) -> Self {
let is_binary = git2_blob.is_binary();
let content = BlobRef { inner: git2_blob };
Self {
id,
is_binary,
content,
commit,
}
}
/// Converts into a `Blob` with owned content bytes.
pub fn to_owned(&self) -> Blob<BlobVec> {
Blob::<BlobVec>::new(
self.id,
self.content.as_bytes().to_vec(),
self.commit.clone(),
self.is_binary,
)
}
}
#[cfg(feature = "serde")]
impl Serialize for Blob {
impl<'a> Serialize for Blob<BlobRef<'a>> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -82,54 +95,96 @@ impl Serialize for Blob {
}
}
/// Variants of blob content.
#[derive(PartialEq, Eq)]
pub enum BlobContent {
/// Content is plain text and can be passed as a string.
Plain(String),
/// Content is binary and needs special treatment.
Binary(Vec<u8>),
/// Represents a blob with borrowed content bytes.
pub struct BlobRef<'a> {
inner: git2::Blob<'a>,
}
impl BlobContent {
/// Returns the size of this `BlobContent`.
impl<'a> BlobRef<'a> {
/// Returns the size of the blob content.
pub fn size(&self) -> usize {
match self {
Self::Plain(content) => content.len(),
Self::Binary(bytes) => bytes.len(),
}
self.inner.size()
}
/// Returns the content as bytes.
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Plain(content) => content.as_bytes(),
Self::Binary(bytes) => &bytes[..],
self.inner.content()
}
}
#[cfg(feature = "serde")]
impl<'a> Serialize for BlobRef<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_bytes(self.as_bytes(), serializer)
}
}
/// Represents a blob with owned content bytes.
pub struct BlobVec {
inner: Vec<u8>,
}
impl BlobVec {
pub fn size(&self) -> usize {
self.inner.len()
}
pub fn as_bytes(&self) -> &[u8] {
self.inner.as_ref()
}
}
impl Blob<BlobVec> {
pub(crate) fn new(id: Oid, bytes: Vec<u8>, commit: Commit, is_binary: bool) -> Self {
let content = BlobVec { inner: bytes };
Self {
id,
is_binary,
content,
commit,
}
}
}
#[cfg(feature = "serde")]
impl Serialize for BlobContent {
impl Serialize for BlobVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Plain(content) => serializer.serialize_str(content),
Self::Binary(bytes) => {
let encoded = base64::encode(bytes);
serializer.serialize_str(&encoded)
},
}
serialize_bytes(self.as_bytes(), serializer)
}
}
impl From<&[u8]> for BlobContent {
fn from(bytes: &[u8]) -> Self {
match str::from_utf8(bytes) {
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
Err(_) => BlobContent::Binary(bytes.to_owned()),
}
#[cfg(feature = "serde")]
impl Serialize for Blob<BlobVec> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
const FIELDS: usize = 5;
let mut state = serializer.serialize_struct("Blob", FIELDS)?;
state.serialize_field("binary", &self.is_binary())?;
state.serialize_field("content", &self.content)?;
state.serialize_field("lastCommit", &self.commit)?;
state.end()
}
}
/// Common serialization for a `Blob`'s content bytes.
#[cfg(feature = "serde")]
fn serialize_bytes<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match std::str::from_utf8(bytes) {
Ok(s) => serializer.serialize_str(s),
Err(_) => {
let encoded = base64::encode(bytes);
serializer.serialize_str(&encoded)
},
}
}
+8 -4
View File
@@ -26,7 +26,7 @@ use git_ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString};
use radicle_git_ext::Oid;
use crate::{
blob::Blob,
blob::{Blob, BlobRef},
diff::Diff,
fs::{Directory, File, FileContent},
refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
@@ -261,7 +261,11 @@ impl Repository {
}
/// Returns a [`Blob`] for `path` in `commit`.
pub fn blob<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Blob, Error> {
pub fn blob<'a, C: ToCommit, P: AsRef<Path>>(
&'a self,
commit: C,
path: &P,
) -> Result<Blob<BlobRef<'a>>, Error> {
let commit = commit
.to_commit(self)
.map_err(|e| Error::ToCommit(e.into()))?;
@@ -269,8 +273,8 @@ impl Repository {
let last_commit = self
.last_commit(path, commit)?
.ok_or_else(|| error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
let content = file.content(self)?;
Ok(Blob::new(file.id(), content.as_bytes(), last_commit))
let git2_blob = self.find_blob(file.id())?;
Ok(Blob::<BlobRef<'a>>::new(file.id(), git2_blob, last_commit))
}
/// Returns the last commit, if exists, for a `path` in the history of
+13
View File
@@ -163,6 +163,19 @@ fn repo_blob() {
// Verify as_bytes.
assert_eq!(content.as_bytes().len(), content.size());
// Verify to_owned().
let blob_owned = blob.to_owned();
assert_eq!(blob_owned.content().size(), 6253);
assert_eq!(blob_owned.content().as_bytes(), content.as_bytes());
// Verify JSON output is the same.
let json_ref = json!({ "content": content }).to_string();
let json_owned = json!( {
"content": blob_owned.content()
})
.to_string();
assert_eq!(json_ref, json_owned);
}
#[test]