branchless/git/
reference.rs

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/// The target of a reference.
14#[derive(Debug, PartialEq, Eq)]
15pub enum ReferenceTarget<'a> {
16    /// The reference points directly to an object. This is the case for most
17    /// references, such as branches.
18    Direct {
19        /// The OID of the pointed-to object.
20        oid: MaybeZeroOid,
21    },
22
23    /// The reference points to another reference with the given name.
24    Symbolic {
25        /// The name of the pointed-to reference.
26        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/// The name of a reference, like `refs/heads/master`.
37#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
38pub struct ReferenceName(String);
39
40impl ReferenceName {
41    /// Create a reference name from the provided bytestring. Non-UTF-8 references are not supported.
42    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    /// View this reference name as a string. (This is a zero-cost conversion.)
48    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
84/// Represents a reference to an object.
85pub 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    /// Determine if the given name is a valid name for a reference.
100    pub fn is_valid_name(name: &str) -> bool {
101        git2::Reference::is_valid_name(name)
102    }
103
104    /// Get the name of this reference.
105    #[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    /// Get the commit object pointed to by this reference. Returns `None` if
111    /// the object pointed to by the reference is a different kind of object.
112    #[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    /// Delete the reference.
126    #[instrument]
127    pub fn delete(&mut self) -> Result<()> {
128        self.inner.delete().map_err(Error::DeleteReference)?;
129        Ok(())
130    }
131}
132
133/// Determine what kind of branch a reference is, given its name.
134///
135/// The returned `suffix` value is converted to a `String` to be rendered to
136/// the screen, so it may have lost some information if the reference name had
137/// unusual characters.
138///
139/// FIXME: This abstraction seems uncomfortable and clunky to use; consider
140/// revising.
141#[derive(Debug)]
142pub enum CategorizedReferenceName<'a> {
143    /// The reference represents a local branch.
144    LocalBranch {
145        /// The full name of the reference.
146        name: &'a str,
147
148        /// The string `refs/heads/`.
149        prefix: &'static str,
150    },
151
152    /// The reference represents a remote branch.
153    RemoteBranch {
154        /// The full name of the reference.
155        name: &'a str,
156
157        /// The string `refs/remotes/`.
158        prefix: &'static str,
159    },
160
161    /// Some other kind of reference which isn't a branch at all.
162    OtherRef {
163        /// The full name of the reference.
164        name: &'a str,
165    },
166}
167
168impl<'a> CategorizedReferenceName<'a> {
169    /// Categorize the provided reference name.
170    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    /// Render the full name of the reference, including its prefix, lossily as
188    /// a `String`.
189    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    /// Render only the suffix of the reference name lossily as a `String`. The
199    /// caller will usually check the type of reference and add additional
200    /// information to the reference name.
201    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    /// Render the reference name lossily, and prepend a helpful string like
211    /// `branch` to the description.
212    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
226/// Re-export of [`git2::BranchType`]. This might change to be an opaque type later.
227pub type BranchType = git2::BranchType;
228
229/// Represents a Git branch.
230pub 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    /// Get the OID pointed to by the branch. Returns `None` if the branch is
251    /// not a direct reference (which is unusual).
252    pub fn get_oid(&self) -> Result<Option<NonZeroOid>> {
253        Ok(self.inner.get().target().map(make_non_zero_oid))
254    }
255
256    /// Get the name of this branch, not including any `refs/heads/` prefix. To get the full
257    /// reference name of this branch, instead call `.into_reference().get_name()?`.
258    #[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    /// Get the full reference name of this branch, including the `refs/heads/` or `refs/remotes/`
266    /// prefix, as appropriate
267    #[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    /// If this branch tracks a remote ("upstream") branch, return that branch.
278    #[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    /// If this branch tracks a remote ("upstream") branch, return the OID of the commit which that
299    /// branch points to.
300    #[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    /// If this branch tracks a remote ("upstream") branch, return the name of
311    /// that branch without the leading remote name. For example, if the
312    /// upstream branch is `origin/main`, this will return `main`. (Usually,
313    /// this is the same as the name of the local branch, but not always.)
314    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    /// Get the associated remote to push to for this branch. If there is no
342    /// associated remote, returns `None`. Note that this never reads the value
343    /// of `push.remoteDefault`.
344    #[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    /// Convert the branch into its underlying `Reference`.
361    pub fn into_reference(self) -> Reference<'repo> {
362        Reference {
363            inner: self.inner.into_reference(),
364        }
365    }
366
367    /// Rename the branch. The new name should not start with `refs/heads`.
368    #[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    /// Delete the branch.
380    #[instrument]
381    pub fn delete(&mut self) -> Result<()> {
382        self.inner.delete().map_err(Error::DeleteBranch)?;
383        Ok(())
384    }
385}