Skip to content

Commit 64979e5

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 f619e3e commit 64979e5

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())
84+
}
85+
86+
/// see `git2_hooks::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())
11893
}
11994

12095
/// see `git2_hooks::hooks_post_commit`
121-
pub fn 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
use std::fs::File;
@@ -215,11 +185,9 @@ mod tests {
215185
let subfolder = root.join("foo/");
216186
std::fs::create_dir_all(&subfolder).unwrap();
217187

218-
let res = hooks_post_commit(
219-
&subfolder.to_str().unwrap().into(),
220-
Duration::ZERO,
221-
)
222-
.unwrap();
188+
let res =
189+
hooks_post_commit(&subfolder.to_str().unwrap().into())
190+
.unwrap();
223191

224192
assert_eq!(
225193
res,
@@ -250,8 +218,7 @@ mod tests {
250218
git2_hooks::HOOK_PRE_COMMIT,
251219
hook,
252220
);
253-
let res =
254-
hooks_pre_commit(repo_path, Duration::ZERO).unwrap();
221+
let res = hooks_pre_commit(repo_path).unwrap();
255222
if let HookResult::NotOk(res) = res {
256223
assert_eq!(
257224
std::path::Path::new(res.trim_end()),
@@ -286,7 +253,6 @@ mod tests {
286253
let res = hooks_commit_msg(
287254
&subfolder.to_str().unwrap().into(),
288255
&mut msg,
289-
Duration::ZERO,
290256
)
291257
.unwrap();
292258

@@ -346,16 +312,13 @@ mod tests {
346312
hook,
347313
);
348314

349-
let res = hooks_pre_commit(
315+
let res = hooks_pre_commit_with_timeout(
350316
&root.to_str().unwrap().into(),
351317
Duration::from_millis(200),
352318
)
353319
.unwrap();
354320

355-
assert_eq!(
356-
res,
357-
HookResult::NotOk("hook timed out".to_string())
358-
);
321+
assert_eq!(res, HookResult::TimedOut);
359322
}
360323

361324
#[test]
@@ -373,7 +336,7 @@ mod tests {
373336
hook,
374337
);
375338

376-
let res = hooks_pre_commit(
339+
let res = hooks_pre_commit_with_timeout(
377340
&root.to_str().unwrap().into(),
378341
Duration::from_millis(110),
379342
)
@@ -397,12 +360,43 @@ mod tests {
397360
hook,
398361
);
399362

400-
let res = hooks_post_commit(
363+
let res = hooks_post_commit_with_timeout(
401364
&root.to_str().unwrap().into(),
402365
Duration::ZERO,
403366
)
404367
.unwrap();
405368

406369
assert_eq!(res, HookResult::Ok);
407370
}
371+
372+
#[test]
373+
fn test_run_with_timeout_kills() {
374+
let (_td, repo) = repo_init().unwrap();
375+
let root = repo.path().parent().unwrap();
376+
377+
let temp_dir = tempdir().expect("temp dir");
378+
let file = temp_dir.path().join("test");
379+
let hook = format!(
380+
"#!/usr/bin/env sh
381+
sleep 1
382+
383+
echo 'after sleep' > {}
384+
",
385+
file.as_path().to_str().unwrap()
386+
);
387+
388+
git2_hooks::create_hook(
389+
&repo,
390+
git2_hooks::HOOK_PRE_COMMIT,
391+
hook.as_bytes(),
392+
);
393+
394+
let res = hooks_pre_commit_with_timeout(
395+
&root.to_str().unwrap().into(),
396+
Duration::from_millis(100),
397+
);
398+
399+
assert!(res.is_ok());
400+
assert!(!file.exists());
401+
}
408402
}

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)