From 9f4dce4ffd00e4fc1bb6ceae7e0d72410e5877e1 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 18 Jan 2023 16:42:06 -0800 Subject: [PATCH] change BlobContent to BlobRef that borrows its content from git2::Blob The motivation is to avoid copying the content bytes that could be potentially quite large. Also support Blob::to_owned() just in case. Signed-off-by: Han Xu --- radicle-surf/src/blob.rs | 163 +++++++++++++++++++++++------------ radicle-surf/src/repo.rs | 12 ++- radicle-surf/t/src/source.rs | 13 +++ 3 files changed, 130 insertions(+), 58 deletions(-) diff --git a/radicle-surf/src/blob.rs b/radicle-surf/src/blob.rs index f449dca..1bdd2ed 100644 --- a/radicle-surf/src/blob.rs +++ b/radicle-surf/src/blob.rs @@ -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 { 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 Blob { 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> { + /// 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 { + Blob::::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> { fn serialize(&self, serializer: S) -> Result 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), +/// 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(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_bytes(self.as_bytes(), serializer) + } +} + +/// Represents a blob with owned content bytes. +pub struct BlobVec { + inner: Vec, +} + +impl BlobVec { + pub fn size(&self) -> usize { + self.inner.len() + } + + pub fn as_bytes(&self) -> &[u8] { + self.inner.as_ref() + } +} + +impl Blob { + pub(crate) fn new(id: Oid, bytes: Vec, 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(&self, serializer: S) -> Result 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 { + fn serialize(&self, serializer: S) -> Result + 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(bytes: &[u8], serializer: S) -> Result +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) + }, } } diff --git a/radicle-surf/src/repo.rs b/radicle-surf/src/repo.rs index 2f0e2f9..e972c8d 100644 --- a/radicle-surf/src/repo.rs +++ b/radicle-surf/src/repo.rs @@ -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}, @@ -258,7 +258,11 @@ impl Repository { } /// Returns a [`Blob`] for `path` in `commit`. - pub fn blob>(&self, commit: C, path: &P) -> Result { + pub fn blob<'a, C: ToCommit, P: AsRef>( + &'a self, + commit: C, + path: &P, + ) -> Result>, Error> { let commit = commit .to_commit(self) .map_err(|e| Error::ToCommit(e.into()))?; @@ -266,8 +270,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::>::new(file.id(), git2_blob, last_commit)) } /// Returns the last commit, if exists, for a `path` in the history of diff --git a/radicle-surf/t/src/source.rs b/radicle-surf/t/src/source.rs index a658ede..e634c73 100644 --- a/radicle-surf/t/src/source.rs +++ b/radicle-surf/t/src/source.rs @@ -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]