From 435de9cda3bc6419d98fd3c183344506d4a4c201 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Mon, 17 Jan 2022 15:06:54 +0100 Subject: [PATCH] support hookspath (#1054) --- CHANGELOG.md | 1 + Cargo.lock | 10 +++++ README.md | 1 - asyncgit/Cargo.toml | 1 + asyncgit/src/error.rs | 8 ++++ asyncgit/src/sync/hooks.rs | 89 ++++++++++++++++++++++++++++++++------ 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c2b113..9d292e52 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 ![delete-tag-remote](assets/delete-tag-remote.gif) ### 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 24a9660f..5554cc59 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 cb8f5090..1b1f05f4 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 48ba2235..71293454 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 9f2a2566..e733284d 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 fcb30559..130c81e0 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();