use std::fmt::{Display, Formatter, Result};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::str::FromStr;
use anyhow::{bail, Context};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum GitHookType {
PostMerge,
PostRewrite,
}
impl Display for GitHookType {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
GitHookType::PostMerge => write!(f, "post-merge"),
GitHookType::PostRewrite => write!(f, "post-rewrite"),
}
}
}
impl FromStr for GitHookType {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"post-merge" => Ok(GitHookType::PostMerge),
"post-rewrite" => Ok(GitHookType::PostRewrite),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GitHook(String);
impl GitHook {
#[allow(dead_code)]
pub fn new<S: Into<String>>(hook: S) -> Self {
Self(hook.into())
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
#[allow(dead_code)]
pub fn into_string(self) -> String {
self.0
}
}
impl Default for GitHook {
fn default() -> Self {
Self(
r#"#!/bin/sh
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
case "$HEAD_BRANCH" in
'main'|'master'|'develop') ;;
*) exit ;;
esac
git snip run --yes
"#
.to_string(),
)
}
}
impl AsRef<str> for GitHook {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for GitHook {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for GitHook {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.0.fmt(f)
}
}
pub fn install(
git_dir: &Path,
hook: &GitHook,
hook_type: GitHookType,
force: bool,
) -> anyhow::Result<()> {
let hook_path = git_dir.join("hooks").join(hook_type.to_string());
if !force && hook_path.exists() {
bail!("Hook already exists at {}", hook_path.to_string_lossy());
}
let mut file = File::create(&hook_path).context("Failed to create hook file")?;
file.write_all(hook.as_bytes())
.context("Failed to write hook script")?;
#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
file.set_permissions(Permissions::from_mode(0o755))
.context("Failed to set permissions on hook file")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_type_display() {
assert_eq!(GitHookType::PostMerge.to_string(), "post-merge");
assert_eq!(GitHookType::PostRewrite.to_string(), "post-rewrite");
}
#[test]
fn test_hook_type_from_str() {
assert_eq!("post-merge".parse(), Ok(GitHookType::PostMerge));
assert_eq!("post-rewrite".parse(), Ok(GitHookType::PostRewrite));
assert!("other-hook".parse::<GitHookType>().is_err());
}
#[test]
fn test_hook_as_bytes() {
let hook = GitHook::new("echo 'Hello, world!'");
assert_eq!(hook.as_bytes(), b"echo 'Hello, world!'");
}
#[test]
fn test_into_string() {
let hook = GitHook::new("echo 'Hello, world!'");
assert_eq!(hook.into_string(), "echo 'Hello, world!'");
}
#[test]
fn test_as_ref_and_deref() {
let hook = GitHook::new("echo 'Hi'");
assert_eq!(hook.as_ref(), "echo 'Hi'");
assert_eq!(&*hook, "echo 'Hi'");
}
#[test]
fn test_display() {
let hook = GitHook::new("echo 'Hi'");
assert_eq!(hook.to_string(), "echo 'Hi'");
}
#[test]
fn test_default() {
let hook = GitHook::default();
assert!(hook.as_ref().contains("git snip run --yes"));
}
#[test]
fn test_install() {
let tempdir = tempfile::tempdir().unwrap();
let hooks_dir = tempdir.path().join("hooks");
std::fs::create_dir(&hooks_dir).unwrap();
let mock_script = String::from("echo 'Hello, world!'");
let hook = GitHook::new(mock_script);
let result = install(tempdir.path(), &hook, GitHookType::PostMerge, false);
assert!(result.is_ok());
let hook_path = hooks_dir.join(GitHookType::PostMerge.to_string());
let hook_script = std::fs::read_to_string(hook_path).unwrap();
assert_eq!(hook_script, hook.into_string());
}
#[test]
fn test_install_already_exists() {
let tempdir = tempfile::tempdir().unwrap();
let hooks_dir = tempdir.path().join("hooks");
std::fs::create_dir(&hooks_dir).unwrap();
let hook_path = hooks_dir.join(GitHookType::PostMerge.to_string());
std::fs::File::create(&hook_path).unwrap();
let hook = GitHook::default();
let result = install(tempdir.path(), &hook, GitHookType::PostMerge, false);
assert!(result.is_err());
}
#[test]
fn test_install_force_overwrites_existing() {
let tempdir = tempfile::tempdir().unwrap();
let hooks_dir = tempdir.path().join("hooks");
std::fs::create_dir(&hooks_dir).unwrap();
let hook_path = hooks_dir.join(GitHookType::PostMerge.to_string());
std::fs::write(&hook_path, "old content").unwrap();
let hook = GitHook::new("new content");
let result = install(tempdir.path(), &hook, GitHookType::PostMerge, true);
assert!(result.is_ok());
let hook_script = std::fs::read_to_string(hook_path).unwrap();
assert_eq!(hook_script, "new content");
}
}