@@ -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
+ ) ?;
90
+
91
+ if let Some ( updated_msg) = msg_opt {
92
+ msg. clear ( ) ;
93
+ msg. push_str ( & updated_msg) ;
94
+ }
37
95
38
- Ok ( git2_hooks :: hooks_commit_msg ( & repo , None , msg ) ? . into ( ) )
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) ]
@@ -99,7 +201,7 @@ mod tests {
99
201
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
100
202
let root = repo. workdir ( ) . unwrap ( ) ;
101
203
102
- let hook = b"#!/bin/sh
204
+ let hook = b"#!/usr/ bin/env sh
103
205
echo 'rejected'
104
206
exit 1
105
207
" ;
@@ -113,9 +215,11 @@ mod tests {
113
215
let subfolder = root. join ( "foo/" ) ;
114
216
std:: fs:: create_dir_all ( & subfolder) . unwrap ( ) ;
115
217
116
- let res =
117
- hooks_post_commit ( & subfolder. to_str ( ) . unwrap ( ) . into ( ) )
118
- . unwrap ( ) ;
218
+ let res = hooks_post_commit (
219
+ & subfolder. to_str ( ) . unwrap ( ) . into ( ) ,
220
+ Duration :: ZERO ,
221
+ )
222
+ . unwrap ( ) ;
119
223
120
224
assert_eq ! (
121
225
res,
@@ -136,7 +240,7 @@ mod tests {
136
240
let workdir =
137
241
crate :: sync:: utils:: repo_work_dir ( repo_path) . unwrap ( ) ;
138
242
139
- let hook = b"#!/bin/sh
243
+ let hook = b"#!/usr/ bin/env sh
140
244
echo $(pwd)
141
245
exit 1
142
246
" ;
@@ -146,7 +250,8 @@ mod tests {
146
250
git2_hooks:: HOOK_PRE_COMMIT ,
147
251
hook,
148
252
) ;
149
- let res = hooks_pre_commit ( repo_path) . unwrap ( ) ;
253
+ let res =
254
+ hooks_pre_commit ( repo_path, Duration :: ZERO ) . unwrap ( ) ;
150
255
if let HookResult :: NotOk ( res) = res {
151
256
assert_eq ! (
152
257
std:: path:: Path :: new( res. trim_end( ) ) ,
@@ -162,7 +267,7 @@ mod tests {
162
267
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
163
268
let root = repo. workdir ( ) . unwrap ( ) ;
164
269
165
- let hook = b"#!/bin/sh
270
+ let hook = b"#!/usr/ bin/env sh
166
271
echo 'msg' > $1
167
272
echo 'rejected'
168
273
exit 1
@@ -181,6 +286,7 @@ mod tests {
181
286
let res = hooks_commit_msg (
182
287
& subfolder. to_str ( ) . unwrap ( ) . into ( ) ,
183
288
& mut msg,
289
+ Duration :: ZERO ,
184
290
)
185
291
. unwrap ( ) ;
186
292
@@ -224,4 +330,53 @@ mod tests {
224
330
225
331
assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
226
332
}
333
+
334
+ #[ test]
335
+ fn test_hooks_respect_timeout ( ) {
336
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
337
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
338
+
339
+ let hook = b"#!/usr/bin/env sh
340
+ sleep 1
341
+ " ;
342
+
343
+ git2_hooks:: create_hook (
344
+ & repo,
345
+ git2_hooks:: HOOK_COMMIT_MSG ,
346
+ hook,
347
+ ) ;
348
+
349
+ let res = hooks_pre_commit (
350
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
351
+ Duration :: ZERO ,
352
+ )
353
+ . unwrap ( ) ;
354
+
355
+ assert_eq ! ( res, HookResult :: Ok ) ;
356
+ }
357
+
358
+ #[ test]
359
+ fn test_hooks_timeout_zero ( ) {
360
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
361
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
362
+
363
+ let hook = b"#!/usr/bin/env sh
364
+ sleep 1
365
+ " ;
366
+
367
+ git2_hooks:: create_hook (
368
+ & repo,
369
+ git2_hooks:: HOOK_COMMIT_MSG ,
370
+ hook,
371
+ ) ;
372
+
373
+ let res = hooks_commit_msg (
374
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
375
+ & mut String :: new ( ) ,
376
+ Duration :: ZERO ,
377
+ )
378
+ . unwrap ( ) ;
379
+
380
+ assert_eq ! ( res, HookResult :: Ok ) ;
381
+ }
227
382
}
0 commit comments