@@ -2,6 +2,7 @@ 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:: time:: Duration ;
5
6
6
7
///
7
8
#[ derive( Debug , PartialEq , Eq ) ]
@@ -10,6 +11,13 @@ pub enum HookResult {
10
11
Ok ,
11
12
/// Hook returned error
12
13
NotOk ( String ) ,
14
+ /// Hook timed out
15
+ TimedOut {
16
+ /// Stdout
17
+ stdout : String ,
18
+ /// Stderr
19
+ stderr : String ,
20
+ } ,
13
21
}
14
22
15
23
impl From < git2_hooks:: HookResult > for HookResult {
@@ -22,6 +30,11 @@ impl From<git2_hooks::HookResult> for HookResult {
22
30
stderr,
23
31
..
24
32
} => Self :: NotOk ( format ! ( "{stdout}{stderr}" ) ) ,
33
+ git2_hooks:: HookResult :: TimedOut {
34
+ stdout,
35
+ stderr,
36
+ ..
37
+ } => Self :: TimedOut { stdout, stderr } ,
25
38
}
26
39
}
27
40
}
@@ -30,30 +43,66 @@ impl From<git2_hooks::HookResult> for HookResult {
30
43
pub fn hooks_commit_msg (
31
44
repo_path : & RepoPath ,
32
45
msg : & mut String ,
46
+ ) -> Result < HookResult > {
47
+ hooks_commit_msg_with_timeout ( repo_path, msg, None )
48
+ }
49
+
50
+ /// see `git2_hooks::hooks_commit_msg`
51
+ #[ allow( unused) ]
52
+ pub fn hooks_commit_msg_with_timeout (
53
+ repo_path : & RepoPath ,
54
+ msg : & mut String ,
55
+ timeout : Option < Duration > ,
33
56
) -> Result < HookResult > {
34
57
scope_time ! ( "hooks_commit_msg" ) ;
35
58
36
59
let repo = repo ( repo_path) ?;
37
-
38
- Ok ( git2_hooks:: hooks_commit_msg ( & repo, None , msg) ?. into ( ) )
60
+ Ok ( git2_hooks:: hooks_commit_msg_with_timeout (
61
+ & repo, None , msg, timeout,
62
+ ) ?
63
+ . into ( ) )
39
64
}
40
65
41
66
/// see `git2_hooks::hooks_pre_commit`
42
67
pub fn hooks_pre_commit ( repo_path : & RepoPath ) -> Result < HookResult > {
68
+ hooks_pre_commit_with_timeout ( repo_path, None )
69
+ }
70
+
71
+ /// see `git2_hooks::hooks_pre_commit`
72
+ #[ allow( unused) ]
73
+ pub fn hooks_pre_commit_with_timeout (
74
+ repo_path : & RepoPath ,
75
+ timeout : Option < Duration > ,
76
+ ) -> Result < HookResult > {
43
77
scope_time ! ( "hooks_pre_commit" ) ;
44
78
45
79
let repo = repo ( repo_path) ?;
46
80
47
- Ok ( git2_hooks:: hooks_pre_commit ( & repo, None ) ?. into ( ) )
81
+ Ok ( git2_hooks:: hooks_pre_commit_with_timeout (
82
+ & repo, None , timeout,
83
+ ) ?
84
+ . into ( ) )
48
85
}
49
86
50
87
/// see `git2_hooks::hooks_post_commit`
51
88
pub fn hooks_post_commit ( repo_path : & RepoPath ) -> Result < HookResult > {
89
+ hooks_post_commit_with_timeout ( repo_path, None )
90
+ }
91
+
92
+ /// see `git2_hooks::hooks_post_commit`
93
+ #[ allow( unused) ]
94
+ pub fn hooks_post_commit_with_timeout (
95
+ repo_path : & RepoPath ,
96
+ timeout : Option < Duration > ,
97
+ ) -> Result < HookResult > {
52
98
scope_time ! ( "hooks_post_commit" ) ;
53
99
54
100
let repo = repo ( repo_path) ?;
55
101
56
- Ok ( git2_hooks:: hooks_post_commit ( & repo, None ) ?. into ( ) )
102
+ Ok ( git2_hooks:: hooks_post_commit_with_timeout (
103
+ & repo, None , timeout,
104
+ ) ?
105
+ . into ( ) )
57
106
}
58
107
59
108
/// see `git2_hooks::hooks_prepare_commit_msg`
@@ -66,8 +115,26 @@ pub fn hooks_prepare_commit_msg(
66
115
67
116
let repo = repo ( repo_path) ?;
68
117
69
- Ok ( git2_hooks:: hooks_prepare_commit_msg (
70
- & repo, None , source, msg,
118
+ Ok ( git2_hooks:: hooks_prepare_commit_msg_with_timeout (
119
+ & repo, None , source, msg, None ,
120
+ ) ?
121
+ . into ( ) )
122
+ }
123
+
124
+ /// see `git2_hooks::hooks_prepare_commit_msg`
125
+ #[ allow( unused) ]
126
+ pub fn hooks_prepare_commit_msg_with_timeout (
127
+ repo_path : & RepoPath ,
128
+ source : PrepareCommitMsgSource ,
129
+ msg : & mut String ,
130
+ timeout : Option < Duration > ,
131
+ ) -> Result < HookResult > {
132
+ scope_time ! ( "hooks_prepare_commit_msg" ) ;
133
+
134
+ let repo = repo ( repo_path) ?;
135
+
136
+ Ok ( git2_hooks:: hooks_prepare_commit_msg_with_timeout (
137
+ & repo, None , source, msg, timeout,
71
138
) ?
72
139
. into ( ) )
73
140
}
@@ -77,7 +144,7 @@ mod tests {
77
144
use std:: { ffi:: OsString , io:: Write as _, path:: Path } ;
78
145
79
146
use git2:: Repository ;
80
- use tempfile:: TempDir ;
147
+ use tempfile:: { tempdir , TempDir } ;
81
148
82
149
use super :: * ;
83
150
use crate :: sync:: tests:: repo_init_with_prefix;
@@ -125,7 +192,7 @@ mod tests {
125
192
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
126
193
let root = repo. workdir ( ) . unwrap ( ) ;
127
194
128
- let hook = b"#!/bin/sh
195
+ let hook = b"#!/usr/ bin/env sh
129
196
echo 'rejected'
130
197
exit 1
131
198
" ;
@@ -239,4 +306,144 @@ mod tests {
239
306
240
307
assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
241
308
}
309
+
310
+ #[ test]
311
+ fn test_hooks_respect_timeout ( ) {
312
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
313
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
314
+
315
+ let hook = b"#!/usr/bin/env sh
316
+ sleep 0.250
317
+ " ;
318
+
319
+ git2_hooks:: create_hook (
320
+ & repo,
321
+ git2_hooks:: HOOK_PRE_COMMIT ,
322
+ hook,
323
+ ) ;
324
+
325
+ let res = hooks_pre_commit_with_timeout (
326
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
327
+ Some ( Duration :: from_millis ( 200 ) ) ,
328
+ )
329
+ . unwrap ( ) ;
330
+
331
+ assert_eq ! (
332
+ res,
333
+ HookResult :: TimedOut {
334
+ stdout: String :: new( ) ,
335
+ stderr: String :: new( )
336
+ }
337
+ ) ;
338
+ }
339
+
340
+ #[ test]
341
+ fn test_hooks_faster_than_timeout ( ) {
342
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
343
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
344
+
345
+ let hook = b"#!/usr/bin/env sh
346
+ sleep 0.1
347
+ " ;
348
+
349
+ git2_hooks:: create_hook (
350
+ & repo,
351
+ git2_hooks:: HOOK_PRE_COMMIT ,
352
+ hook,
353
+ ) ;
354
+
355
+ let res = hooks_pre_commit_with_timeout (
356
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
357
+ Some ( Duration :: from_millis ( 150 ) ) ,
358
+ )
359
+ . unwrap ( ) ;
360
+
361
+ assert_eq ! ( res, HookResult :: Ok ) ;
362
+ }
363
+
364
+ #[ test]
365
+ fn test_hooks_timeout_zero ( ) {
366
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
367
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
368
+
369
+ let hook = b"#!/usr/bin/env sh
370
+ sleep 1
371
+ " ;
372
+
373
+ git2_hooks:: create_hook (
374
+ & repo,
375
+ git2_hooks:: HOOK_POST_COMMIT ,
376
+ hook,
377
+ ) ;
378
+
379
+ let res = hooks_post_commit_with_timeout (
380
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
381
+ Some ( Duration :: ZERO ) ,
382
+ )
383
+ . unwrap ( ) ;
384
+
385
+ assert_eq ! ( res, HookResult :: Ok ) ;
386
+ }
387
+
388
+ #[ test]
389
+ fn test_run_with_timeout_kills ( ) {
390
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
391
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
392
+
393
+ let temp_dir = tempdir ( ) . expect ( "temp dir" ) ;
394
+ let file = temp_dir. path ( ) . join ( "test" ) ;
395
+ let hook = format ! (
396
+ "#!/usr/bin/env sh
397
+ sleep 5
398
+ echo 'after sleep' > {}
399
+ " ,
400
+ file. as_path( ) . to_str( ) . unwrap( )
401
+ ) ;
402
+
403
+ git2_hooks:: create_hook (
404
+ & repo,
405
+ git2_hooks:: HOOK_PRE_COMMIT ,
406
+ hook. as_bytes ( ) ,
407
+ ) ;
408
+
409
+ let res = hooks_pre_commit_with_timeout (
410
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
411
+ Some ( Duration :: from_millis ( 100 ) ) ,
412
+ ) ;
413
+
414
+ assert ! ( res. is_ok( ) ) ;
415
+ assert ! ( !file. exists( ) ) ;
416
+ }
417
+
418
+ #[ test]
419
+ #[ cfg( unix) ]
420
+ fn test_ensure_group_kill_works ( ) {
421
+ let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
422
+ let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
423
+
424
+ let hook = b"#!/usr/bin/env sh
425
+ sleep 30
426
+ " ;
427
+
428
+ git2_hooks:: create_hook (
429
+ & repo,
430
+ git2_hooks:: HOOK_PRE_COMMIT ,
431
+ hook,
432
+ ) ;
433
+
434
+ let time_start = std:: time:: Instant :: now ( ) ;
435
+ let res = hooks_pre_commit_with_timeout (
436
+ & root. to_str ( ) . unwrap ( ) . into ( ) ,
437
+ Some ( Duration :: from_millis ( 150 ) ) ,
438
+ )
439
+ . unwrap ( ) ;
440
+ let time_end = std:: time:: Instant :: now ( ) ;
441
+ let elapsed = time_end. duration_since ( time_start) ;
442
+
443
+ log:: info!( "elapsed: {:?}" , elapsed) ;
444
+ // If the children didn't get killed this would
445
+ // have taken the full 30 seconds.
446
+ assert ! ( elapsed. as_secs( ) < 15 ) ;
447
+ assert ! ( matches!( res, HookResult :: TimedOut { .. } ) )
448
+ }
242
449
}
0 commit comments