1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
use std::borrow::Cow;
use std::ffi::OsStr;
use std::string::FromUtf8Error;

use thiserror::Error;
use tracing::instrument;

use crate::git::config::ConfigRead;
use crate::git::oid::make_non_zero_oid;
use crate::git::repo::{Error, Result};
use crate::git::{Commit, MaybeZeroOid, NonZeroOid, Repo};

/// The target of a reference.
#[derive(Debug, PartialEq, Eq)]
pub enum ReferenceTarget<'a> {
    /// The reference points directly to an object. This is the case for most
    /// references, such as branches.
    Direct {
        /// The OID of the pointed-to object.
        oid: MaybeZeroOid,
    },

    /// The reference points to another reference with the given name.
    Symbolic {
        /// The name of the pointed-to reference.
        reference_name: Cow<'a, OsStr>,
    },
}

#[derive(Debug, Error)]
pub enum ReferenceNameError {
    #[error("reference name was not valid UTF-8: {0}")]
    InvalidUtf8(FromUtf8Error),
}

/// The name of a reference, like `refs/heads/master`.
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct ReferenceName(String);

impl ReferenceName {
    /// Create a reference name from the provided bytestring. Non-UTF-8 references are not supported.
    pub fn from_bytes(bytes: Vec<u8>) -> std::result::Result<ReferenceName, ReferenceNameError> {
        let reference_name = String::from_utf8(bytes).map_err(ReferenceNameError::InvalidUtf8)?;
        Ok(Self(reference_name))
    }

    /// View this reference name as a string. (This is a zero-cost conversion.)
    pub fn as_str(&self) -> &str {
        let Self(reference_name) = self;
        reference_name
    }
}

impl From<&str> for ReferenceName {
    fn from(s: &str) -> Self {
        ReferenceName(s.to_owned())
    }
}

impl From<String> for ReferenceName {
    fn from(s: String) -> Self {
        ReferenceName(s)
    }
}

impl From<NonZeroOid> for ReferenceName {
    fn from(oid: NonZeroOid) -> Self {
        Self::from(oid.to_string())
    }
}

impl From<MaybeZeroOid> for ReferenceName {
    fn from(oid: MaybeZeroOid) -> Self {
        Self::from(oid.to_string())
    }
}

impl AsRef<str> for ReferenceName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

/// Represents a reference to an object.
pub struct Reference<'repo> {
    pub(super) inner: git2::Reference<'repo>,
}

impl std::fmt::Debug for Reference<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.inner.name() {
            Some(name) => write!(f, "<Reference name={:?}>", name),
            None => write!(f, "<Reference name={:?}>", self.inner.name_bytes()),
        }
    }
}

impl<'repo> Reference<'repo> {
    /// Determine if the given name is a valid name for a reference.
    pub fn is_valid_name(name: &str) -> bool {
        git2::Reference::is_valid_name(name)
    }

    /// Get the name of this reference.
    #[instrument]
    pub fn get_name(&self) -> Result<ReferenceName> {
        let name = ReferenceName::from_bytes(self.inner.name_bytes().to_vec())?;
        Ok(name)
    }
    /// Get the commit object pointed to by this reference. Returns `None` if
    /// the object pointed to by the reference is a different kind of object.
    #[instrument]
    pub fn peel_to_commit(&self) -> Result<Option<Commit<'repo>>> {
        let object = match self.inner.peel(git2::ObjectType::Commit) {
            Ok(object) => object,
            Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(None),
            Err(err) => return Err(Error::ResolveReference(err)),
        };
        match object.into_commit() {
            Ok(commit) => Ok(Some(Commit { inner: commit })),
            Err(_) => Ok(None),
        }
    }

    /// Delete the reference.
    #[instrument]
    pub fn delete(&mut self) -> Result<()> {
        self.inner.delete().map_err(Error::DeleteReference)?;
        Ok(())
    }
}

/// Determine what kind of branch a reference is, given its name. The returned
/// `suffix` value is converted to a `String` to be rendered to the screen, so
/// it may have lost some information if the reference name had unusual
/// characters.
///
/// FIXME: This abstraction seems uncomfortable and clunky to use; consider
/// revising.
#[derive(Debug)]
pub enum CategorizedReferenceName<'a> {
    /// The reference represents a local branch.
    LocalBranch {
        /// The full name of the reference.
        name: &'a str,

        /// The string `refs/heads/`.
        prefix: &'static str,
    },

    /// The reference represents a remote branch.
    RemoteBranch {
        /// The full name of the reference.
        name: &'a str,

        /// The string `refs/remotes/`.
        prefix: &'static str,
    },

    /// Some other kind of reference which isn't a branch at all.
    OtherRef {
        /// The full name of the reference.
        name: &'a str,
    },
}

impl<'a> CategorizedReferenceName<'a> {
    /// Categorize the provided reference name.
    pub fn new(name: &'a ReferenceName) -> Self {
        let name = name.as_str();
        if name.starts_with("refs/heads/") {
            Self::LocalBranch {
                name,
                prefix: "refs/heads/",
            }
        } else if name.starts_with("refs/remotes/") {
            Self::RemoteBranch {
                name,
                prefix: "refs/remotes/",
            }
        } else {
            Self::OtherRef { name }
        }
    }

