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                            (_, Some(sym)) => Ok(revwalk::Start::Ref(sym.to_string())),
96                            (_, _) => Err(Error::NotFound(NotFound::NoRefTarget)),
97                        }
98                    }
99                }?;
100
101                let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
102                match revwalk.into_iter().next() {
103                    None => Err(Error::NotFound(NotFound::NoSuchBlob(
104                        path.display().to_string(),
105                    ))),
106                    Some(oid) => {
107                        let oid = oid?;
108                        let tree = git.find_commit(oid)?.tree()?;
109                        blob(git, tree, path)
110                    }
111                }
112            }
113
114            Self::At { object, path } => {
115                let tree = git
116                    .find_object(object, None)
117                    .or_matches(is_not_found_err, || {
118                        Err(Error::NotFound(NotFound::NoSuchObject(object)))
119                    })
120                    .and_then(|obj| Ok(obj.peel_to_tree()?))?;
121                blob(git, tree, path)
122            }
123        }
124    }
125}
126
127fn blob<'a>(
128    repo: &'a git2::Repository,
129    tree: git2::Tree<'a>,
130    path: &'a Path,
131) -> Result<git2::Blob<'a>, Error> {
132    let entry = tree.get_path(path).or_matches(is_not_found_err, || {
133        Err(Error::NotFound(NotFound::NoSuchBlob(
134            path.display().to_string(),
135        )))
136    })?;
137
138    entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
139}