diff --git a/radicle-surf/src/repo.rs b/radicle-surf/src/repo.rs index 8a1fc37..6c9edc1 100644 --- a/radicle-surf/src/repo.rs +++ b/radicle-surf/src/repo.rs @@ -48,6 +48,7 @@ use crate::{ /// Enumeration of errors that can occur in repo operations. pub mod error { + use crate::Oid; use std::path::PathBuf; use thiserror::Error; @@ -56,6 +57,8 @@ pub mod error { pub enum Repo { #[error("path not found for: {0}")] PathNotFound(PathBuf), + #[error("blob not found for: {0}")] + BlobNotFound(Oid), } } @@ -301,6 +304,20 @@ impl Repository { Ok(Blob::>::new(file.id(), git2_blob, last_commit)) } + /// Retrieves the blob with `oid` in `commit`. + pub fn blob_at<'a, C: ToCommit>( + &'a self, + commit: C, + oid: Oid, + ) -> Result>, Error> { + let commit = commit + .to_commit(self) + .map_err(|e| Error::ToCommit(e.into()))?; + let git2_blob = self.find_blob(oid)?; + let last_commit = self.find_commit_of_blob(oid, &commit)?; + Ok(Blob::>::new(oid, git2_blob, last_commit)) + } + /// Returns the last commit, if exists, for a `path` in the history of /// `rev`. pub fn last_commit(&self, path: &P, rev: C) -> Result, Error> @@ -524,6 +541,53 @@ impl Repository { Ok(diff) } + /// Returns true if the diff between `from` and `to` creates `oid`. + fn diff_commits_has_oid( + &self, + from: Option<&git2::Commit>, + to: &git2::Commit, + oid: &git2::Oid, + ) -> Result { + let diff = self.diff_commits(None, from, to)?; + for delta in diff.deltas() { + if &delta.new_file().id() == oid { + return Ok(true); + } + } + Ok(false) + } + + /// Returns whether `oid` was created in `commit` or not. + fn is_oid_in_commit(&self, oid: Oid, commit: &git2::Commit) -> Result { + if commit.parent_count() == 0 { + return self.diff_commits_has_oid(None, commit, oid.as_ref()); + } + + for p in commit.parents() { + if self.diff_commits_has_oid(Some(&p), commit, oid.as_ref())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Returns the commit that created the blob with `oid`. + /// + /// It is assumed that `oid` exists in `head`. + fn find_commit_of_blob(&self, oid: Oid, head: &Commit) -> Result { + let mut revwalk = self.revwalk()?; + revwalk.push(head.id.into())?; + for commit_id in revwalk { + let commit_id = commit_id?; + let git2_commit = self.inner.find_commit(commit_id)?; + if self.is_oid_in_commit(oid, &git2_commit)? { + return Ok(Commit::try_from(git2_commit)?); + } + } + Err(Error::Repo(error::Repo::BlobNotFound(oid))) + } + /// Returns a full reference name with namespace(s) included. pub(crate) fn namespaced_refname<'a>( &'a self, diff --git a/radicle-surf/t/src/source.rs b/radicle-surf/t/src/source.rs index 7bd4ab0..44609d5 100644 --- a/radicle-surf/t/src/source.rs +++ b/radicle-surf/t/src/source.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use radicle_git_ext::ref_format::refname; -use radicle_surf::{Branch, Glob, Repository}; +use radicle_surf::{Branch, Glob, Oid, Repository}; use serde_json::json; const GIT_PLATINUM: &str = "../data/git-platinum"; @@ -175,6 +175,32 @@ fn repo_blob() { assert_eq!(json_ref, json_owned); } +#[test] +fn repo_blob_at() { + let repo = Repository::open(GIT_PLATINUM).unwrap(); + let oid = Oid::try_from("b84992d24be67536837f5ab45a943f1b3f501878").unwrap(); + + // Retrieve the blob using its oid. + let blob = repo + .blob_at("27acd68c7504755aa11023300890bb85bbd69d45", oid) + .unwrap(); + + // Verify the blob oid. + let blob_oid = blob.object_id(); + assert_eq!(blob_oid, oid); + + // Verify the commit that created the blob. + let blob_commit = blob.commit(); + assert_eq!( + blob_commit.id.to_string(), + "e24124b7538658220b5aaf3b6ef53758f0a106dc" + ); + + // Verify the blob content ("memory.rs"). + assert!(!blob.is_binary()); + assert_eq!(blob.size(), 6253); +} + #[test] fn tree_ordering() { let repo = Repository::open(GIT_PLATINUM).unwrap();