hooksmith/
lib.rs

1pub mod commands;
2
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use clap::Parser;
9use commands::Command;
10use serde::Deserialize;
11
12/// Root directory of a Git repository.
13pub const GIT_ROOT: &str = ".git";
14
15/// # `Cli`
16/// Command line interface structure for hooksmith.
17#[derive(Parser)]
18#[command(about = "A trivial Git hooks utility.")]
19#[command(author = "@TomPlanche")]
20#[command(name = "hooksmith")]
21pub struct Cli {
22    /// Command to execute
23    #[command(subcommand)]
24    pub command: Command,
25
26    /// Path to the hooksmith.yaml file
27    #[arg(short, long, default_value_t = String::from("hooksmith.yaml"))]
28    pub config_path: String,
29
30    /// Whether to print verbose output
31    #[arg(short, long, default_value_t = false)]
32    pub verbose: bool,
33
34    /// Whether to perform a dry run
35    #[arg(long, default_value_t = false)]
36    pub dry_run: bool,
37}
38
39/// Configuration structure for hooksmith.
40#[derive(Deserialize)]
41pub struct Config {
42    #[serde(flatten)]
43    hooks: std::collections::HashMap<String, Hook>,
44}
45
46/// Hook structure for hooksmith.
47#[derive(Deserialize)]
48pub struct Hook {
49    commands: Vec<String>,
50}
51
52/// # `get_git_hooks_path`
53/// Get the path to the Git hooks directory.
54///
55/// ## Errors
56/// * If the `git` command fails to execute
57///
58/// ## Returns
59/// * `PathBuf` - Path to the Git hooks directory
60pub fn get_git_hooks_path() -> std::io::Result<PathBuf> {
61    // get the output of the `git rev-parse --git-path hooks` command
62    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/// # `check_for_git_hooks`
74/// Check if the current directory is a Git repository and if it has hooks.
75///
76/// ## Arguments
77/// * `path` - Path to the directory to check
78///
79/// ## Returns
80/// * `bool` - True if the directory is a Git repository with hooks, false otherwise
81#[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
89/// # `read_config`
90/// Read the configuration file and parse it into a Config struct.
91///
92/// ## Arguments
93/// * `config_path` - Path to the configuration file
94///
95/// ## Errors
96/// * If the configuration file cannot be read or parsed
97///
98/// ## Returns
99/// * `Config` - Parsed configuration file
100pub 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
108/// # `init`
109/// Initialize Hooksmith by reading the configuration file and installing hooks.
110///
111/// ## Arguments
112/// * `config_path` - Path to the configuration file
113///
114/// ## Errors
115/// * If the configuration file cannot be read or parsed
116///
117/// ## Returns
118/// * `Config` - Parsed configuration file
119pub 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}