@@ -2,6 +2,10 @@ use super::{repository::repo, RepoPath};
22use crate :: error:: Result ;
33pub use git2_hooks:: PrepareCommitMsgSource ;
44use scopetime:: scope_time;
5+ use std:: {
6+ sync:: mpsc:: { channel, RecvTimeoutError } ,
7+ time:: Duration ,
8+ } ;
59
610///
711#[ derive( Debug , PartialEq , Eq ) ]
@@ -26,50 +30,148 @@ impl From<git2_hooks::HookResult> for HookResult {
2630 }
2731}
2832
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+
2965/// see `git2_hooks::hooks_commit_msg`
3066pub fn hooks_commit_msg (
3167 repo_path : & RepoPath ,
3268 msg : & mut String ,
69+ timeout : Duration ,
3370) -> Result < HookResult > {
3471 scope_time ! ( "hooks_commit_msg" ) ;
3572
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+ }
3795
38- Ok ( git2_hooks :: hooks_commit_msg ( & repo , None , msg ) ? . into ( ) )
96+ Ok ( result )
3997}
4098
4199/// 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 > {
43104 scope_time ! ( "hooks_pre_commit" ) ;
44105
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 )
48118}
49119
50120/// 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 > {
52125 scope_time ! ( "hooks_post_commit" ) ;
53126
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 )
57139}
58140
59141/// see `git2_hooks::hooks_prepare_commit_msg`
60142pub fn hooks_prepare_commit_msg (
61143 repo_path : & RepoPath ,
62144 source : PrepareCommitMsgSource ,
63145 msg : & mut String ,
146+ timeout : Duration ,
64147) -> Result < HookResult > {
65148 scope_time ! ( "hooks_prepare_commit_msg" ) ;
66149
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+ }
68173
69- Ok ( git2_hooks:: hooks_prepare_commit_msg (
70- & repo, None , source, msg,
71- ) ?
72- . into ( ) )
174+ Ok ( result)
73175}
74176
75177#[ cfg( test) ]
@@ -99,7 +201,7 @@ mod tests {
99201 let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
100202 let root = repo. workdir ( ) . unwrap ( ) ;
101203
102- let hook = b"#!/bin/sh
204+ let hook = b"#!/usr/ bin/env sh
103205 echo 'rejected'
104206 exit 1
105207 " ;
@@ -113,9 +215,11 @@ mod tests {
113215 let subfolder = root. join ( "foo/" ) ;
114216 std:: fs:: create_dir_all ( & subfolder) . unwrap ( ) ;
115217
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 ( ) ;
119223
120224 assert_eq ! (
121225 res,
@@ -136,7 +240,7 @@ mod tests {
136240 let workdir =
137241 crate :: sync:: utils:: repo_work_dir ( repo_path) . unwrap ( ) ;
138242
139- let hook = b"#!/bin/sh
243+ let hook = b"#!/usr/ bin/env sh
140244 echo $(pwd)
141245 exit 1
142246 " ;
@@ -146,7 +250,8 @@ mod tests {
146250 git2_hooks:: HOOK_PRE_COMMIT ,
147251 hook,
148252 ) ;
149- let res = hooks_pre_commit ( repo_path) . unwrap ( ) ;
253+ let res =
254+ hooks_pre_commit ( repo_path, Duration :: ZERO ) . unwrap ( ) ;
150255 if let HookResult :: NotOk ( res) = res {
151256 assert_eq ! (
152257 std:: path:: Path :: new( res. trim_end( ) ) ,
@@ -162,7 +267,7 @@ mod tests {
162267 let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
163268 let root = repo. workdir ( ) . unwrap ( ) ;
164269
165- let hook = b"#!/bin/sh
270+ let hook = b"#!/usr/ bin/env sh
166271 echo 'msg' > $1
167272 echo 'rejected'
168273 exit 1
@@ -181,6 +286,7 @@ mod tests {
181286 let res = hooks_commit_msg (
182287 & subfolder. to_str ( ) . unwrap ( ) . into ( ) ,
183288 & mut msg,
289+ Duration :: ZERO ,
184290 )
185291 . unwrap ( ) ;
186292
@@ -224,4 +330,53 @@ mod tests {
224330
225331 assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
226332 }
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+ }
227382}
0 commit comments