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
57pub enum Blob<'a> {
59 Tip { branch: Branch<'a>, path: &'a Path },
63 Init { branch: Branch<'a>, path: &'a Path },
67 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}