@@ -2,6 +2,10 @@ use super::{repository::repo, RepoPath};
2
2
use crate :: error:: Result ;
3
3
pub use git2_hooks:: PrepareCommitMsgSource ;
4
4
use scopetime:: scope_time;
5
+ use std:: {
6
+ sync:: mpsc:: { channel, RecvTimeoutError } ,
7
+ time:: Duration ,
8
+ } ;
5
9
6
10
///
7
11
#[ derive( Debug , PartialEq , Eq ) ]
@@ -26,50 +30,148 @@ impl From<git2_hooks::HookResult> for HookResult {
26
30
}
27
31
}
28
32
33
+ fn run_with_timeout < F > (
34
+ f : F ,
35
+ timeout : Duration ,
36
+ ) -> Result < ( HookResult , Option < String > ) >
37
+ where
38
+ F : FnOnce ( ) -> Result < ( HookResult , Option < String > ) >
39
+ + Send
40
+ + Sync
41
+ + ' static ,
42
+ {
43
+ if timeout. is_zero ( ) {
44
+ return f ( ) ; // Don't bother with threads if we don't have a timeout
45
+ }
46
+
47
+ let ( tx, rx) = channel ( ) ;
48
+ let _ = std:: thread:: spawn ( move || {
49
+ let result = f ( ) ;
50
+ tx. send ( result)
51
+ } ) ;
52
+
53
+ match rx. recv_timeout ( timeout) {
54
+ Ok ( result) => result,
55
+ Err ( RecvTimeoutError :: Timeout ) => Ok ( (
56
+ HookResult :: NotOk ( "hook timed out" . to_string ( ) ) ,
57
+ None ,
58
+ ) ) ,
59
+ Err ( RecvTimeoutError :: Disconnected ) => {
60
+ unreachable ! ( )
61
+ }
62
+ }
63
+ }
64
+
29
65
/// see `git2_hooks::hooks_commit_msg`
30
66
pub fn hooks_commit_msg (
31
67
repo_path : & RepoPath ,
32
68
msg : & mut String ,
69
+ timeout : Duration ,
33
70
) -> Result < HookResult > {
34
71
scope_time ! ( "hooks_commit_msg" ) ;
35
72
36
- let repo = repo ( repo_path) ?;
73
+ let repo_path = repo_path. clone ( ) ;
74
+ let mut msg_clone = msg. clone ( ) ;
75
+ let ( result, msg_opt) = run_with_timeout (
76
+ move || {
77
+ let repo = repo ( & repo_path) ?;
78
+ Ok ( (
79
+ git2_hooks:: hooks_commit_msg (
80
+ & repo,
81
+ None ,
82
+ & mut msg_clone,
83
+ ) ?
84
+ . into ( ) ,
85
+ Some ( msg_clone) ,
86
+ ) )
87
+ } ,
88
+ timeout,
89
+ ) ?;
37
90
38
- Ok ( git2_hooks:: hooks_commit_msg ( & repo, None , msg) ?. into ( ) )
91
+ if let Some ( updated_msg) = msg_opt {
92
+ msg. clear ( ) ;
93
+ msg. push_str ( & updated_msg) ;
94
+ }
95
+
96
+ Ok ( result)
39
97
}
40
98
41
99
/// see `git2_hooks::hooks_pre_commit`
42
- pub fn hooks_pre_commit ( repo_path : & RepoPath ) -> Result < HookResult > {
100
+ pub fn hooks_pre_commit (
101
+ repo_path : & RepoPath ,
102
+ timeout : Duration ,
103
+ ) -> Result < HookResult > {
43
104
scope_time ! ( "hooks_pre_commit" ) ;
44
105
45
- let repo = repo ( repo_path) ?;
46
-
47
- Ok ( git2_hooks:: hooks_pre_commit ( & repo, None ) ?. into ( ) )
106
+ let repo_path = repo_path. clone ( ) ;
107
+ run_with_timeout (
108
+ move || {
109
+ let repo = repo ( & repo_path) ?;
110
+ Ok ( (
111
+ git2_hooks:: hooks_pre_commit ( & repo, None ) ?. into ( ) ,
112
+ None ,
113
+ ) )
114
+ } ,
115
+ timeout,
116
+ )
117
+ . map ( |res| res. 0 )
48
118
}
49
119
50
120
/// see `git2_hooks::hooks_post_commit`
51
- pub fn hooks_post_commit ( repo_path : & RepoPath ) -> Result < HookResult > {
121
+ pub fn hooks_post_commit (
122
+ repo_path : & RepoPath ,
123
+ timeout : Duration ,
124
+ ) -> Result < HookResult > {
52
125
scope_time ! ( "hooks_post_commit" ) ;
53
126
54
- let repo = repo ( repo_path) ?;
55
-
56
- Ok ( git2_hooks:: hooks_post_commit ( & repo, None ) ?. into ( ) )
127
+ let repo_path = repo_path. clone ( ) ;
128
+ run_with_timeout (
129
+ move || {
130
+ let repo = repo ( & repo_path) ?;
131
+ Ok ( (
132
+ git2_hooks:: hooks_post_commit ( & repo, None ) ?. into ( ) ,
133
+ None ,
134
+ ) )
135
+ } ,
136
+ timeout,
137
+ )
138
+ . map ( |res| res. 0 )
57
139
}
58
140
59
141
/// see `git2_hooks::hooks_prepare_commit_msg`
60
142
pub fn hooks_prepare_commit_msg (
61
143
repo_path : & RepoPath ,
62
144
source : PrepareCommitMsgSource ,
63
145
msg : & mut String ,
146
+ timeout : Duration ,
64
147
) -> Result < HookResult > {
65
148
scope_time ! ( "hooks_prepare_commit_msg" ) ;
66
149
67
- let repo = repo ( repo_path) ?;
150
+ let repo_path = repo_path. clone ( ) ;
151
+ let mut msg_cloned = msg. clone ( ) ;
152
+ let ( result, msg_opt) = run_with_timeout (
153
+ move || {
154
+ let repo = repo ( & repo_path) ?;
155
+ Ok ( (
156
+ git2_hooks:: hooks_prepare_commit_msg (
157
+ & repo,
158
+ None ,
159
+ source,
160
+ & mut msg_cloned,
161
+ ) ?
162
+ . into ( ) ,
163
+ Some ( msg_cloned) ,
164
+ ) )
165
+ } ,
166
+ timeout,
167
+ ) ?;
168
+
169
+ if let Some ( updated_msg) = msg_opt {
170
+ msg. clear ( ) ;
171
+ msg. push_str ( & updated_msg) ;
172
+ }
68
173
69
- Ok ( git2_hooks:: hooks_prepare_commit_msg (
70
- & repo, None , source, msg,
71
- ) ?
72
- . into ( ) )
174
+ Ok ( result)
73
175
}
74
176
75
177
#[ cfg( test) ]
@@ -82,7 +184,7 @@ mod tests {
82
184
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
83
185
let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
84
186
85
- let hook = b"#!/bin/sh
187
+ let hook = b"#!/usr/ bin/env sh
86
188
echo 'rejected'
87
189
exit 1
88
190
" ;
@@ -96,9 +198,11 @@ mod tests {
96
198
let subfolder = root. join ( "foo/" ) ;
97
199
std:: fs:: create_dir_all ( & subfolder) . unwrap ( ) ;
98
200
99
- let res =
100
- hooks_post_commit ( & subfolder. to_str ( ) . unwrap ( ) . into ( ) )
101
- . unwrap ( ) ;
201
+ let res = hooks_post_commit (
202
+ & subfolder. to_str ( ) . unwrap ( ) . into ( ) ,
203
+ Duration :: ZERO ,
204
+ )
205
+ . unwrap ( ) ;
102
206
103
207
assert_eq ! (
104
208
res,
@@ -119,7 +223,7 @@ mod tests {
119
223
let workdir =
120
224
crate :: sync:: utils:: repo_work_dir ( repo_path) . unwrap ( ) ;
121
225
122
- let hook = b"#!/bin/sh
226
+ let hook = b"#!/usr/ bin/env sh
123
227
echo $(pwd)
124
228
exit 1
125
229
" ;
@@ -129,7 +233,8 @@ mod tests {
129
233
git2_hooks:: HOOK_PRE_COMMIT ,
130
234
hook,
131
235
) ;
132
- let res = hooks_pre_commit ( repo_path) . unwrap ( ) ;
236
+ let res =
237
+ hooks_pre_commit ( repo_path, Duration :: ZERO ) . unwrap ( ) ;
133
238
if let HookResult :: NotOk ( res) = res {
134
239
assert_eq ! (
135
240
std:: path:: Path :: new( res. trim_end( ) ) ,
@@ -145,7 +250,7 @@ mod tests {
145
250
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
146
251
let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
147
252
148
- let hook = b"#!/bin/sh
253
+ let hook = b"#!/usr/ bin/env sh
149
254
echo 'msg' > $1
150
255
echo 'rejected'
151
256
exit 1
@@ -164,6 +269,7 @@ mod tests {
164
269
let res = hooks_commit_msg (
165
270
& subfolder. to_str ( ) . unwrap ( ) . into ( ) ,
166
271
& mut msg,
272
+ Duration :: ZERO ,
167
273
)
168
274
. unwrap ( ) ;
169
275
@@ -174,4 +280,53 @@ mod tests {
174
280
175
281
assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
176
282
}
283
+
284
+ #[ test]
285
+ fn test_hooks_respect_timeout ( ) {
286
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
287
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
288
+
289
+ let hook = b"#!/usr/bin/env sh
290
+ sleep 1
291
+ " ;
292
+
293
+ git2_hooks:: create_hook (
294
+ & repo,
295
+ git2_hooks:: HOOK_COMMIT_MSG ,
296
+ hook,
297
+ ) ;
298
+
299
+ let res = hooks_pre_commit (
300
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
301
+ Duration :: ZERO ,
302
+ )
303
+ . unwrap ( ) ;
304
+
305
+ assert_eq ! ( res, HookResult :: Ok ) ;
306
+ }
307
+
308
+ #[ test]
309
+ fn test_hooks_timeout_zero ( ) {
310
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
311
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
312
+
313
+ let hook = b"#!/usr/bin/env sh
314
+ sleep 1
315
+ " ;
316
+
317
+ git2_hooks:: create_hook (
318
+ & repo,
319
+ git2_hooks:: HOOK_COMMIT_MSG ,
320
+ hook,
321
+ ) ;
322
+
323
+ let res = hooks_commit_msg (
324
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
325
+ & mut String :: new ( ) ,
326
+ Duration :: ZERO ,
327
+ )
328
+ . unwrap ( ) ;
329
+
330
+ assert_eq ! ( res, HookResult :: Ok ) ;
331
+ }
177
332
}
0 commit comments