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};
#[derive(Debug, PartialEq, Eq)]
pub enum ReferenceTarget<'a> {
Direct {
oid: MaybeZeroOid,
},
Symbolic {
reference_name: Cow<'a, OsStr>,
},
}
#[derive(Debug, Error)]
pub enum ReferenceNameError {
#[error("reference name was not valid UTF-8: {0}")]
InvalidUtf8(FromUtf8Error),
}
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct ReferenceName(String);
impl ReferenceName {
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))
}
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
}
}
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> {
pub fn is_valid_name(name: &str) -> bool {
git2::Reference::is_valid_name(name)
}
#[instrument]
pub fn get_name(&self) -> Result<ReferenceName> {
let name = ReferenceName::from_bytes(self.inner.name_bytes().to_vec())?;
Ok(name)
}
#[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),
}
}
#[instrument]
pub fn delete(&mut self) -> Result<()> {
self.inner.delete().map_err(Error::DeleteReference)?;
Ok(())
}
}
#[derive(Debug)]
pub enum CategorizedReferenceName<'a> {
LocalBranch {
name: &'a str,
prefix: &'static str,
},
RemoteBranch {
name: &'a str,
prefix: &'static str,
},
OtherRef {
name: &'a str,
},
}
impl<'a> CategorizedReferenceName<'a> {
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 }
}
}
#[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())
}
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()
}
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()
}
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}"),
}
}
}
pub type BranchType = git2::BranchType;
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> {
pub fn get_oid(&self) -> Result<Option<NonZeroOid>> {
Ok(self.inner.get().target().map(make_non_zero_oid))
}
#[instrument]
pub fn get_name(&self) -> eyre::Result<&str> {
self.inner
.name()?
.ok_or_else(|| eyre::eyre!("Could not decode branch name"))
}
#[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()))
}
#[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(),
})
}
}
}
#[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)
}
#[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)
}
}
pub fn into_reference(self) -> Reference<'repo> {
Reference {
inner: self.inner.into_reference(),
}
}
#[instrument]
pub fn delete(&mut self) -> Result<()> {
self.inner.delete().map_err(Error::DeleteBranch)?;
Ok(())
}
}