Lines
98.89 %
Functions
66.67 %
Branches
100 %
use std::fs::{File, Permissions};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
/// Hook types used in git.
#[derive(Debug)]
enum GitHookType {
PostMerge,
PostRewrite,
}
/// Implementation of to_string() for HookType. This function converts HookType
/// to a string. This is used to create the hook file name. For example,
/// HookType::PreCommit will be converted to "pre-commit".
impl std::fmt::Display for GitHookType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
GitHookType::PostMerge => write!(f, "post-merge"),
GitHookType::PostRewrite => write!(f, "post-rewrite"),
/// Representation of a git hook.
pub struct GitHook {
/// List of hook types to install.
types: Vec<GitHookType>,
/// Hook script.
script: String,
impl GitHook {
/// Default git-snip hook script.
pub fn default() -> Self {
Self {
types: vec![GitHookType::PostMerge, GitHookType::PostRewrite],
script: String::from(
"\
#!/bin/sh
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
case \"$HEAD_BRANCH\" in
'main'|'master'|'develop') ;;
*) exit ;;
esac
git snip --yes
",
),
/// Install the hook script.
pub fn install(&self, repository_path: &Path) -> std::io::Result<()> {
let hook_dir = repository_path.join("hooks");
for hook_type in &self.types {
let hook_path = hook_dir.join(hook_type.to_string());
// Return error if the hook already exists.
// This is to prevent overwriting existing hooks.
if hook_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("Hook already exists: {:?}", hook_path),
));
let mut file = File::create(&hook_path)?;
file.write_all(self.script.as_bytes())?;
file.set_permissions(Permissions::from_mode(0o755))?;
Ok(())
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utilities;
#[test]
fn test_hook_type_to_string() {
// GIVEN a HookType
let hook_type = GitHookType::PostMerge;
// WHEN converting HookType to a string
let actual = hook_type.to_string();
// THEN the result should be what is expected
assert_eq!(actual, "post-merge");
fn test_git_hook_install() {
// GIVEN a hook path and a hook script
let (_testdir, repo) = test_utilities::create_mock_repo();
let mock_script = String::from("echo 'Hello, world!'");
// WHEN installing the hook
let hook = GitHook {
types: vec![GitHookType::PostMerge],
script: mock_script.clone(),
};
let result = hook.install(repo.path());
// THEN the installation should be successful
assert!(result.is_ok());
let hook_path = &repo
.path()
.to_path_buf()
.join("hooks")
.join(GitHookType::PostMerge.to_string());
let hook_script = std::fs::read_to_string(hook_path).unwrap();
assert_eq!(hook_script, hook.script);
fn test_git_hook_install_already_exists() {
// GIVEN an existing hook
let _ = File::create(&hook_path).unwrap();
let hook = GitHook::default();
// THEN the installation should fail
assert!(result.is_err());