radicle_git_ext/
blob.rs

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