Skip to main content

radicle_git_ext/
blob.rs

1use std::{borrow::Cow, path::Path};
2
3use radicle_std_ext::result::ResultExt as _;
4use thiserror::Error;
5
6use crate::{error::is_not_found_err, revwalk};
7
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum Error {
11    #[error(transparent)]
12    NotFound(#[from] NotFound),
13
14    #[error(transparent)]
15    Git(#[from] git2::Error),
16}
17
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum NotFound {
21    #[error("blob with path {0} not found")]
22    NoSuchBlob(String),
23
24    #[error("branch {0} not found")]
25    NoSuchBranch(String),
26
27    #[error("object {0} not found")]
28    NoSuchObject(git2::Oid),
29
30    #[error("the supplied git2::Reference does not have a target")]
31    NoRefTarget,
32}
33
34pub enum Branch<'a> {
35    Name(Cow<'a, str>),
36    Ref(git2::Reference<'a>),
37}
38
39impl<'a> From<&'a str> for Branch<'a> {
40    fn from(s: &'a str) -> Self {
41        Self::Name(Cow::Borrowed(s))
42    }
43}
44
45impl From<String> for Branch<'_> {
46    fn from(s: String) -> Self {
47        Self::Name(Cow::Owned(s))
48    }
49}
50
51impl<'a> From<git2::Reference<'a>> for Branch<'a> {
52    fn from(r: git2::Reference<'a>) -> Self {
53        Self::Ref(r)
54    }
55}
56
57/// Conveniently read a [`git2::Blob`] from a starting point.
58pub enum Blob<'a> {
59    /// Look up the tip of the reference specified by [`Branch`], peel until a
60    /// tree is found, and traverse the tree along the given [`Path`] until
61    /// the blob is found.
62    Tip { branch: Branch<'a>, path: &'a Path },
63    /// Traverse the history from the tip of [`Branch`] along the first parent
64    /// until a commit without parents is found. Try to get the blob in that
65    /// commit's tree at [`Path`].
66    Init { branch: Branch<'a>, path: &'a Path },
67    /// Look up `object`, peel until a tree is found, and try to get at the blob
68    /// at [`Path`].
69    At { object: git2::Oid, path: &'a Path },
70}
71
72impl<'a> Blob<'a> {
73    pub fn get(self, git: &'a git2::Repository) -> Result<git2::Blob<'a>, Error> {
74        match self {
75            Self::Tip { branch, path } => {
76                let reference = match branch {
77                    Branch::Name(name) => {
78                        git.find_reference(&name).or_matches(is_not_found_err, || {
79                            Err(Error::NotFound(NotFound::NoSuchBranch(name.into_owned())))
80                        })
81                    }
82
83                    Branch::Ref(reference) => Ok(reference),
84                }?;
85                let tree = reference.peel_to_tree()?;
86                blob(git, tree, path)
87            }
88
89            Self::Init { branch, path } => {
90                let start = match branch {
91                    Branch::Name(name) => Ok(revwalk::Start::Ref(name.to_string())),
92                    Branch::Ref(reference) => {
93                        match (reference.target(), reference.symbolic_target()) {
94                            (Some(oid), _) => Ok(revwalk::Start::Oid(oid)),
95                            (_, Ok(Some(sym))) => Ok(revwalk::Start::Ref(sym.to_string())),
96                            (_, Ok(None)) => Err(Error::NotFound(NotFound::NoRefTarget)),
97                            (_, Err(err)) => Err(Error::Git(err)),
98                        }
99                    }
100                }?;
101
102                let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
103                match revwalk.into_iter().next() {
104                    None => Err(Error::NotFound(NotFound::NoSuchBlob(
105                        path.display().to_string(),
106                    ))),
107                    Some(oid) => {
108                        let oid = oid?;
109                        let tree = git.find_commit(oid)?.tree()?;
110                        blob(git, tree, path)
111                    }
112                }
113            }
114
115            Self::At { object, path } => {
116                let tree = git
117                    .find_object(object, None)
118                    .or_matches(is_not_found_err, || {
119                        Err(Error::NotFound(NotFound::NoSuchObject(object)))
120                    })
121                    .and_then(|obj| Ok(obj.peel_to_tree()?))?;
122                blob(git, tree, path)
123            }
124        }
125    }
126}
127
128fn blob<'a>(
129    repo: &'a git2::Repository,
130    tree: git2::Tree<'a>,
131    path: &'a Path,
132) -> Result<git2::Blob<'a>, Error> {
133    let entry = tree.get_path(path).or_matches(is_not_found_err, || {
134        Err(Error::NotFound(NotFound::NoSuchBlob(
135            path.display().to_string(),
136        )))
137    })?;
138
139    entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
140}