1pub mod commands;
2
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use clap::Parser;
9use commands::Command;
10use serde::Deserialize;
11
12pub const GIT_ROOT: &str = ".git";
14
15#[derive(Parser)]
18#[command(about = "A trivial Git hooks utility.")]
19#[command(author = "@TomPlanche")]
20#[command(name = "hooksmith")]
21pub struct Cli {
22    #[command(subcommand)]
24    pub command: Command,
25
26    #[arg(short, long, default_value_t = String::from("hooksmith.yaml"))]
28    pub config_path: String,
29
30    #[arg(short, long, default_value_t = false)]
32    pub verbose: bool,
33
34    #[arg(long, default_value_t = false)]
36    pub dry_run: bool,
37}
38
39#[derive(Deserialize)]
41pub struct Config {
42    #[serde(flatten)]
43    hooks: std::collections::HashMap<String, Hook>,
44}
45
46#[derive(Deserialize)]
48pub struct Hook {
49    commands: Vec<String>,
50}
51
52pub fn get_git_hooks_path() -> std::io::Result<PathBuf> {
61    let output = std::process::Command::new("git")
63        .arg("rev-parse")
64        .arg("--git-path")
65        .arg("hooks")
66        .output()?;
67
68    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
69
70    Ok(PathBuf::from(path))
71}
72
73#[must_use]
82pub fn check_for_git_hooks() -> bool {
83    let git_root = Path::new(GIT_ROOT);
84    let git_hooks = get_git_hooks_path().ok();
85
86    git_root.exists() && git_hooks.is_some_and(|path| path.exists())
87}
88
89pub fn read_config(config_path: &Path) -> std::io::Result<Config> {
101    let config_string = fs::read_to_string(config_path)?;
102    let config = serde_yaml::from_str(&config_string)
103        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
104
105    Ok(config)
106}
107
108pub fn init(config_path: &Path) -> std::io::Result<()> {
120    let config = read_config(config_path)?;
121    let dry_run = false;
122    let verbose = false;
123
124    commands::install_hooks(&config, dry_run, verbose)?;
125
126    Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_read_config() -> std::io::Result<()> {
135        let config_path = Path::new("hooksmith.yaml");
136        let config = read_config(config_path)?;
137
138        assert!(config.hooks.contains_key("pre-commit"));
139        assert!(config.hooks.contains_key("pre-push"));
140        Ok(())
141    }
142}