@@ -2,6 +2,7 @@ use super::{repository::repo, RepoPath};
22use crate :: error:: Result ;
33pub use git2_hooks:: PrepareCommitMsgSource ;
44use scopetime:: scope_time;
5+ use std:: time:: Duration ;
56
67///
78#[ derive( Debug , PartialEq , Eq ) ]
@@ -10,6 +11,13 @@ pub enum HookResult {
1011 Ok ,
1112 /// Hook returned error
1213 NotOk ( String ) ,
14+ /// Hook timed out
15+ TimedOut {
16+ /// Stdout
17+ stdout : String ,
18+ /// Stderr
19+ stderr : String ,
20+ } ,
1321}
1422
1523impl From < git2_hooks:: HookResult > for HookResult {
@@ -22,6 +30,11 @@ impl From<git2_hooks::HookResult> for HookResult {
2230 stderr,
2331 ..
2432 } => Self :: NotOk ( format ! ( "{stdout}{stderr}" ) ) ,
33+ git2_hooks:: HookResult :: TimedOut {
34+ stdout,
35+ stderr,
36+ ..
37+ } => Self :: TimedOut { stdout, stderr } ,
2538 }
2639 }
2740}
@@ -30,30 +43,66 @@ impl From<git2_hooks::HookResult> for HookResult {
3043pub fn hooks_commit_msg (
3144 repo_path : & RepoPath ,
3245 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 > ,
3356) -> Result < HookResult > {
3457 scope_time ! ( "hooks_commit_msg" ) ;
3558
3659 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 ( ) )
3964}
4065
4166/// see `git2_hooks::hooks_pre_commit`
4267pub 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 > {
4377 scope_time ! ( "hooks_pre_commit" ) ;
4478
4579 let repo = repo ( repo_path) ?;
4680
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 ( ) )
4885}
4986
5087/// see `git2_hooks::hooks_post_commit`
5188pub 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 > {
5298 scope_time ! ( "hooks_post_commit" ) ;
5399
54100 let repo = repo ( repo_path) ?;
55101
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 ( ) )
57106}
58107
59108/// see `git2_hooks::hooks_prepare_commit_msg`
@@ -66,8 +115,26 @@ pub fn hooks_prepare_commit_msg(
66115
67116 let repo = repo ( repo_path) ?;
68117
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,
71138 ) ?
72139 . into ( ) )
73140}
@@ -77,7 +144,7 @@ mod tests {
77144 use std:: { ffi:: OsString , io:: Write as _, path:: Path } ;
78145
79146 use git2:: Repository ;
80- use tempfile:: TempDir ;
147+ use tempfile:: { tempdir , TempDir } ;
81148
82149 use super :: * ;
83150 use crate :: sync:: tests:: repo_init_with_prefix;
@@ -125,7 +192,7 @@ mod tests {
125192 let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
126193 let root = repo. workdir ( ) . unwrap ( ) ;
127194
128- let hook = b"#!/bin/sh
195+ let hook = b"#!/usr/ bin/env sh
129196 echo 'rejected'
130197 exit 1
131198 " ;
@@ -239,4 +306,144 @@ mod tests {
239306
240307 assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
241308 }
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+ }
242449}
0 commit comments