1use crate::config::Config;
6use crate::error::Error;
7use std::ffi::OsStr;
8use std::fs::{create_dir_all, OpenOptions};
9use std::io::Write;
10use std::os::unix::fs::PermissionsExt;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14const HOOK_DIR_NAME: &'static str = "hooks";
15
16pub enum HookType {
18 BeforeEdit,
21
22 AfterEdit,
25
26 BeforeRollover,
29
30 AfterRollover,
34}
35
36impl HookType {
37 pub fn name(&self) -> String {
40 match self {
41 HookType::BeforeEdit => "before-edit",
42 HookType::AfterEdit => "after-edit",
43 HookType::BeforeRollover => "before-rollover",
44 HookType::AfterRollover => "after-rollover",
45 }
46 .to_string()
47 }
48}
49
50const ALL_HOOK_TYPES: &[HookType] = &[
51 HookType::BeforeEdit,
52 HookType::AfterEdit,
53 HookType::BeforeRollover,
54 HookType::AfterRollover,
55];
56
57const HOOK_TEMPLATE: &'static str = "#!/usr/bin/env sh
58# To enable this hook, make this file executable.
59echo \"$0 $@\"
60";
61
62pub fn init_hooks(repo_dir: &Path) -> Result<(), Error> {
65 let hook_dir = hook_dir_path(repo_dir);
66 create_dir_all(&hook_dir)?;
67 for hook_type in ALL_HOOK_TYPES {
68 let mut p = hook_dir.clone();
69 p.push(hook_type.name());
70
71 if !p.exists() {
72 let mut f = OpenOptions::new()
73 .create(true)
74 .truncate(true)
75 .write(true)
76 .open(&p)
77 .unwrap();
78 write!(f, "{}", HOOK_TEMPLATE)?;
79 }
80 }
81 Ok(())
82}
83
84pub fn execute_hook<W: Write>(
88 w: &mut W,
89 config: &Config,
90 hook_type: &HookType,
91 args: &[&OsStr],
92) -> Result<(), Error> {
93 if let Some(mut cmd) = hook_cmd(config.repo_dir(), hook_type)? {
94 let status = cmd.args(args).status()?;
95 if !status.success() {
96 if let Some(code) = status.code() {
97 write!(w, "{} hook exited with status {}\n", hook_type.name(), code)?;
98 }
99 }
100 }
101 Ok(())
102}
103
104pub fn hook_cmd(repo_dir: &Path, hook_type: &HookType) -> Result<Option<Command>, Error> {
106 let mut p = hook_dir_path(repo_dir);
107 p.push(hook_type.name());
108 is_valid(&p).map(|valid| if valid { Some(Command::new(&p)) } else { None })
109}
110
111fn hook_dir_path(repo_dir: &Path) -> PathBuf {
112 let mut p = repo_dir.to_path_buf();
113 p.push(HOOK_DIR_NAME);
114 p
115}
116
117fn is_valid(p: &Path) -> Result<bool, Error> {
118 Ok(p.exists() && is_executable(p)?)
119}
120
121fn is_executable(p: &Path) -> Result<bool, Error> {
122 p.metadata()
123 .map(|metadata| {
124 let perm = metadata.permissions();
125 perm.mode() & 0o111 != 0
126 })
127 .map_err(From::from)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use std::fs::{create_dir, set_permissions, File, Permissions};
134 use std::io::Read;
135 use std::os::unix::fs::PermissionsExt;
136 use tempfile::tempdir;
137
138 fn create_hook_dir(repo_dir: &Path) {
139 let mut hook_dir = repo_dir.to_path_buf();
140 hook_dir.push(HOOK_DIR_NAME);
141 create_dir(&hook_dir).unwrap();
142 }
143
144 fn create_hook_file(repo_dir: &Path, hook_type: HookType, executable: bool) {
145 let mut p = repo_dir.to_path_buf();
146 p.push(HOOK_DIR_NAME);
147 p.push(hook_type.name());
148
149 let mut f = OpenOptions::new()
150 .create(true)
151 .write(true)
152 .open(&p)
153 .unwrap();
154 write!(f, "#!/usr/bin/env sh\necho 'Hello world!'\n").unwrap();
155
156 if executable {
157 set_permissions(&p, Permissions::from_mode(0o555)).unwrap()
158 }
159 }
160
161 #[test]
162 fn test_init_hooks() {
163 let repo_dir = tempdir().unwrap();
164 init_hooks(repo_dir.path()).unwrap();
165
166 for hook_type in ALL_HOOK_TYPES {
168 let result = hook_cmd(repo_dir.path(), hook_type).unwrap();
169 assert!(result.is_none());
170 }
171
172 for hook_type in ALL_HOOK_TYPES {
174 let mut p = repo_dir.path().to_path_buf();
175 p.push(HOOK_DIR_NAME);
176 p.push(hook_type.name());
177 set_permissions(&p, Permissions::from_mode(0o555)).unwrap()
178 }
179
180 for hook_type in ALL_HOOK_TYPES {
182 let result = hook_cmd(repo_dir.path(), hook_type).unwrap();
183 assert!(result.is_some());
184
185 let mut cmd = result.unwrap();
186 let status = cmd.status().unwrap();
187 assert!(status.success())
188 }
189 }
190
191 #[test]
192 fn test_init_hooks_some_already_exist() {
193 let repo_dir = tempdir().unwrap();
194
195 let mut p = repo_dir.path().to_path_buf();
197 p.push(HOOK_DIR_NAME);
198 create_dir_all(&p).unwrap();
199
200 p.push(HookType::AfterEdit.name());
201 let mut f = File::create(&p).unwrap();
202 write!(f, "existing hook").unwrap();
203
204 init_hooks(repo_dir.path()).unwrap();
206
207 for hook_type in ALL_HOOK_TYPES {
209 let mut p = repo_dir.path().to_path_buf();
210 p.push(HOOK_DIR_NAME);
211 p.push(hook_type.name());
212 assert!(p.exists());
213 }
214
215 let mut f = File::open(&p).unwrap();
217 let mut s = String::new();
218 f.read_to_string(&mut s).unwrap();
219 assert_eq!(s, "existing hook");
220 }
221
222 #[test]
223 fn test_hook_dir_does_not_exist() {
224 let repo_dir = tempdir().unwrap();
225 let result = hook_cmd(repo_dir.path(), &HookType::BeforeEdit).unwrap();
226 assert!(result.is_none());
227 }
228
229 #[test]
230 fn test_hook_file_does_not_exist() {
231 let repo_dir = tempdir().unwrap();
232 create_hook_dir(repo_dir.path());
233 let result = hook_cmd(repo_dir.path(), &HookType::BeforeEdit).unwrap();
234 assert!(result.is_none());
235 }
236
237 #[test]
238 fn test_hook_file_is_not_executable() {
239 let repo_dir = tempdir().unwrap();
240 create_hook_dir(repo_dir.path());
241 create_hook_file(repo_dir.path(), HookType::BeforeEdit, false);
242 let result = hook_cmd(repo_dir.path(), &HookType::BeforeEdit).unwrap();
243 assert!(result.is_none());
244 }
245
246 #[test]
247 fn test_hook_valid_hook_cmd() {
248 let repo_dir = tempdir().unwrap();
249 create_hook_dir(repo_dir.path());
250 create_hook_file(repo_dir.path(), HookType::BeforeEdit, true);
251
252 let result = hook_cmd(repo_dir.path(), &HookType::BeforeEdit).unwrap();
253 assert!(result.is_some());
254
255 let mut cmd = result.unwrap();
256 let status = cmd.status().unwrap();
257 assert!(status.success())
258 }
259}