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 (_, 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}