Lines
87.36 %
Functions
52.38 %
Branches
100 %
use std::collections::HashSet;
use std::fmt::{Debug, Display};
use anyhow::{bail, Context, Result};
use git2::Reference;
/// A wrapper around git2::Branch.
pub struct Branch<'a>(git2::Branch<'a>);
impl Branch<'_> {
/// Delete the git branch.
///
/// ## Errors
/// Returns GitSnipError::DeleteBranchError if the branch cannot be deleted.
pub fn delete(&mut self) -> Result<()> {
self.0.delete().context("Failed to delete branch")
}
/// Return the name of the branch.
pub fn name(&self) -> Result<String> {
match self.0.name() {
Ok(Some(name)) => Ok(name.to_string()),
Ok(None) => bail!("Branch has no name"),
Err(e) => bail!("Failed to get branch name: {}", e),
/// Remove a branch name prefix from a list of possible prefixes.
/// If none match, return the original branch name.
pub fn name_without_prefix(&self, prefixes: &HashSet<String>) -> Result<String> {
let name = self.name().context("Branch has no name")?;
for prefix in prefixes {
if name.starts_with(prefix) {
return Ok(name[prefix.len()..].to_string());
Ok(name)
impl<'a> From<git2::Branch<'a>> for Branch<'a> {
fn from(branch: git2::Branch<'a>) -> Self {
Branch(branch)
// Optionally, allow conversion from your own Reference wrapper if desired.
impl<'a> From<Reference<'a>> for Branch<'a> {
fn from(reference: Reference<'a>) -> Self {
Branch(git2::Branch::wrap(reference))
impl PartialEq for Branch<'_> {
fn eq(&self, other: &Self) -> bool {
self.name().ok() == other.name().ok()
impl Eq for Branch<'_> {}
impl Debug for Branch<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Branch")
.field("name", &self.name())
.finish()
impl Display for Branch<'_> {
match self.name() {
Ok(name) => write!(f, "{name}"),
Err(_) => write!(f, "<invalid branch>"),
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utilities;
#[test]
fn test_name() {
// GIVEN a mock repository with a branch
let (_testdir, repo) = test_utilities::create_mock_repo();
let branch_name = "test-branch";
let target_commit = test_utilities::get_latest_commit(&repo);
let branch = repo.branch(branch_name, &target_commit, false).unwrap();
// WHEN we get the name of the branch
let actual = Branch::from(branch).name().unwrap();
// THEN the name should match the branch name
assert_eq!(actual, branch_name);
fn test_name_without_prefix() {
// GIVEN a mock repository with a branch that has a remote prefix
let branch_name = "origin/test-branch";
// WHEN we get the name without the remote prefix
let remote_prefixes = HashSet::from_iter(vec!["origin/".to_string()]);
let actual = Branch::from(branch)
.name_without_prefix(&remote_prefixes)
.unwrap();
// THEN the name should be the branch name without the prefix
assert_eq!(actual, "test-branch");
fn test_name_without_prefix_no_prefix() {
// GIVEN a mock repository with a branch that does not have a remote prefix
// THEN the name should be the branch name as it has no prefix
fn test_delete() {
let branch = repo.branch(branch_name, &target_commit, false);
// WHEN we delete the branch
let mut branch = Branch::from(branch.unwrap());
let _ = branch.delete();
// THEN the branch should no longer exist
assert!(repo
.find_branch(branch_name, git2::BranchType::Local)
.is_err());
fn test_branch_equality() {
// GIVEN a mock repository with two branches of the same name
// WHEN we create two branches with the same name
let branch1 = repo.branch(branch_name, &target_commit, false).unwrap();
let branch2 = repo
// THEN both branches should be equal
assert_eq!(Branch::from(branch1), Branch::from(branch2));