Lines
96.75 %
Functions
62.5 %
Branches
100 %
use std::collections::HashSet;
use git2::{BranchType, Repository};
use crate::remote;
/// Get the current branch name of a repository.
fn current(repo: &Repository) -> Option<String> {
let head = repo.head().ok()?;
let shorthand = head.shorthand()?;
Some(shorthand.to_string())
}
/// Delete a git branch from a given repository.
///
/// ## Errors
/// Return git2::Error if the delete operation fails.
pub fn delete(repo: &Repository, branch_name: &str) -> Result<(), git2::Error> {
let mut branch = repo.find_branch(branch_name, git2::BranchType::Local)?;
branch.delete()?;
println!("Deleting branch: {}", branch_name);
Ok(())
/// Return a list of all branches of a given BranchType in a repository as a
/// HashSet. If the repository is not found, or there are no local branches,
/// return an empty HashSet.
fn list(repo: &Repository, branch_type: BranchType) -> HashSet<String> {
let mut branches = HashSet::new();
if let Ok(local_branches) = repo.branches(Some(branch_type)) {
for (b, _) in local_branches.flatten() {
if let Some(name) = b.into_reference().shorthand() {
branches.insert(name.to_string());
branches
/// Remove the remote branch prefix from a branch name.
/// If the branch name does not contain a remote prefix, return the original
/// branch name.
fn remove_remote_prefix<'a>(branch_name: &'a str, remote_prefixes: &HashSet<String>) -> &'a str {
for prefix in remote_prefixes {
if branch_name.starts_with(prefix) {
return &branch_name[prefix.len()..];
branch_name
/// Return a list all branches to delete from a repository - branches without a
/// remote counterpart. If there are no branches to delete, return an empty
/// HashSet.
pub fn list_to_delete(repo: &Repository) -> HashSet<String> {
let local_branches = list(repo, BranchType::Local);
let remote_branches: HashSet<_> = list(repo, BranchType::Remote)
.iter()
.map(|b| remove_remote_prefix(b, &remote::list_remote_prefixes(repo)).to_string())
.collect();
// Mark local branches that are not in remote branches to delete.
let mut branches_to_delete: HashSet<String> = local_branches
.difference(&remote_branches)
.map(|b| b.to_string())
let current_branch = current(repo);
if let Some(current_branch) = current_branch {
if branches_to_delete.contains(¤t_branch) {
println!("Cannot delete current branch: {}", current_branch);
// Remove current branch from the list of branches to delete.
branches_to_delete.retain(|b| b != ¤t_branch);
branches_to_delete
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utilities;
/// Find the latest commit in a repository.
fn get_latest_commit(repo: &Repository) -> git2::Commit {
let head = repo.head().unwrap();
let commit = head.peel_to_commit().unwrap();
commit
#[test]
fn test_current() {
// GIVEN a repository with a branch
let (_testdir, repo) = test_utilities::create_mock_repo();
// WHEN the current branch is retrieved
let actual = current(&repo).unwrap();
let binding = repo.head().unwrap();
let expected = binding.shorthand().unwrap();
// THEN it should match the expected branch name
assert_eq!(actual, expected);
fn test_delete() {
let branch_name = "test-branch";
let target_commit = get_latest_commit(&repo);
let _ = repo.branch(branch_name, &target_commit, false);
// WHEN the branch is deleted
let _ = delete(&repo, branch_name);
// THEN the branch should not exist
assert!(repo.find_branch(branch_name, BranchType::Local).is_err());
fn test_list_local() {
// GIVEN a repository with local branches
let local_branches = vec!["local-branch-1", "local-branch-2"];
for branch_name in local_branches.iter() {
let mut expected = HashSet::from_iter(local_branches.iter().map(|b| b.to_string()));
expected.insert("main".to_string());
// WHEN the list of branches is retrieved
let actual = list(&repo, BranchType::Local);
// THEN the set of branches should match the expected one
fn test_list_to_delete() {
// GIVEN a repository with local and remote branches
let orphaned_branches = vec!["to-delete-1", "to-delete-2"];
for branch_name in orphaned_branches.iter() {
// WHEN the list of branches to delete is retrieved
let expected = HashSet::from_iter(orphaned_branches.iter().map(|b| b.to_string()));
let actual = list_to_delete(&repo);
fn test_remove_remote_prefix() {
// GIVEN a branch name with a remote prefix
let branch_name = "origin/branch-name";
let remote_prefixes = HashSet::from_iter(vec!["origin/".to_string()]);
// WHEN the remote prefix is removed
let actual = remove_remote_prefix(branch_name, &remote_prefixes);
// THEN the branch name should match the expected one
assert_eq!(actual, "branch-name");