Skip to content

Commit 521ab91

Browse files
authored
git2-hooks: allows customizing what places to look for hooks (gitui-org#1975)
* allows customizing what places to look for hooks
1 parent fd400cf commit 521ab91

File tree

8 files changed

+287
-112
lines changed

8 files changed

+287
-112
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

asyncgit/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ crossbeam-channel = "0.5"
1717
easy-cast = "0.5"
1818
fuzzy-matcher = "0.3"
1919
git2 = "0.17"
20-
git2-hooks = { path = "../git2-hooks", version = "0.2" }
20+
git2-hooks = { path = "../git2-hooks", version = "0.3" }
2121
log = "0.4"
2222
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}
2323
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]}

asyncgit/src/sync/hooks.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ pub enum HookResult {
1414
impl From<git2_hooks::HookResult> for HookResult {
1515
fn from(v: git2_hooks::HookResult) -> Self {
1616
match v {
17-
git2_hooks::HookResult::Ok => Self::Ok,
18-
git2_hooks::HookResult::NotOk { stdout, stderr } => {
19-
Self::NotOk(format!("{stdout}{stderr}"))
20-
}
17+
git2_hooks::HookResult::Ok { .. }
18+
| git2_hooks::HookResult::NoHookFound => Self::Ok,
19+
git2_hooks::HookResult::RunNotSuccessful {
20+
stdout,
21+
stderr,
22+
..
23+
} => Self::NotOk(format!("{stdout}{stderr}")),
2124
}
2225
}
2326
}
@@ -34,7 +37,7 @@ pub fn hooks_commit_msg(
3437

3538
let repo = repo(repo_path)?;
3639

37-
Ok(git2_hooks::hooks_commit_msg(&repo, msg)?.into())
40+
Ok(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())
3841
}
3942

4043
/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_commit>
@@ -44,7 +47,7 @@ pub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {
4447

4548
let repo = repo(repo_path)?;
4649

47-
Ok(git2_hooks::hooks_pre_commit(&repo)?.into())
50+
Ok(git2_hooks::hooks_pre_commit(&repo, None)?.into())
4851
}
4952

5053
///
@@ -53,7 +56,7 @@ pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
5356

5457
let repo = repo(repo_path)?;
5558

56-
Ok(git2_hooks::hooks_post_commit(&repo)?.into())
59+
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
5760
}
5861

5962
#[cfg(test)]

git2-hooks/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
[package]
22
name = "git2-hooks"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
authors = ["extrawurst <mail@rusticorn.com>"]
55
edition = "2021"
66
description = "adds git hooks support based on git2-rs"
77
homepage = "https://github.com/extrawurst/gitui"
88
repository = "https://github.com/extrawurst/gitui"
9+
documentation = "https://docs.rs/git2-hooks/"
910
readme = "README.md"
1011
license = "MIT"
1112
categories = ["development-tools"]

git2-hooks/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22

33
adds git hook functionality on top of git2-rs
44

5+
## todo
6+
7+
- [ ] unittest coverage symlinks from `.git/hooks/<hook>` -> `X`
8+
- [ ] unittest coverage `~` expansion inside `core.hooksPath`

