Skip to content

Commit 687c899

Browse files
committed
refactor: move timeout logic to git2-hooks & add new functions for timeouts
All hooks functions now have their own with_timeout variant inside git2-hooks and asyncgit. The previous implementation of using a new thread with a timeout has been ditched in favour of a child command which we can now poll and kill to prevent the hook from continueing. I've added a hard coded default 5 second timeout but this will be handled by the Options struct later
1 parent 7703743 commit 687c899

File tree

5 files changed

+304
-172
lines changed

5 files changed

+304
-172
lines changed

asyncgit/src/sync/hooks.rs

Lines changed: 118 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ use super::{repository::repo, RepoPath};
22
use crate::error::Result;
33
pub use git2_hooks::PrepareCommitMsgSource;
44
use scopetime::scope_time;
5-
use std::{
6-
sync::mpsc::{channel, RecvTimeoutError},
7-
time::Duration,
8-
};
5+
use std::time::Duration;
96

107
///
118
#[derive(Debug, PartialEq, Eq)]
@@ -14,6 +11,8 @@ pub enum HookResult {
1411
Ok,
1512
/// Hook returned error
1613
NotOk(String),
14+
/// Hook timed out
15+
TimedOut,
1716
}
1817

1918
impl From<git2_hooks::HookResult> for HookResult {
@@ -26,156 +25,127 @@ impl From<git2_hooks::HookResult> for HookResult {
2625
stderr,
2726
..
2827
} => Self::NotOk(format!("{stdout}{stderr}")),
28+
git2_hooks::HookResult::TimedOut { .. } => Self::TimedOut,
2929
}
3030
}
3131
}
3232

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-
}
33+
/// see `git2_hooks::hooks_commit_msg`
34+
pub fn hooks_commit_msg(
35+
repo_path: &RepoPath,
36+
msg: &mut String,
37+
) -> Result<HookResult> {
38+
scope_time!("hooks_commit_msg");
4639

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-
}
40+
let repo = repo(repo_path)?;
41+
42+
Ok(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())
6343
}
6444

65-
/// see `git2_hooks::hooks_commit_msg`
66-
pub fn hooks_commit_msg(
45+
/// see `git2_hooks::hooks_prepare_commit_msg`
46+
#[allow(unused)]
47+
pub fn hooks_commit_msg_with_timeout(
6748
repo_path: &RepoPath,
6849
msg: &mut String,
6950
timeout: Duration,
7051
) -> Result<HookResult> {
71-
scope_time!("hooks_commit_msg");
52+
scope_time!("hooks_prepare_commit_msg");
7253

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-
}
54+
let repo = repo(repo_path)?;
55+
Ok(git2_hooks::hooks_commit_msg_with_timeout(
56+
&repo, None, msg, timeout,
57+
)?
58+
.into())
59+
}
9560

96-
Ok(result)
61+
/// see `git2_hooks::hooks_pre_commit`
62+
pub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {
63+
scope_time!("hooks_pre_commit");
64+
65+
let repo = repo(repo_path)?;
66+
67+
Ok(git2_hooks::hooks_pre_commit(&repo, None)?.into())
9768
}
9869

9970
/// see `git2_hooks::hooks_pre_commit`
100-
pub fn hooks_pre_commit(
71+
#[allow(unused)]
72+
pub fn hooks_pre_commit_with_timeout(
10173
repo_path: &RepoPath,
10274
timeout: Duration,
10375
) -> Result<HookResult> {
10476
scope_time!("hooks_pre_commit");
10577

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)
78+
let repo = repo(repo_path)?;
79+
80+
Ok(git2_hooks::hooks_pre_commit_with_timeout(
81+
&repo, None, timeout,
82+
)?
83+
.into())
11884
}
11985

12086
/// see `git2_hooks::hooks_post_commit`
121-
pub fn hooks_post_commit(
87+
pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
88+
scope_time!("hooks_post_commit");
89+
90+
let repo = repo(repo_path)?;
91+
92+
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
93+
}
94+
95+
/// see `git2_hooks::hooks_post_commit`
96+
#[allow(unused)]
97+
pub fn hooks_post_commit_with_timeout(
12298
repo_path: &RepoPath,
12399
timeout: Duration,
124100
) -> Result<HookResult> {
125101
scope_time!("hooks_post_commit");
126102

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)
103+
let repo = repo(repo_path)?;
104+
105+
Ok(git2_hooks::hooks_post_commit_with_timeout(
106+
&repo, None, timeout,
107+
)?
108+
.into())
139109
}
140110

141111
/// see `git2_hooks::hooks_prepare_commit_msg`
142112
pub fn hooks_prepare_commit_msg(
143113
repo_path: &RepoPath,
144114
source: PrepareCommitMsgSource,
145115
msg: &mut String,
116+
) -> Result<HookResult> {
117+
scope_time!("hooks_prepare_commit_msg");
118+
119+
let repo = repo(repo_path)?;
120+
121+
Ok(git2_hooks::hooks_prepare_commit_msg(
122+
&repo, None, source, msg,
123+
)?
124+
.into())
125+
}
126+
127+
/// see `git2_hooks::hooks_prepare_commit_msg`
128+
#[allow(unused)]
129+
pub fn hooks_prepare_commit_msg_with_timeout(
130+
repo_path: &RepoPath,
131+
source: PrepareCommitMsgSource,
132+
msg: &mut String,
146133
timeout: Duration,
147134
) -> Result<HookResult> {
148135
scope_time!("hooks_prepare_commit_msg");
149136

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-
}
137+
let repo = repo(repo_path)?;
173138

