1use std::{
2 convert::TryFrom,
3 str::{self, FromStr},
4};
5
6use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};
7
8use crate::refs::refstr_join;
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum Branch {
17 Local(Local),
18 Remote(Remote),
19}
20
21impl Branch {
22 pub fn local<R>(name: R) -> Self
24 where
25 R: AsRef<RefStr>,
26 {
27 Self::Local(Local::new(name))
28 }
29
30 pub fn remote<R>(remote: Component<'_>, name: R) -> Self
34 where
35 R: AsRef<RefStr>,
36 {
37 Self::Remote(Remote::new(remote, name))
38 }
39
40 pub fn short_name(&self) -> &RefString {
43 match self {
44 Branch::Local(local) => local.short_name(),
45 Branch::Remote(remote) => remote.short_name(),
46 }
47 }
48
49 pub fn refname(&self) -> Qualified {
53 match self {
54 Branch::Local(local) => local.refname(),
55 Branch::Remote(remote) => remote.refname(),
56 }
57 }
58}
59
60impl TryFrom<&git2::Reference<'_>> for Branch {
61 type Error = error::Branch;
62
63 fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
64 let name = str::from_utf8(reference.name_bytes())?;
65 Self::from_str(name)
66 }
67}
68
69impl TryFrom<&str> for Branch {
70 type Error = error::Branch;
71
72 fn try_from(name: &str) -> Result<Self, Self::Error> {
73 Self::from_str(name)
74 }
75}
76
77impl FromStr for Branch {
78 type Err = error::Branch;
79
80 fn from_str(name: &str) -> Result<Self, Self::Err> {
81 let name = RefStr::try_from_str(name)?;
82 let name = match name.to_namespaced() {
83 None => name
84 .qualified()
85 .ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
86 Some(name) => name.strip_namespace_recursive(),
87 };
88
89 let (_ref, category, c, cs) = name.non_empty_components();
90
91 if category == component::HEADS {
92 Ok(Self::Local(Local::new(refstr_join(c, cs))))
93 } else if category == component::REMOTES {
94 Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
95 } else {
96 Err(error::Branch::InvalidName(name.into()))
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct Local {
109 name: RefString,
110}
111
112impl Local {
113 pub(crate) fn new<R>(name: R) -> Self
119 where
120 R: AsRef<RefStr>,
121 {
122 match name.as_ref().qualified() {
123 None => Self {
124 name: name.as_ref().to_ref_string(),
125 },
126 Some(qualified) => {
127 let (_refs, heads, c, cs) = qualified.non_empty_components();
128 if heads == component::HEADS {
129 Self {
130 name: refstr_join(c, cs),
131 }
132 } else {
133 Self {
134 name: name.as_ref().to_ref_string(),
135 }
136 }
137 }
138 }
139 }
140
141 pub fn short_name(&self) -> &RefString {
144 &self.name
145 }
146
147 pub fn refname(&self) -> Qualified {
150 lit::refs_heads(&self.name).into()
151 }
152}
153
154impl TryFrom<&git2::Reference<'_>> for Local {
155 type Error = error::Local;
156
157 fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
158 let name = str::from_utf8(reference.name_bytes())?;
159 Self::from_str(name)
160 }
161}
162
163impl TryFrom<&str> for Local {
164 type Error = error::Local;
165
166 fn try_from(name: &str) -> Result<Self, Self::Error> {
167 Self::from_str(name)
168 }
169}
170
171impl FromStr for Local {
172 type Err = error::Local;
173
174 fn from_str(name: &str) -> Result<Self, Self::Err> {
175 let name = RefStr::try_from_str(name)?;
176 let name = match name.to_namespaced() {
177 None => name
178 .qualified()
179 .ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
180 Some(name) => name.strip_namespace_recursive(),
181 };
182
183 let (_ref, heads, c, cs) = name.non_empty_components();
184 if heads == component::HEADS {
185 Ok(Self::new(refstr_join(c, cs)))
186 } else {
187 Err(error::Local::NotHeads(name.into()))
188 }
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub struct Remote {
200 remote: RefString,
201 name: RefString,
202}
203
204impl Remote {
205 pub(crate) fn new<R>(remote: Component, name: R) -> Self
216 where
217 R: AsRef<RefStr>,
218 {
219 Self {
220 name: name.as_ref().to_ref_string(),
221 remote: remote.to_ref_string(),
222 }
223 }
224
225 pub fn from_refs_remotes<R>(name: R) -> Option<Self>
229 where
230 R: AsRef<RefStr>,
231 {
232 let qualified = name.as_ref().qualified()?;
233 let (_refs, remotes, remote, cs) = qualified.non_empty_components();
234 (remotes == component::REMOTES).then_some(Self {
235 name: cs.collect(),
236 remote: remote.to_ref_string(),
237 })
238 }
239
240 pub fn short_name(&self) -> &RefString {
243 &self.name
244 }
245
246 pub fn remote(&self) -> &RefString {
249 &self.remote
250 }
251
252 pub fn refname(&self) -> Qualified {
255 lit::refs_remotes(self.remote.join(&self.name)).into()
256 }
257}
258
259impl TryFrom<&git2::Reference<'_>> for Remote {
260 type Error = error::Remote;
261
262 fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
263 let name = str::from_utf8(reference.name_bytes())?;
264 Self::from_str(name)
265 }
266}
267
268impl TryFrom<&str> for Remote {
269 type Error = error::Remote;
270
271 fn try_from(name: &str) -> Result<Self, Self::Error> {
272 Self::from_str(name)
273 }
274}
275
276impl FromStr for Remote {
277 type Err = error::Remote;
278
279 fn from_str(name: &str) -> Result<Self, Self::Err> {
280 let name = RefStr::try_from_str(name)?;
281 let name = match name.to_namespaced() {
282 None => name
283 .qualified()
284 .ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
285 Some(name) => name.strip_namespace_recursive(),
286 };
287
288 let (_ref, remotes, remote, cs) = name.non_empty_components();
289 if remotes == component::REMOTES {
290 Ok(Self::new(remote, cs.collect::<RefString>()))
291 } else {
292 Err(error::Remote::NotRemotes(name.into()))
293 }
294 }
295}
296
297pub mod error {
298 use radicle_git_ext::ref_format::{self, RefString};
299 use thiserror::Error;
300
301 #[derive(Debug, Error)]
302 pub enum Branch {
303 #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
304 InvalidName(RefString),
305 #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
306 NotQualified(String),
307 #[error(transparent)]
308 RefFormat(#[from] ref_format::Error),
309 #[error(transparent)]
310 Utf8(#[from] std::str::Utf8Error),
311 }
312
313 #[derive(Debug, Error)]
314 pub enum Local {
315 #[error("the refname '{0}' did not begin with 'refs/heads'")]
316 NotHeads(RefString),
317 #[error("the refname '{0}' did not begin with 'refs/heads'")]
318 NotQualified(String),
319 #[error(transparent)]
320 RefFormat(#[from] ref_format::Error),
321 #[error(transparent)]
322 Utf8(#[from] std::str::Utf8Error),
323 }
324
325 #[derive(Debug, Error)]
326 pub enum Remote {
327 #[error("the refname '{0}' did not begin with 'refs/remotes'")]
328 NotQualified(String),
329 #[error("the refname '{0}' did not begin with 'refs/remotes'")]
330 NotRemotes(RefString),
331 #[error(transparent)]
332 RefFormat(#[from] ref_format::Error),
333 #[error(transparent)]
334 Utf8(#[from] std::str::Utf8Error),
335 }
336}