git2-hooks/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ use thiserror::Error;
33
///
44
#[derive(Error, Debug)]
55
pub enum HooksError {
6+
///
7+
#[error("git error:{0}")]
8+
Git(#[from] git2::Error),
9+
610
///
711
#[error("io error:{0}")]
812
Io(#[from] std::io::Error),

git2-hooks/src/hookspath.rs

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,56 +12,106 @@ pub struct HookPaths {
1212
pub pwd: PathBuf,
1313
}
1414

15+
const CONFIG_HOOKS_PATH: &str = "core.hooksPath";
16+
const DEFAULT_HOOKS_PATH: &str = "hooks";
17+
1518
impl HookPaths {
16-
pub fn new(repo: &Repository, hook: &str) -> Result<Self> {
19+
/// `core.hooksPath` always takes precendence.
20+
/// If its defined and there is no hook `hook` this is not considered
21+
/// an error or a reason to search in other paths.
22+
/// If the config is not set we go into search mode and
23+
/// first check standard `.git/hooks` folder and any sub path provided in `other_paths`.
24+
///
25+
/// Note: we try to model as closely as possible what git shell is doing.
26+
pub fn new(
27+
repo: &Repository,
28+
other_paths: Option<&[&str]>,
29+
hook: &str,
30+
) -> Result<Self> {
1731
let pwd = repo
1832
.workdir()
1933
.unwrap_or_else(|| repo.path())
2034
.to_path_buf();
2135

2236
let git_dir = repo.path().to_path_buf();
23-
let hooks_path = repo
24-
.config()
25-
.and_then(|config| config.get_string("core.hooksPath"))
26-
.map_or_else(
27-
|e| {
28-
log::error!("hookspath error: {}", e);
29-
repo.path().to_path_buf().join("hooks/")
30-
},
31-
PathBuf::from,
32-
);
3337

34-
let hook = hooks_path.join(hook);
38+
if let Some(config_path) = Self::config_hook_path(repo)? {
39+
let hooks_path = PathBuf::from(config_path);
40+
41+
let hook = hooks_path.join(hook);
42+
43+
let hook = shellexpand::full(
44+
hook.as_os_str()
45+
.to_str()
46+
.ok_or(HooksError::PathToString)?,
47+
)?;
3548

36-
let hook = shellexpand::full(
37-
hook.as_os_str()
38-
.to_str()
39-
.ok_or(HooksError::PathToString)?,
40-
)?;
49+
let hook = PathBuf::from_str(hook.as_ref())
50+
.map_err(|_| HooksError::PathToString)?;
4151

42-
let hook = PathBuf::from_str(hook.as_ref())
43-
.map_err(|_| HooksError::PathToString)?;
52+
return Ok(Self {
53+
git: git_dir,
54+
hook,
55+
pwd,
56+
});
57+
}
4458

4559
Ok(Self {
4660
git: git_dir,
47-
hook,
61+
hook: Self::find_hook(repo, other_paths, hook),
4862
pwd,
4963
})
5064
}
5165

52-
pub fn is_executable(&self) -> bool {
66+
fn config_hook_path(repo: &Repository) -> Result<Option<String>> {
67+
Ok(repo.config()?.get_string(CONFIG_HOOKS_PATH).ok())
68+
}
69+
70+
/// check default hook path first and then followed by `other_paths`.
71+
/// if no hook is found we return the default hook path
72+
fn find_hook(
73+
repo: &Repository,
74+
other_paths: Option<&[&str]>,
75+
hook: &str,
76+
) -> PathBuf {
77+
let mut paths = vec![DEFAULT_HOOKS_PATH.to_string()];
78+
if let Some(others) = other_paths {
79+
paths.extend(
80+
others
81+
.iter()
82+
.map(|p| p.trim_end_matches('/').to_string()),
83+
);
84+
}
85+
86+
for p in paths {
87+
let p = repo.path().to_path_buf().join(p).join(hook);
88+
if p.exists() {
89+
return p;
90+
}
91+
}
92+
93+
repo.path()
94+
.to_path_buf()
95+
.join(DEFAULT_HOOKS_PATH)
96+
.join(hook)
97+
}
98+
99+
/// was a hook file found and is it executable
100+
pub fn found(&self) -> bool {
53101
self.hook.exists() && is_executable(&self.hook)
54102
}
55103

56104
/// this function calls hook scripts based on conventions documented here
57105
/// see <https://git-scm.com/docs/githooks>
58106
pub fn run_hook(&self, args: &[&str]) -> Result<HookResult> {
59-
let arg_str = format!("{:?} {}", self.hook, args.join(" "));
107+
let hook = self.hook.clone();
108+
109+
let arg_str = format!("{:?} {}", hook, args.join(" "));
60110
// Use -l to avoid "command not found" on Windows.
61111
let bash_args =
62112
vec!["-l".to_string(), "-c".to_string(), arg_str];
63113

64-
log::trace!("run hook '{:?}' in '{:?}'", self.hook, self.pwd);
114+
log::trace!("run hook '{:?}' in '{:?}'", hook, self.pwd);
65115

66116
let git_bash = find_bash_executable()
67117
.unwrap_or_else(|| PathBuf::from("bash"));
@@ -78,19 +128,24 @@ impl HookPaths {
78128
.output()?;
79129

80130
if output.status.success() {
81-
Ok(HookResult::Ok)
131+
Ok(HookResult::Ok { hook })
82132
} else {
83133
let stderr =
84134
String::from_utf8_lossy(&output.stderr).to_string();
85135
let stdout =
86136
String::from_utf8_lossy(&output.stdout).to_string();
87137

88-
Ok(HookResult::NotOk { stdout, stderr })
138+
Ok(HookResult::RunNotSuccessful {
139+
code: output.status.code(),
140+
stdout,
141+
stderr,
142+
hook,
143+
})
89144
}
90145
}
91146
}
92147

93-
#[cfg(not(windows))]
148+
#[cfg(unix)]
94149
fn is_executable(path: &Path) -> bool {
95150
use std::os::unix::fs::PermissionsExt;
96151

0 commit comments

Comments
 (0)