Lines
86.79 %
Functions
61.54 %
Branches
100 %
mod branch;
mod hook;
mod input;
mod reference;
mod remote;
mod repo;
use std::env;
use std::io::BufRead;
use anyhow::{Context, Result};
use hook::{GitHook, GitHookType};
/// Install git-snip hook scripts for post-merge and post-rewrite.
pub fn install_hooks() -> Result<()> {
let current_dir = env::current_dir().context("Could not get current directory")?;
let repo = repo::Repository::open(current_dir)?;
println!("Installing git-snip hook script.");
let h = GitHook::default();
hook::install(repo.path(), &h, GitHookType::PostMerge)?;
hook::install(repo.path(), &h, GitHookType::PostRewrite)?;
Ok(())
}
/// Delete all local branches that are not in remote branches.
pub fn snip(no_confirm: bool, reader: impl BufRead) -> Result<()> {
snip_repo(&repo, no_confirm, reader)
fn snip_repo(repo: &repo::Repository, no_confirm: bool, reader: impl BufRead) -> Result<()> {
let result = repo.orphaned_branches();
if let Some(head) = &result.skipped_head {
println!("Cannot delete current branch: {head}");
let mut branches_to_delete = result.branches;
if branches_to_delete.is_empty() {
println!("No local branches to delete.");
return Ok(());
if !no_confirm {
println!("Local branches to delete:");
for branch in &branches_to_delete {
println!(" - {branch}");
let user_input = input::prompt("Delete these branches? (y/n): ", reader);
if user_input != "y" && user_input != "yes" {
println!("Aborting.");
for branch in &mut branches_to_delete {
println!("Deleting branch: {branch}");
branch.delete()?;
#[cfg(test)]
mod tests {
use std::io::Cursor;
use crate::test_utilities;
use super::*;
fn open_repo(testdir: &tempfile::TempDir) -> repo::Repository {
repo::Repository::open(testdir.path()).unwrap()
#[test]
fn test_snip_no_confirm() {
// GIVEN a repo with orphaned branches and no_confirm=true
let (testdir, repo) = test_utilities::create_mock_repo();
let commit = test_utilities::get_latest_commit(&repo);
repo.branch("orphan-1", &commit, false).unwrap();
repo.branch("orphan-2", &commit, false).unwrap();
// WHEN snip is called with no_confirm
let result = snip_repo(&open_repo(&testdir), true, Cursor::new(""));
// THEN it should succeed and the branches should be deleted
assert!(result.is_ok());
assert!(repo
.find_branch("orphan-1", git2::BranchType::Local)
.is_err());
.find_branch("orphan-2", git2::BranchType::Local)
fn test_snip_user_confirms() {
// GIVEN a repo with orphaned branches and a reader that answers "y"
// WHEN snip is called and user confirms with "y"
let result = snip_repo(&open_repo(&testdir), false, Cursor::new("y\n"));
// THEN it should succeed and the branch should be deleted
fn test_snip_user_declines() {
// GIVEN a repo with orphaned branches and a reader that answers "n"
// WHEN snip is called and user declines with "n"
let result = snip_repo(&open_repo(&testdir), false, Cursor::new("n\n"));
// THEN it should succeed and the branch should still exist
.is_ok());
fn test_snip_no_orphaned_branches() {
// GIVEN a repo with no orphaned branches
let (testdir, _repo) = test_utilities::create_mock_repo();
// WHEN snip is called
// THEN it should succeed with no deletions
pub mod test_utilities {
use git2::{Repository, RepositoryInitOptions};
use tempfile::TempDir;
/// Create a mock Git repository with initial commit in a temporary
/// directory for testing.
pub fn create_mock_repo() -> (TempDir, Repository) {
let tempdir = TempDir::new().unwrap();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(tempdir.path(), &opts).unwrap();
// Create initial commit
{
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
let mut index = repo.index().unwrap();
let id = index.write_tree().unwrap();
let tree = repo.find_tree(id).unwrap();
let sig = repo.signature().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
.unwrap();
(tempdir, repo)
/// Find the latest commit in a repository.
pub fn get_latest_commit(repo: &git2::Repository) -> git2::Commit<'_> {
let head = repo.head().unwrap();
let commit = head.peel_to_commit().unwrap();
commit