1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::string::FromUtf8Error;
4
5use thiserror::Error;
6use tracing::{instrument, warn};
7
8use crate::git::config::ConfigRead;
9use crate::git::oid::make_non_zero_oid;
10use crate::git::repo::{Error, Result};
11use crate::git::{Commit, MaybeZeroOid, NonZeroOid, Repo};
12
13#[derive(Debug, PartialEq, Eq)]
15pub enum ReferenceTarget<'a> {
16 Direct {
19 oid: MaybeZeroOid,
21 },
22
23 Symbolic {
25 reference_name: Cow<'a, OsStr>,
27 },
28}
29
30#[derive(Debug, Error)]
31pub enum ReferenceNameError {
32 #[error("reference name was not valid UTF-8: {0}")]
33 InvalidUtf8(FromUtf8Error),
34}
35
36#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
38pub struct ReferenceName(String);
39
40impl ReferenceName {
41 pub fn from_bytes(bytes: Vec<u8>) -> std::result::Result<ReferenceName, ReferenceNameError> {
43 let reference_name = String::from_utf8(bytes).map_err(ReferenceNameError::InvalidUtf8)?;
44 Ok(Self(reference_name))
45 }
46
47 pub fn as_str(&self) -> &str {
49 let Self(reference_name) = self;
50 reference_name
51 }
52}
53
54impl From<&str> for ReferenceName {
55 fn from(s: &str) -> Self {
56 ReferenceName(s.to_owned())
57 }
58}
59
60impl From<String> for ReferenceName {
61 fn from(s: String) -> Self {
62 ReferenceName(s)
63 }
64}
65
66impl From<NonZeroOid> for ReferenceName {
67 fn from(oid: NonZeroOid) -> Self {
68 Self::from(oid.to_string())
69 }
70}
71
72impl From<MaybeZeroOid> for ReferenceName {
73 fn from(oid: MaybeZeroOid) -> Self {
74 Self::from(oid.to_string())
75 }
76}
77
78impl AsRef<str> for ReferenceName {
79 fn as_ref(&self) -> &str {
80 &self.0
81 }
82}
83
84pub struct Reference<'repo> {
86 pub(super) inner: git2::Reference<'repo>,
87}
88
89impl std::fmt::Debug for Reference<'_> {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self.inner.name() {
92 Some(name) => write!(f, "<Reference name={name:?}>"),
93 None => write!(f, "<Reference name={:?}>", self.inner.name_bytes()),
94 }
95 }
96}
97
98impl<'repo> Reference<'repo> {
99 pub fn is_valid_name(name: &str) -> bool {
101 git2::Reference::is_valid_name(name)
102 }
103
104 #[instrument]
106 pub fn get_name(&self) -> Result<ReferenceName> {
107 let name = ReferenceName::from_bytes(self.inner.name_bytes().to_vec())?;
108 Ok(name)
109 }
110 #[instrument]
113 pub fn peel_to_commit(&self) -> Result<Option<Commit<'repo>>> {
114 let object = match self.inner.peel(git2::ObjectType::Commit) {
115 Ok(object) => object,
116 Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(None),
117 Err(err) => return Err(Error::ResolveReference(err)),
118 };
119 match object.into_commit() {
120 Ok(commit) => Ok(Some(Commit { inner: commit })),
121 Err(_) => Ok(None),
122 }
123 }
124
125 #[instrument]
127 pub fn delete(&mut self) -> Result<()> {
128 self.inner.delete().map_err(Error::DeleteReference)?;
129 Ok(())
130 }
131}
132
133#[derive(Debug)]
142pub enum CategorizedReferenceName<'a> {
143 LocalBranch {
145 name: &'a str,
147
148 prefix: &'static str,
150 },
151
152 RemoteBranch {
154 name: &'a str,
156
157 prefix: &'static str,
159 },
160
161 OtherRef {
163 name: &'a str,
165 },
166}
167
168impl<'a> CategorizedReferenceName<'a> {
169 pub fn new(name: &'a ReferenceName) -> Self {
171 let name = name.as_str();
172 if name.starts_with("refs/heads/") {
173 Self::LocalBranch {
174 name,
175 prefix: "refs/heads/",
176 }
177 } else if name.starts_with("refs/remotes/") {
178 Self::RemoteBranch {
179 name,
180 prefix: "refs/remotes/",
181 }
182 } else {
183 Self::OtherRef { name }
184 }
185 }
186
187 pub fn render_full(&self) -> String {
190 let name = match self {
191 Self::LocalBranch { name, prefix: _ } => name,
192 Self::RemoteBranch { name, prefix: _ } => name,
193 Self::OtherRef { name } => name,
194 };
195 (*name).to_owned()
196 }
197
198 pub fn render_suffix(&self) -> String {
202 let (name, prefix): (_, &'static str) = match self {
203 Self::LocalBranch { name, prefix } => (name, prefix),
204 Self::RemoteBranch { name, prefix } => (name, prefix),
205 Self::OtherRef { name } => (name, ""),
206 };
207 name.strip_prefix(prefix).unwrap_or(name).to_owned()
208 }
209
210 pub fn friendly_describe(&self) -> String {
213 let name = self.render_suffix();
214 match self {
215 CategorizedReferenceName::LocalBranch { .. } => {
216 format!("branch {name}")
217 }
218 CategorizedReferenceName::RemoteBranch { .. } => {
219 format!("remote branch {name}")
220 }
221 CategorizedReferenceName::OtherRef { .. } => format!("ref {name}"),
222 }
223 }
224}
225
226pub type BranchType = git2::BranchType;
228
229pub struct Branch<'repo> {
231 pub(super) repo: &'repo Repo,
232 pub(super) inner: git2::Branch<'repo>,
233}
234
235impl std::fmt::Debug for Branch<'_> {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 write!(
238 f,
239 "<Branch name={:?}>",
240 String::from_utf8_lossy(
241 self.inner
242 .name_bytes()
243 .unwrap_or(b"(could not get branch name)")
244 ),
245 )
246 }
247}
248
249impl<'repo> Branch<'repo> {
250 pub fn get_oid(&self) -> Result<Option<NonZeroOid>> {
253 Ok(self.inner.get().target().map(make_non_zero_oid))
254 }
255
256 #[instrument]
259 pub fn get_name(&self) -> eyre::Result<&str> {
260 self.inner
261 .name()?
262 .ok_or_else(|| eyre::eyre!("Could not decode branch name"))
263 }
264
265 #[instrument]
268 pub fn get_reference_name(&self) -> eyre::Result<ReferenceName> {
269 let reference_name = self
270 .inner
271 .get()
272 .name()
273 .ok_or_else(|| eyre::eyre!("Could not decode branch reference name"))?;
274 Ok(ReferenceName(reference_name.to_owned()))
275 }
276
277 #[instrument]
279 pub fn get_upstream_branch(&self) -> Result<Option<Branch<'repo>>> {
280 match self.inner.upstream() {
281 Ok(upstream) => Ok(Some(Branch {
282 repo: self.repo,
283 inner: upstream,
284 })),
285 Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
286 Err(err) => {
287 let branch_name = self.inner.name_bytes().map_err(|_err| Error::DecodeUtf8 {
288 item: "branch name",
289 })?;
290 Err(Error::FindUpstreamBranch {
291 source: err,
292 name: String::from_utf8_lossy(branch_name).into_owned(),
293 })
294 }
295 }
296 }
297
298 #[instrument]
301 pub fn get_upstream_branch_target(&self) -> eyre::Result<Option<NonZeroOid>> {
302 let upstream_branch = match self.get_upstream_branch()? {
303 Some(upstream_branch) => upstream_branch,
304 None => return Ok(None),
305 };
306 let target_oid = upstream_branch.get_oid()?;
307 Ok(target_oid)
308 }
309
310 pub fn get_upstream_branch_name_without_push_remote_name(
315 &self,
316 ) -> eyre::Result<Option<String>> {
317 let push_remote_name = match self.get_push_remote_name()? {
318 Some(stack_remote_name) => stack_remote_name,
319 None => return Ok(None),
320 };
321 let upstream_branch = match self.get_upstream_branch()? {
322 Some(upstream_branch) => upstream_branch,
323 None => return Ok(None),
324 };
325 let upstream_branch_name = upstream_branch.get_name()?;
326 let upstream_branch_name_without_remote =
327 match upstream_branch_name.strip_prefix(&format!("{push_remote_name}/")) {
328 Some(upstream_branch_name_without_remote) => upstream_branch_name_without_remote,
329 None => {
330 warn!(
331 ?push_remote_name,
332 ?upstream_branch,
333 "Upstream branch name did not start with push remote name"
334 );
335 upstream_branch_name
336 }
337 };
338 Ok(Some(upstream_branch_name_without_remote.to_owned()))
339 }
340
341 #[instrument]
345 pub fn get_push_remote_name(&self) -> eyre::Result<Option<String>> {
346 let branch_name = self
347 .inner
348 .name()?
349 .ok_or_else(|| eyre::eyre!("Branch name was not UTF-8: {self:?}"))?;
350 let config = self.repo.get_readonly_config()?;
351 if let Some(remote_name) = config.get(format!("branch.{branch_name}.pushRemote"))? {
352 Ok(Some(remote_name))
353 } else if let Some(remote_name) = config.get(format!("branch.{branch_name}.remote"))? {
354 Ok(Some(remote_name))
355 } else {
356 Ok(None)
357 }
358 }
359
360 pub fn into_reference(self) -> Reference<'repo> {
362 Reference {
363 inner: self.inner.into_reference(),
364 }
365 }
366
367 #[instrument]
369 pub fn rename(&mut self, new_name: &str, force: bool) -> Result<()> {
370 self.inner
371 .rename(new_name, force)
372 .map_err(|err| Error::RenameBranch {
373 source: err,
374 new_name: new_name.to_owned(),
375 })?;
376 Ok(())
377 }
378
379 #[instrument]
381 pub fn delete(&mut self) -> Result<()> {
382 self.inner.delete().map_err(Error::DeleteBranch)?;
383 Ok(())
384 }
385}