use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::SystemTime;
use color_eyre::Help;
use cursive::theme::BaseColor;
use cursive::utils::markup::StyledString;
use eyre::{eyre, Context};
use os_str_bytes::{OsStrBytes, OsStringBytes};
use tracing::{instrument, warn};
use crate::core::config::get_main_branch_name;
use crate::core::metadata::{render_commit_metadata, CommitMessageProvider, CommitOidProvider};
use crate::git::config::Config;
use crate::git::oid::{make_non_zero_oid, MaybeZeroOid, NonZeroOid};
use crate::tui::{Effects, OperationType};
use super::GitRunInfo;
pub(super) fn wrap_git_error(error: git2::Error) -> eyre::Error {
eyre::eyre!("Git error {:?}: {}", error.code(), error.message())
}
#[derive(Debug)]
pub struct HeadInfo {
pub oid: Option<NonZeroOid>,
reference_name: Option<String>,
}
impl HeadInfo {
pub fn get_branch_name(&self) -> Option<&str> {
self.reference_name
.as_ref()
.map(|name| match name.strip_prefix("refs/heads/") {
Some(branch_name) => branch_name,
None => name,
})
}
}
#[derive(Debug, PartialEq, PartialOrd, Eq)]
pub struct GitVersion(pub isize, pub isize, pub isize);
impl FromStr for GitVersion {
type Err = eyre::Error;
#[instrument]
fn from_str(output: &str) -> eyre::Result<GitVersion> {
let output = output.trim();
let words = output.split(&[' ', '-'][..]).collect::<Vec<&str>>();
let version_str = match &words.as_slice() {
[_git, _version, version_str, ..] => version_str,
_ => eyre::bail!("Could not parse Git version output: {:?}", output),
};
match version_str.split('.').collect::<Vec<&str>>().as_slice() {
[major, minor, patch, ..] => {
let major = major.parse()?;
let minor = minor.parse()?;
let patch = patch.parse()?;
Ok(GitVersion(major, minor, patch))
}
_ => eyre::bail!("Could not parse Git version string: {}", version_str),
}
}
}
pub struct Repo {
inner: git2::Repository,
}
impl std::fmt::Debug for Repo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<Git repository at: {:?}>", self.get_path())
}
}
impl Repo {
#[instrument]
pub fn from_dir(path: &Path) -> eyre::Result<Self> {
let repo = git2::Repository::discover(path).map_err(wrap_git_error)?;
Ok(Repo { inner: repo })
}
#[instrument]
pub fn from_current_dir() -> eyre::Result<Self> {
let path = std::env::current_dir().wrap_err_with(|| "Getting working directory")?;
Repo::from_dir(&path)
}
pub fn try_clone(&self) -> eyre::Result<Self> {
let path = self.get_path();
let repo = git2::Repository::open(path)?;
Ok(Repo { inner: repo })
}
pub fn get_path(&self) -> &Path {
self.inner.path()
}
pub fn get_rebase_state_dir_path(&self) -> PathBuf {
self.inner.path().join("rebase-merge")
}
pub fn get_working_copy_path(&self) -> Option<&Path> {
self.inner.workdir()
}
#[instrument]
pub fn get_config(&self) -> eyre::Result<Config> {
let config = self
.inner
.config()
.map_err(wrap_git_error)
.wrap_err_with(|| "Creating `git2::Config` object")?;
Ok(config.into())
}
#[instrument]
pub fn get_db_conn(&self) -> eyre::Result<rusqlite::Connection> {
let dir = self.get_path().join("branchless");
std::fs::create_dir_all(&dir).wrap_err_with(|| "Creating .git/branchless dir")?;
let path = dir.join("db.sqlite3");
let conn = rusqlite::Connection::open(&path)
.wrap_err_with(|| format!("Opening database connection at {:?}", &path))?;
Ok(conn)
}
#[instrument]
pub fn get_head_info(&self) -> eyre::Result<HeadInfo> {
let head_reference = match self.inner.find_reference("HEAD") {
Err(err) if err.code() == git2::ErrorCode::NotFound => None,
Err(err) => return Err(wrap_git_error(err)),
Ok(result) => Some(result),
};
let (head_oid, reference_name) = match &head_reference {
Some(head_reference) => {
let head_oid = head_reference
.peel_to_commit()
.wrap_err_with(|| "Resolving `HEAD` reference")?
.id();
let reference_name = match head_reference.kind() {
Some(git2::ReferenceType::Direct) => None,
Some(git2::ReferenceType::Symbolic) => match head_reference.symbolic_target() {
Some(name) => Some(name.to_string()),
None => eyre::bail!(
"`HEAD` reference was resolved to OID: {:?}, but its name could not be decoded: {:?}",
head_oid, head_reference.name_bytes()
),
}
None => eyre::bail!("Unknown `HEAD` reference type")
};
(
MaybeZeroOid::NonZero(make_non_zero_oid(head_oid)),
reference_name,
)
}
None => (MaybeZeroOid::Zero, None),
};
Ok(HeadInfo {
oid: head_oid.into(),
reference_name,
})
}
#[instrument]
pub fn set_head(&self, oid: NonZeroOid) -> eyre::Result<()> {
self.inner.set_head_detached(oid.inner)?;
Ok(())
}
#[instrument]
pub fn detach_head(&self, head_info: &HeadInfo) -> eyre::Result<()> {
match head_info.oid {
Some(oid) => self
.inner
.set_head_detached(oid.inner)
.map_err(wrap_git_error),
None => {
warn!("Attempted to detach `HEAD` while `HEAD` is unborn");
Ok(())
}
}
}
pub fn get_main_branch_reference(&self) -> eyre::Result<Reference> {
let main_branch_name = get_main_branch_name(self)?;
match self.find_branch(&main_branch_name, git2::BranchType::Local)? {
Some(branch) => Ok(branch.into_reference()),
None => match self.find_branch(&main_branch_name, git2::BranchType::Remote)? {
Some(branch) => Ok(branch.into_reference()),
None => {
let suggestion = format!(
r"
The main branch {:?} could not be found in your repository
at path: {:?}.
These branches exist: {:?}
Either create it, or update the main branch setting by running:
git config branchless.core.mainBranch <branch>
",
get_main_branch_name(self)?,
self.get_path(),
self.get_all_local_branches()?
.into_iter()
.map(|branch| {
branch
.into_reference()
.get_name()
.map(|s| format!("{:?}", s))
})
.collect::<eyre::Result<Vec<String>>>()?,
);
Err(eyre!("Could not find repository main branch")
.with_suggestion(|| suggestion))
}
},
}
}
#[instrument]
pub fn get_main_branch_oid(&self) -> eyre::Result<NonZeroOid> {
let main_branch_reference = self.get_main_branch_reference()?;
let commit = main_branch_reference.peel_to_commit()?;
match commit {
Some(commit) => Ok(commit.get_oid()),
None => eyre::bail!(
"Could not find commit pointed to by main branch: {:?}",
main_branch_reference.get_name()?
),
}
}
#[instrument]
pub fn get_branch_oid_to_names(&self) -> eyre::Result<HashMap<NonZeroOid, HashSet<OsString>>> {
let branches = self
.inner
.branches(Some(git2::BranchType::Local))
.wrap_err_with(|| "Reading branches")?;
let mut result: HashMap<NonZeroOid, HashSet<OsString>> = HashMap::new();
for branch_info in branches {
let branch_info = branch_info.wrap_err_with(|| "Iterating over branches")?;
let branch = match branch_info {
(branch, git2::BranchType::Remote) => eyre::bail!(
"Unexpectedly got a remote branch in local branch iterator: {:?}",
branch.name()
),
(branch, git2::BranchType::Local) => branch,
};
let reference = branch.into_reference();
let reference_name = match reference.name() {
None => {
warn!(
reference_name = ?reference.name_bytes(),
"Could not decode branch name, skipping"
);
continue;
}
Some(reference_name) => reference_name,
};
let branch_oid = reference
.resolve()
.wrap_err_with(|| format!("Resolving branch into commit: {}", reference_name))?
.target()
.unwrap();
result
.entry(make_non_zero_oid(branch_oid))
.or_insert_with(HashSet::new)
.insert(OsString::from(reference_name.to_owned()));
}
let main_branch_name = self.get_main_branch_reference()?.get_name()?;
let main_branch_oid = self.get_main_branch_oid()?;
result
.entry(main_branch_oid)
.or_insert_with(HashSet::new)
.insert(main_branch_name);
Ok(result)
}
#[instrument]
pub fn is_rebase_underway(&self) -> eyre::Result<bool> {
use git2::RepositoryState::*;
match self.inner.state() {
Rebase | RebaseInteractive | RebaseMerge => Ok(true),
Clean | Merge | Revert | RevertSequence | CherryPick | CherryPickSequence | Bisect
| ApplyMailbox | ApplyMailboxOrRebase => Ok(false),
}
}
pub fn get_current_operation_type(&self) -> Option<&str> {
use git2::RepositoryState::*;
match self.inner.state() {
Clean | Bisect => None,
Merge => Some("merge"),
Revert | RevertSequence => Some("revert"),
CherryPick | CherryPickSequence => Some("cherry-pick"),
Rebase | RebaseInteractive | RebaseMerge => Some("rebase"),
ApplyMailbox | ApplyMailboxOrRebase => Some("am"),
}
}
#[instrument]
pub fn find_merge_base(
&self,
lhs: NonZeroOid,
rhs: NonZeroOid,
) -> eyre::Result<Option<NonZeroOid>> {
match self.inner.merge_base(lhs.inner, rhs.inner) {
Ok(merge_base_oid) => Ok(Some(make_non_zero_oid(merge_base_oid))),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
#[instrument]
fn get_diff_for_commit(
&self,
effects: &Effects,
commit: &Commit,
) -> eyre::Result<Option<git2::Diff>> {
let (_effects, _progress) = effects.start_operation(OperationType::CalculateDiff);
let only_parent = match commit.get_only_parent() {
None => return Ok(None),
Some(only_parent) => only_parent,
};
let parent_tree = only_parent.get_tree()?;
let current_tree = commit.get_tree()?;
let diff = self
.inner
.diff_tree_to_tree(Some(&parent_tree.inner), Some(¤t_tree.inner), None)
.wrap_err_with(|| {
format!(
"Calculating diff between: {:?} and {:?}",
commit, only_parent
)
})?;
Ok(Some(diff))
}
pub fn get_paths_touched_by_commit(
&self,
effects: &Effects,
commit: &Commit,
) -> eyre::Result<Option<HashSet<PathBuf>>> {
let diff = match self.get_diff_for_commit(effects, commit)? {
None => return Ok(None),
Some(diff) => diff,
};
let mut files = HashSet::new();
let mut file_cb = |delta: git2::DiffDelta, _progress: f32| {
if let Some(old_path) = delta.old_file().path() {
files.insert(old_path.to_path_buf());
}
if let Some(new_path) = delta.new_file().path() {
files.insert(new_path.to_path_buf());
}
true
};
diff.foreach(&mut file_cb, None, None, None)
.wrap_err_with(|| format!("Iterating over diff for commit: {:?}", commit))?;
Ok(Some(files))
}
#[instrument]
pub fn get_patch_id(
&self,
effects: &Effects,
commit: &Commit,
) -> eyre::Result<Option<PatchId>> {
let diff = match self.get_diff_for_commit(effects, commit)? {
None => return Ok(None),
Some(diff) => diff,
};
let patch_id = {
let (_effects, _progress) = effects.start_operation(OperationType::CalculatePatchId);
diff.patchid(None)
.wrap_err_with(|| format!("Computing patch ID for: {:?}", commit))?
};
Ok(Some(PatchId { patch_id }))
}
pub fn revparse_single_commit(&self, spec: &str) -> eyre::Result<Option<Commit>> {
match self.inner.revparse_single(spec) {
Ok(object) => match object.into_commit() {
Ok(commit) => Ok(Some(Commit { inner: commit })),
Err(_) => Ok(None),
},
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
#[instrument]
pub fn get_all_references(&self) -> eyre::Result<Vec<Reference>> {
let mut all_references = Vec::new();
for reference in self
.inner
.references()
.map_err(wrap_git_error)
.wrap_err_with(|| "Iterating through references")?
{
let reference = reference.wrap_err_with(|| "Accessing individual reference")?;
all_references.push(Reference { inner: reference });
}
Ok(all_references)
}
#[instrument]
pub fn has_changed_files(
&self,
effects: &Effects,
git_run_info: &GitRunInfo,
) -> eyre::Result<bool> {
let exit_code = git_run_info.run(
effects,
None,
&["diff", "--quiet"],
)?;
if exit_code == 0 {
Ok(false)
} else {
Ok(true)
}
}
#[instrument]
pub fn create_reference(
&self,
name: &OsStr,
oid: NonZeroOid,
force: bool,
log_message: &str,
) -> eyre::Result<Reference> {
let name = match name.to_str() {
Some(name) => name,
None => eyre::bail!(
"Reference name is not a UTF-8 string (libgit2 limitation): {:?}",
name
),
};
let reference = self
.inner
.reference(name, oid.inner, force, log_message)
.map_err(wrap_git_error)?;
Ok(Reference { inner: reference })
}
#[instrument]
pub fn find_reference(&self, name: &OsStr) -> eyre::Result<Option<Reference>> {
let name = match name.to_str() {
Some(name) => name,
None => eyre::bail!(
"Reference name is not a UTF-8 string (libgit2 limitation): {:?}",
name
),
};
match self.inner.find_reference(name) {
Ok(reference) => Ok(Some(Reference { inner: reference })),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
#[instrument]
pub fn get_all_local_branches(&self) -> eyre::Result<Vec<Branch>> {
let mut all_branches = Vec::new();
for branch in self
.inner
.branches(Some(git2::BranchType::Local))
.map_err(wrap_git_error)
.wrap_err_with(|| "Iterating over all local branches")?
{
let (branch, _branch_type) = branch.wrap_err_with(|| "Accessing individual branch")?;
all_branches.push(Branch { inner: branch });
}
Ok(all_branches)
}
#[instrument]
pub fn find_branch(&self, name: &str, branch_type: BranchType) -> eyre::Result<Option<Branch>> {
match self.inner.find_branch(name, branch_type) {
Ok(branch) => Ok(Some(Branch { inner: branch })),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
#[instrument]
pub fn create_branch(
&self,
name: &OsStr,
target: &Commit,
force: bool,
) -> eyre::Result<git2::Branch> {
let name = match name.to_str() {
Some(name) => name,
None => eyre::bail!(
"Branch name is not a UTF-8 string (libgit2 limitation): {:?}",
name
),
};
self.inner
.branch(name, &target.inner, force)
.map_err(wrap_git_error)
}
#[instrument]
pub fn find_commit(&self, oid: NonZeroOid) -> eyre::Result<Option<Commit>> {
match self.inner.find_commit(oid.inner) {
Ok(commit) => Ok(Some(Commit { inner: commit })),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
pub fn friendly_describe_commit_from_oid(&self, oid: NonZeroOid) -> eyre::Result<StyledString> {
match self.find_commit(oid)? {
Some(commit) => Ok(commit.friendly_describe()?),
None => Ok(StyledString::styled(
format!("<commit not found: {:?}>", oid),
BaseColor::Red.light(),
)),
}
}
#[instrument]
pub fn create_commit(
&self,
update_ref: Option<&str>,
author: &Signature,
committer: &Signature,
message: &str,
tree: &git2::Tree,
parents: &[&Commit],
) -> eyre::Result<NonZeroOid> {
let parents = parents
.iter()
.map(|commit| &commit.inner)
.collect::<Vec<_>>();
let oid = self
.inner
.commit(
update_ref,
&author.inner,
&committer.inner,
message,
tree,
parents.as_slice(),
)
.map_err(wrap_git_error)?;
Ok(make_non_zero_oid(oid))
}
#[instrument]
pub fn cherrypick_commit(
&self,
cherrypick_commit: &Commit,
our_commit: &Commit,
mainline: u32,
) -> eyre::Result<Index> {
let index = self
.inner
.cherrypick_commit(&cherrypick_commit.inner, &our_commit.inner, mainline, None)
.map_err(wrap_git_error)?;
Ok(Index { inner: index })
}
#[instrument]
pub fn find_tree(&self, oid: NonZeroOid) -> eyre::Result<Option<git2::Tree>> {
match self.inner.find_tree(oid.inner) {
Ok(tree) => Ok(Some(tree)),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(wrap_git_error(err)),
}
}
#[instrument]
pub fn write_index_to_tree(&self, index: &mut Index) -> eyre::Result<NonZeroOid> {
let oid = index
.inner
.write_tree_to(&self.inner)
.map_err(wrap_git_error)?;
Ok(make_non_zero_oid(oid))
}
}
pub struct Signature<'repo> {
inner: git2::Signature<'repo>,
}
impl std::fmt::Debug for Signature<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<Signature>")
}
}
impl<'repo> Signature<'repo> {
#[instrument]
pub fn update_timestamp(self, now: SystemTime) -> eyre::Result<Signature<'repo>> {
let seconds: i64 = now
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs()
.try_into()?;
let time = git2::Time::new(seconds, self.inner.when().offset_minutes());
let name = match self.inner.name() {
Some(name) => name,
None => eyre::bail!(
"Could not decode signature name: {:?}",
self.inner.name_bytes()
),
};
let email = match self.inner.email() {
Some(email) => email,
None => eyre::bail!(
"Could not decode signature email: {:?}",
self.inner.email_bytes()
),
};
let signature = git2::Signature::new(name, email, &time)?;
Ok(Signature { inner: signature })
}
pub fn get_time(&self) -> git2::Time {
self.inner.when()
}
}
pub struct Tree<'repo> {
inner: git2::Tree<'repo>,
}
impl Tree<'_> {
pub fn get_oid_for_path(&self, path: &Path) -> eyre::Result<Option<MaybeZeroOid>> {
match self.inner.get_path(path) {
Ok(entry) => Ok(Some(entry.id().into())),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}
}
pub struct Index {
inner: git2::Index,
}
impl std::fmt::Debug for Index {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<Index>")
}
}
impl Index {
pub fn has_conflicts(&self) -> bool {
self.inner.has_conflicts()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PatchId {
patch_id: git2::Oid,
}
#[derive(Clone, Debug)]
pub struct Commit<'repo> {
inner: git2::Commit<'repo>,
}
impl<'repo> Commit<'repo> {
pub fn get_oid(&self) -> NonZeroOid {
NonZeroOid {
inner: self.inner.id(),
}
}
pub fn get_parent_oids(&self) -> Vec<NonZeroOid> {
self.inner.parent_ids().map(make_non_zero_oid).collect()
}
pub fn get_only_parent_oid(&self) -> Option<NonZeroOid> {
match self.get_parent_oids().as_slice() {
[] | [_, _, ..] => None,
[only_parent_oid] => Some(*only_parent_oid),
}
}
pub fn get_parent_count(&self) -> usize {
self.inner.parent_count()
}
pub fn get_parents(&self) -> Vec<Commit<'repo>> {
self.inner
.parents()
.map(|commit| Commit { inner: commit })
.collect()
}
pub fn get_only_parent(&self) -> Option<Commit<'repo>> {
match self.get_parents().as_slice() {
[] | [_, _, ..] => None,
[only_parent] => Some(only_parent.clone()),
}
}
pub fn get_time(&self) -> git2::Time {
self.inner.time()
}
pub fn get_summary(&self) -> eyre::Result<OsString> {
match self.inner.summary_bytes() {
Some(summary) => Ok(OsString::from_raw_vec(summary.into())?),
None => eyre::bail!("Could not read summary for commit: {:?}", self.get_oid()),
}
}
pub fn get_message_pretty(&self) -> eyre::Result<OsString> {
let message = OsString::from_raw_vec(self.inner.message_bytes().into())?;
Ok(message)
}
pub fn get_message_raw(&self) -> eyre::Result<OsString> {
let message = OsString::from_raw_vec(self.inner.message_raw_bytes().into())?;
Ok(message)
}
pub fn get_author(&self) -> Signature {
Signature {
inner: self.inner.author(),
}
}
pub fn get_committer(&self) -> Signature {
Signature {
inner: self.inner.committer(),
}
}
pub fn get_tree(&self) -> eyre::Result<Tree> {
let tree = self
.inner
.tree()
.wrap_err_with(|| format!("Getting tree object for commit: {:?}", self.get_oid()))?;
Ok(Tree { inner: tree })
}
#[instrument]
pub fn friendly_describe(&self) -> eyre::Result<StyledString> {
let description = render_commit_metadata(
self,
&mut [
&mut CommitOidProvider::new(true)?,
&mut CommitMessageProvider::new()?,
],
)?;
Ok(description)
}
pub fn is_empty(&self) -> bool {
match self.get_parents().as_slice() {
[] => false,
[parent_commit] => self.inner.tree_id() == parent_commit.inner.tree_id(),
_ => false,
}
}
pub fn contains_touched_path(&self, path: &Path) -> eyre::Result<Option<bool>> {
let parent = match self.get_only_parent() {
None => return Ok(None),
Some(parent) => parent,
};
let parent_tree = parent.get_tree()?;
let current_tree = self.get_tree()?;
let parent_oid = parent_tree.get_oid_for_path(path)?;
let current_oid = current_tree.get_oid_for_path(path)?;
match (parent_oid, current_oid) {
(None, None) => Ok(Some(false)),
(None, Some(_)) | (Some(_), None) => Ok(Some(true)),
(Some(parent_oid), Some(current_oid)) => Ok(Some(parent_oid != current_oid)),
}
}
}
#[derive(Debug)]
pub enum ReferenceTarget<'a> {
Direct {
oid: MaybeZeroOid,
},
Symbolic {
reference_name: Cow<'a, OsStr>,
},
}
pub struct Reference<'repo> {
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 name_to_oid(ref_name: &OsStr) -> eyre::Result<Option<NonZeroOid>> {
let oid: MaybeZeroOid = ref_name.try_into()?;
match oid {
MaybeZeroOid::NonZero(oid) => Ok(Some(oid)),
MaybeZeroOid::Zero => Ok(None),
}
}
#[instrument]
pub fn get_name(&self) -> eyre::Result<OsString> {
let name = OsStringBytes::from_raw_vec(self.inner.name_bytes().into())
.wrap_err_with(|| format!("Decoding reference name: {:?}", self.inner.name_bytes()))?;
Ok(name)
}
#[instrument]
pub fn get_target(&self) -> eyre::Result<ReferenceTarget> {
match self.inner.symbolic_target_bytes() {
Some(reference_name) => Ok(ReferenceTarget::Symbolic {
reference_name: OsStr::from_raw_bytes(reference_name).wrap_err_with(|| {
format!("Decoding symbolic reference target: {:?}", reference_name)
})?,
}),
None => Ok(ReferenceTarget::Direct {
oid: match self.inner.target() {
Some(oid) => oid.into(),
None => eyre::bail!(
"Could not get direct reference target for: {:?}",
self.get_name()?
),
},
}),
}
}
#[instrument]
pub fn peel_to_commit(&self) -> eyre::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(err.into()),
};
match object.into_commit() {
Ok(commit) => Ok(Some(Commit { inner: commit })),
Err(_) => Ok(None),
}
}
#[instrument]
pub fn delete(&mut self) -> eyre::Result<()> {
let reference_name = self.get_name()?;
self.inner
.delete()
.wrap_err_with(|| format!("Deleting reference: {:?}", reference_name))?;
Ok(())
}
}
#[derive(Debug)]
pub enum CategorizedReferenceName<'a> {
LocalBranch {
name: &'a OsStr,
prefix: &'static str,
},
RemoteBranch {
name: &'a OsStr,
prefix: &'static str,
},
OtherRef {
name: &'a OsStr,
},
}
impl<'a> CategorizedReferenceName<'a> {
pub fn new(name: &'a OsStr) -> Self {
let bytes = name.to_raw_bytes();
if bytes.starts_with(b"refs/heads/") {
Self::LocalBranch {
name,
prefix: "refs/heads/",
}
} else if bytes.starts_with(b"refs/remotes/") {
Self::RemoteBranch {
name,
prefix: "refs/remotes/",
}
} else {
Self::OtherRef { name }
}
}
#[instrument]
pub fn remove_prefix(&self) -> eyre::Result<OsString> {
let (name, prefix): (_, &'static str) = match self {
Self::LocalBranch { name, prefix } => (name, prefix),
Self::RemoteBranch { name, prefix } => (name, prefix),
Self::OtherRef { name } => (name, ""),
};
let bytes = name.to_raw_bytes();
let bytes = match bytes.strip_prefix(prefix.as_bytes()) {
Some(bytes) => Vec::from(bytes),
None => Vec::from(bytes),
};
let result = OsString::from_raw_vec(bytes)?;
Ok(result)
}
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_string_lossy().into_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, ""),
};
let name = name.to_string_lossy();
match name.strip_prefix(prefix) {
Some(name) => name.to_string(),
None => name.into_owned(),
}
}
pub fn friendly_describe(&self) -> String {
let name = self.render_suffix();
let name = match self {
CategorizedReferenceName::LocalBranch { .. } => {
format!("branch {}", name)
}
CategorizedReferenceName::RemoteBranch { .. } => {
format!("remote branch {}", name)
}
CategorizedReferenceName::OtherRef { .. } => format!("ref {}", name),
};
name
}
}
type BranchType = git2::BranchType;
pub struct Branch<'repo> {
inner: git2::Branch<'repo>,
}
impl<'repo> Branch<'repo> {
pub fn get_oid(&self) -> eyre::Result<Option<NonZeroOid>> {
Ok(self.inner.get().target().map(make_non_zero_oid))
}
pub fn into_reference(self) -> Reference<'repo> {
Reference {
inner: self.inner.into_reference(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_git_version_output() {
assert_eq!(
"git version 12.34.56".parse::<GitVersion>().unwrap(),
GitVersion(12, 34, 56)
);
assert_eq!(
"git version 12.34.56\n".parse::<GitVersion>().unwrap(),
GitVersion(12, 34, 56)
);
assert_eq!(
"git version 12.34.56.78.abcdef"
.parse::<GitVersion>()
.unwrap(),
GitVersion(12, 34, 56)
);
assert_eq!(
"git version 2.33.0-rc0".parse::<GitVersion>().unwrap(),
GitVersion(2, 33, 0)
)
}
}