174-
Ok(result)
139+
Ok(git2_hooks::hooks_prepare_commit_msg_with_timeout(
140+
&repo, None, source, msg, timeout,
141+
)?
142+
.into())
175143
}
176144

177145
#[cfg(test)]
178146
mod tests {
147+
use tempfile::tempdir;
148+
179149
use super::*;
180150
use crate::sync::tests::repo_init;
181151

@@ -198,11 +168,9 @@ mod tests {
198168
let subfolder = root.join("foo/");
199169
std::fs::create_dir_all(&subfolder).unwrap();
200170

201-
let res = hooks_post_commit(
202-
&subfolder.to_str().unwrap().into(),
203-
Duration::ZERO,
204-
)
205-
.unwrap();
171+
let res =
172+
hooks_post_commit(&subfolder.to_str().unwrap().into())
173+
.unwrap();
206174

207175
assert_eq!(
208176
res,
@@ -233,8 +201,7 @@ mod tests {
233201
git2_hooks::HOOK_PRE_COMMIT,
234202
hook,
235203
);
236-
let res =
237-
hooks_pre_commit(repo_path, Duration::ZERO).unwrap();
204+
let res = hooks_pre_commit(repo_path).unwrap();
238205
if let HookResult::NotOk(res) = res {
239206
assert_eq!(
240207
std::path::Path::new(res.trim_end()),
@@ -269,7 +236,6 @@ mod tests {
269236
let res = hooks_commit_msg(
270237
&subfolder.to_str().unwrap().into(),
271238
&mut msg,
272-
Duration::ZERO,
273239
)
274240
.unwrap();
275241

@@ -296,16 +262,13 @@ mod tests {
296262
hook,
297263
);
298264

299-
let res = hooks_pre_commit(
265+
let res = hooks_pre_commit_with_timeout(
300266
&root.to_str().unwrap().into(),
301267
Duration::from_millis(200),
302268
)
303269
.unwrap();
304270

305-
assert_eq!(
306-
res,
307-
HookResult::NotOk("hook timed out".to_string())
308-
);
271+
assert_eq!(res, HookResult::TimedOut);
309272
}
310273

311274
#[test]
@@ -323,7 +286,7 @@ mod tests {
323286
hook,
324287
);
325288

326-
let res = hooks_pre_commit(
289+
let res = hooks_pre_commit_with_timeout(
327290
&root.to_str().unwrap().into(),
328291
Duration::from_millis(110),
329292
)
@@ -347,12 +310,43 @@ mod tests {
347310
hook,
348311
);
349312

350-
let res = hooks_post_commit(
313+
let res = hooks_post_commit_with_timeout(
351314
&root.to_str().unwrap().into(),
352315
Duration::ZERO,
353316
)
354317
.unwrap();
355318

356319
assert_eq!(res, HookResult::Ok);
357320
}
321+
322+
#[test]
323+
fn test_run_with_timeout_kills() {
324+
let (_td, repo) = repo_init().unwrap();
325+
let root = repo.path().parent().unwrap();
326+
327+
let temp_dir = tempdir().expect("temp dir");
328+
let file = temp_dir.path().join("test");
329+
let hook = format!(
330+
"#!/usr/bin/env sh
331+
sleep 1
332+
333+
echo 'after sleep' > {}
334+
",
335+
file.as_path().to_str().unwrap()
336+
);
337+
338+
git2_hooks::create_hook(
339+
&repo,
340+
git2_hooks::HOOK_PRE_COMMIT,
341+
hook.as_bytes(),
342+
);
343+
344+
let res = hooks_pre_commit_with_timeout(
345+
&root.to_str().unwrap().into(),
346+
Duration::from_millis(100),
347+
);
348+
349+
assert!(res.is_ok());
350+
assert!(!file.exists());
351+
}
358352
}

asyncgit/src/sync/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,11 @@ pub use config::{
6666
pub use diff::get_diff_commit;
6767
pub use git2::BranchType;
6868
pub use hooks::{
69-
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
70-
hooks_prepare_commit_msg, HookResult, PrepareCommitMsgSource,
69+
hooks_commit_msg, hooks_commit_msg_with_timeout,
70+
hooks_post_commit, hooks_post_commit_with_timeout,
71+
hooks_pre_commit, hooks_pre_commit_with_timeout,
72+
hooks_prepare_commit_msg, hooks_prepare_commit_msg_with_timeout,
73+
HookResult, PrepareCommitMsgSource,
7174
};
7275
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
7376
pub use ignore::add_to_ignore;

0 commit comments

Comments
 (0)