1use 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
62pub enum Blob<'a> {
64 Tip { branch: Branch<'a>, path: &'a Path },
68 Init { branch: Branch<'a>, path: &'a Path },
72 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}