    /// Remove the prefix from the reference name. May raise an error if the
    /// result couldn't be encoded as an `String` (shouldn't happen).
    #[instrument]
    pub fn remove_prefix(&self) -> Result<String> {
        let (name, prefix): (_, &'static str) = match self {
            Self::LocalBranch { name, prefix } => (name, prefix),
            Self::RemoteBranch { name, prefix } => (name, prefix),
            Self::OtherRef { name } => (name, ""),
        };
        Ok(name.strip_prefix(prefix).unwrap_or(name).to_owned())
    }

    /// Render the full name of the reference, including its prefix, lossily as
    /// a `String`.
    pub fn render_full(&self) -> String {
        let name = match self {
            Self::LocalBranch { name, prefix: _ } => name,
            Self::RemoteBranch { name, prefix: _ } => name,
            Self::OtherRef { name } => name,
        };
        (*name).to_owned()
    }

    /// Render only the suffix of the reference name lossily as a `String`. The
    /// caller will usually check the type of reference and add additional
    /// information to the reference name.
    pub fn render_suffix(&self) -> String {
        let (name, prefix): (_, &'static str) = match self {
            Self::LocalBranch { name, prefix } => (name, prefix),
            Self::RemoteBranch { name, prefix } => (name, prefix),
            Self::OtherRef { name } => (name, ""),
        };
        name.strip_prefix(prefix).unwrap_or(name).to_owned()
    }

    /// Render the reference name lossily, and prepend a helpful string like
    /// `branch` to the description.
    pub fn friendly_describe(&self) -> String {
        let name = self.render_suffix();
        match self {
            CategorizedReferenceName::LocalBranch { .. } => {
                format!("branch {}", name)
            }
            CategorizedReferenceName::RemoteBranch { .. } => {
                format!("remote branch {}", name)
            }
            CategorizedReferenceName::OtherRef { .. } => format!("ref {}", name),
        }
    }
}

/// Re-export of [`git2::BranchType`]. This might change to be an opaque type later.
pub type BranchType = git2::BranchType;

/// Represents a Git branch.
pub struct Branch<'repo> {
    pub(super) repo: &'repo Repo,
    pub(super) inner: git2::Branch<'repo>,
}

impl std::fmt::Debug for Branch<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "<Branch name={:?}>",
            String::from_utf8_lossy(
                self.inner
                    .name_bytes()
                    .unwrap_or(b"(could not get branch name)")
            ),
        )
    }
}

impl<'repo> Branch<'repo> {
    /// Get the OID pointed to by the branch. Returns `None` if the branch is
    /// not a direct reference (which is unusual).
    pub fn get_oid(&self) -> Result<Option<NonZeroOid>> {
        Ok(self.inner.get().target().map(make_non_zero_oid))
    }

    /// Get the name of this branch, not including any `refs/heads/` prefix. To get the full
    /// reference name of this branch, instead call `.into_reference().get_name()?`.
    #[instrument]
    pub fn get_name(&self) -> eyre::Result<&str> {
        self.inner
            .name()?
            .ok_or_else(|| eyre::eyre!("Could not decode branch name"))
    }

    /// Get the full reference name of this branch, including the `refs/heads/` or `refs/remotes/`
    /// prefix, as appropriate
    #[instrument]
    pub fn get_reference_name(&self) -> eyre::Result<ReferenceName> {
        let reference_name = self
            .inner
            .get()
            .name()
            .ok_or_else(|| eyre::eyre!("Could not decode branch reference name"))?;
        Ok(ReferenceName(reference_name.to_owned()))
    }

    /// If this branch tracks a remote ("upstream") branch, return that branch.
    #[instrument]
    pub fn get_upstream_branch(&self) -> Result<Option<Branch<'repo>>> {
        match self.inner.upstream() {
            Ok(upstream) => Ok(Some(Branch {
                repo: self.repo,
                inner: upstream,
            })),
            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
            Err(err) => {
                let branch_name = self.inner.name_bytes().map_err(|_err| Error::DecodeUtf8 {
                    item: "branch name",
                })?;
                Err(Error::FindUpstreamBranch {
                    source: err,
                    name: String::from_utf8_lossy(branch_name).into_owned(),
                })
            }
        }
    }

    /// If this branch tracks a remote ("upstream") branch, return the OID of the commit which that
    /// branch points to.
    #[instrument]
    pub fn get_upstream_branch_target(&self) -> eyre::Result<Option<NonZeroOid>> {
        let upstream_branch = match self.get_upstream_branch()? {
            Some(upstream_branch) => upstream_branch,
            None => return Ok(None),
        };
        let target_oid = upstream_branch.get_oid()?;
        Ok(target_oid)
    }

    /// Get the associated remote to push to for this branch. If there is no
    /// associated remote, returns `None`. Note that this never reads the value
    /// of `push.remoteDefault`.
    #[instrument]
    pub fn get_push_remote_name(&self) -> eyre::Result<Option<String>> {
        let branch_name = self
            .inner
            .name()?
            .ok_or_else(|| eyre::eyre!("Branch name was not UTF-8: {self:?}"))?;
        let config = self.repo.get_readonly_config()?;
        if let Some(remote_name) = config.get(format!("branch.{branch_name}.pushRemote"))? {
            Ok(Some(remote_name))
        } else if let Some(remote_name) = config.get(format!("branch.{branch_name}.remote"))? {
            Ok(Some(remote_name))
        } else {
            Ok(None)
        }
    }

    /// Convert the branch into its underlying `Reference`.
    pub fn into_reference(self) -> Reference<'repo> {
        Reference {
            inner: self.inner.into_reference(),
        }
    }
}