1
use std::fs::{File, Permissions};
2
use std::io::Write;
3
use std::os::unix::fs::PermissionsExt;
4
use std::path::Path;
5

            
6
/// Hook types used in git.
7
#[derive(Debug)]
8
enum GitHookType {
9
    PostMerge,
10
    PostRewrite,
11
}
12

            
13
/// Implementation of to_string() for HookType. This function converts HookType
14
/// to a string. This is used to create the hook file name. For example,
15
/// HookType::PreCommit will be converted to "pre-commit".
16
impl std::fmt::Display for GitHookType {
17
10
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
18
10
        match self {
19
10
            GitHookType::PostMerge => write!(f, "post-merge"),
20
            GitHookType::PostRewrite => write!(f, "post-rewrite"),
21
        }
22
10
    }
23
}
24

            
25
/// Representation of a git hook.
26
#[derive(Debug)]
27
pub struct GitHook {
28
    /// List of hook types to install.
29
    types: Vec<GitHookType>,
30
    /// Hook script.
31
    script: String,
32
}
33

            
34
impl GitHook {
35
    /// Default git-snip hook script.
36
2
    pub fn default() -> Self {
37
2
        Self {
38
2
            types: vec![GitHookType::PostMerge, GitHookType::PostRewrite],
39
2
            script: String::from(
40
2
                "\
41
2
#!/bin/sh
42
2
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
43
2
case \"$HEAD_BRANCH\" in
44
2
    'main'|'master'|'develop') ;;
45
2
        *) exit ;;
46
2
        esac
47
2

            
48
2
        git snip --yes
49
2

            
50
2
",
51
2
            ),
52
2
        }
53
2
    }
54

            
55
    /// Install the hook script.
56
4
    pub fn install(&self, repository_path: &Path) -> std::io::Result<()> {
57
4
        let hook_dir = repository_path.join("hooks");
58
6
        for hook_type in &self.types {
59
4
            let hook_path = hook_dir.join(hook_type.to_string());
60
4

            
61
4
            // Return error if the hook already exists.
62
4
            // This is to prevent overwriting existing hooks.
63
4
            if hook_path.exists() {
64
2
                return Err(std::io::Error::new(
65
2
                    std::io::ErrorKind::AlreadyExists,
66
2
                    format!("Hook already exists: {:?}", hook_path),
67
2
                ));
68
2
            }
69

            
70
2
            let mut file = File::create(&hook_path)?;
71
2
            file.write_all(self.script.as_bytes())?;
72
2
            file.set_permissions(Permissions::from_mode(0o755))?;
73
        }
74
2
        Ok(())
75
4
    }
76
}
77

            
78
#[cfg(test)]
79
mod tests {
80
    use super::*;
81

            
82
    use crate::test_utilities;
83

            
84
    #[test]
85
2
    fn test_hook_type_to_string() {
86
2
        // GIVEN a HookType
87
2
        let hook_type = GitHookType::PostMerge;
88
2

            
89
2
        // WHEN converting HookType to a string
90
2
        let actual = hook_type.to_string();
91
2

            
92
2
        // THEN the result should be what is expected
93
2
        assert_eq!(actual, "post-merge");
94
2
    }
95

            
96
    #[test]
97
2
    fn test_git_hook_install() {
98
2
        // GIVEN a hook path and a hook script
99
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
100
2
        let mock_script = String::from("echo 'Hello, world!'");
101
2

            
102
2
        // WHEN installing the hook
103
2
        let hook = GitHook {
104
2
            types: vec![GitHookType::PostMerge],
105
2
            script: mock_script.clone(),
106
2
        };
107
2
        let result = hook.install(repo.path());
108
2

            
109
2
        // THEN the installation should be successful
110
2
        assert!(result.is_ok());
111
2
        let hook_path = &repo
112
2
            .path()
113
2
            .to_path_buf()
114
2
            .join("hooks")
115
2
            .join(GitHookType::PostMerge.to_string());
116
2
        let hook_script = std::fs::read_to_string(hook_path).unwrap();
117
2
        assert_eq!(hook_script, hook.script);
118
2
    }
119

            
120
    #[test]
121
2
    fn test_git_hook_install_already_exists() {
122
2
        // GIVEN an existing hook
123
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
124
2
        let hook_path = &repo
125
2
            .path()
126
2
            .to_path_buf()
127
2
            .join("hooks")
128
2
            .join(GitHookType::PostMerge.to_string());
129
2
        let _ = File::create(&hook_path).unwrap();
130
2

            
131
2
        // WHEN installing the hook
132
2
        let hook = GitHook::default();
133
2
        let result = hook.install(repo.path());
134
2

            
135
2
        // THEN the installation should fail
136
2
        assert!(result.is_err());
137
2
    }
138
}