diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2c2b113d0..9d292e52aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
+- support `core.hooksPath` ([#1044](https://github.com/extrawurst/gitui/issues/1044))
- allow reverting a commit from the commit log ([#927](https://github.com/extrawurst/gitui/issues/927))
- disable pull cmd on local-only branches ([#1047](https://github.com/extrawurst/gitui/issues/1047))
- support adding annotations to tags ([#747](https://github.com/extrawurst/gitui/issues/747))
diff --git a/Cargo.lock b/Cargo.lock
index 24a9660fb9..5554cc5977 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -75,6 +75,7 @@ dependencies = [
"rayon-core",
"scopetime",
"serial_test",
+ "shellexpand",
"tempfile",
"thiserror",
"unicode-truncate",
@@ -1219,6 +1220,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+[[package]]
+name = "shellexpand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829"
+dependencies = [
+ "dirs-next",
+]
+
[[package]]
name = "signal-hook"
version = "0.3.13"
diff --git a/README.md b/README.md
index cb8f50903c..1b1f05f4c2 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,6 @@ These are the high level goals before calling out `1.0`:
## 5. Known Limitations [Top ▲](#table-of-contents)
-- no support for [core.hooksPath](https://git-scm.com/docs/githooks) config (see [#1044](https://github.com/extrawurst/gitui/issues/1044))
- no support for GPG signing (see [#97](https://github.com/extrawurst/gitui/issues/97))
Currently, this tool does not fully substitute the _git shell_, however both tools work well in tandem.
diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml
index 48ba2235a9..71293454d7 100644
--- a/asyncgit/Cargo.toml
+++ b/asyncgit/Cargo.toml
@@ -22,6 +22,7 @@ log = "0.4"
openssl-sys = { version = '0.9', features = ["vendored"] }
rayon-core = "1.9"
scopetime = { path = "../scopetime", version = "0.1" }
+shellexpand = "2.1"
thiserror = "1.0"
unicode-truncate = "0.2.0"
url = "2.2"
diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs
index 9f2a25665c..e733284d01 100644
--- a/asyncgit/src/error.rs
+++ b/asyncgit/src/error.rs
@@ -61,6 +61,14 @@ pub enum Error {
///
#[error("EasyCast error:{0}")]
EasyCast(#[from] easy_cast::Error),
+
+ ///
+ #[error("shellexpand error:{0}")]
+ Shell(#[from] shellexpand::LookupError),
+
+ ///
+ #[error("path string error")]
+ PathString,
}
///
diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs
index fcb30559ee..130c81e04c 100644
--- a/asyncgit/src/sync/hooks.rs
+++ b/asyncgit/src/sync/hooks.rs
@@ -1,16 +1,17 @@
use super::{repository::repo, RepoPath};
-use crate::error::Result;
+use crate::error::{self, Result};
use scopetime::scope_time;
use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
+ str::FromStr,
};
-const HOOK_POST_COMMIT: &str = "hooks/post-commit";
-const HOOK_PRE_COMMIT: &str = "hooks/pre-commit";
-const HOOK_COMMIT_MSG: &str = "hooks/commit-msg";
+const HOOK_POST_COMMIT: &str = "post-commit";
+const HOOK_PRE_COMMIT: &str = "pre-commit";
+const HOOK_COMMIT_MSG: &str = "commit-msg";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
struct HookPaths {
@@ -26,8 +27,30 @@ impl HookPaths {
.workdir()
.unwrap_or_else(|| repo.path())
.to_path_buf();
+
let git_dir = repo.path().to_path_buf();
- let hook = git_dir.join(hook);
+ let hooks_path = repo
+ .config()
+ .and_then(|config| config.get_string("core.hooksPath"))
+ .map_or_else(
+ |e| {
+ log::error!("hookspath error: {}", e);
+ repo.path().to_path_buf().join("hooks/")
+ },
+ PathBuf::from,
+ );
+
+ let hook = hooks_path.join(hook);
+
+ let hook = shellexpand::full(
+ hook.as_os_str()
+ .to_str()
+ .ok_or(error::Error::PathString)?,
+ )?;
+
+ let hook = PathBuf::from_str(hook.as_ref())
+ .map_err(|_| error::Error::PathString)?;
+
Ok(Self {
git: git_dir,
hook,
@@ -143,10 +166,14 @@ fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
let metadata = match path.metadata() {
Ok(metadata) => metadata,
- Err(_) => return false,
+ Err(e) => {
+ log::error!("metadata error: {}", e);
+ return false;
+ }
};
let permissions = metadata.permissions();
+
permissions.mode() & 0o111 != 0
}
@@ -181,20 +208,28 @@ mod tests {
assert_eq!(res, HookResult::Ok);
}
- fn create_hook(path: &RepoPath, hook: &str, hook_script: &[u8]) {
+ fn create_hook(
+ path: &RepoPath,
+ hook: &str,
+ hook_script: &[u8],
+ ) -> PathBuf {
let hook = HookPaths::new(path, hook).unwrap();
- File::create(&hook.hook)
- .unwrap()
- .write_all(hook_script)
- .unwrap();
+ let path = hook.hook.clone();
+
+ create_hook_in_path(&hook.hook, hook_script);
+
+ path
+ }
+
+ fn create_hook_in_path(path: &Path, hook_script: &[u8]) {
+ File::create(path).unwrap().write_all(hook_script).unwrap();
#[cfg(not(windows))]
{
- let hook = hook.hook.as_os_str();
Command::new("chmod")
.arg("+x")
- .arg(hook)
+ .arg(path)
// .current_dir(path)
.output()
.unwrap();
@@ -255,6 +290,34 @@ exit 1
assert!(res != HookResult::Ok);
}
+ #[test]
+ fn test_pre_commit_fail_hookspath() {
+ let (_td, repo) = repo_init().unwrap();
+ let root = repo.path().parent().unwrap();
+ let hooks = TempDir::new().unwrap();
+ let repo_path: &RepoPath =
+ &root.as_os_str().to_str().unwrap().into();
+
+ let hook = b"#!/bin/sh
+echo 'rejected'
+exit 1
+ ";
+
+ create_hook_in_path(&hooks.path().join("pre-commit"), hook);
+ repo.config()
+ .unwrap()
+ .set_str(
+ "core.hooksPath",
+ hooks.path().as_os_str().to_str().unwrap(),
+ )
+ .unwrap();
+ let res = hooks_pre_commit(repo_path).unwrap();
+ assert_eq!(
+ res,
+ HookResult::NotOk(String::from("rejected\n"))
+ );
+ }
+
#[test]
fn test_pre_commit_fail_bare() {
let (git_root, _repo) = repo_init_bare().unwrap();