From 36ff0be9d11cdd04b69435f45a0a9de503a04a67 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Thu, 9 Apr 2020 09:23:59 +0200 Subject: [PATCH] support hooks: post-commit,commit-msg (#15) * support hooks: post-commit and commit-msg * some unittests * exclude tests on windows for now --- Cargo.lock | 10 +++ README.md | 5 +- asyncgit/Cargo.toml | 3 +- asyncgit/src/sync/hooks.rs | 176 +++++++++++++++++++++++++++++++++++++ asyncgit/src/sync/mod.rs | 2 + asyncgit/src/sync/utils.rs | 2 +- src/components/commit.rs | 13 +++ 7 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 asyncgit/src/sync/hooks.rs diff --git a/Cargo.lock b/Cargo.lock index 39f4d868..2e23d16a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ version = "0.1.3" dependencies = [ "crossbeam-channel", "git2", + "is_executable", "log", "rayon-core", "scopetime", @@ -351,6 +352,15 @@ dependencies = [ "libc", ] +[[package]] +name = "is_executable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d553b8abc8187beb7d663e34c065ac4570b273bc9511a50e940e99409c577" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "itertools" version = "0.8.2" diff --git a/README.md b/README.md index 00646785..e5c29a1e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ blazing fast terminal-ui for git written in rust * fast and intuitive key only control * context based help (**no** need to remember any hot-key) -* inspect/commit changes +* inspect/commit changes (incl. hooks: commit-msg/post-commit) * (un)stage files, revert/reset files * scalable ui layout * async [input polling](assets/perf_compare.jpg) and @@ -58,8 +58,7 @@ GITUI_LOGGING=true gitui # todo for 0.2 (first release) -* [ ] support commit-msg hook -* [ ] support post-commit hook +* [ ] visualize commit-msg hook result * [ ] publish as homebrew-tap # inspiration diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 8202507a..239dd8f5 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -15,7 +15,6 @@ git2 = { git = "https://github.com/rust-lang/git2-rs.git", rev = "617499d7fcf315 rayon-core = "1.7" crossbeam-channel = "0.4" log = "0.4" +is_executable = "0.1" scopetime = { path = "../scopetime", version = "0.1" } - -[dev-dependencies] tempfile = "3.1" \ No newline at end of file diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs new file mode 100644 index 00000000..2e948391 --- /dev/null +++ b/asyncgit/src/sync/hooks.rs @@ -0,0 +1,176 @@ +use is_executable::IsExecutable; +use scopetime::scope_time; +use std::{ + io::{Read, Write}, + path::Path, + process::Command, +}; +use tempfile::NamedTempFile; + +const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit"; +const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg"; + +/// +pub fn hooks_commit_msg( + repo_path: &str, + msg: &mut String, +) -> HookResult { + scope_time!("hooks_commit_msg"); + + if hook_runable(repo_path, HOOK_COMMIT_MSG) { + let mut file = NamedTempFile::new().unwrap(); + + write!(file, "{}", msg).unwrap(); + + let file_path = file.path().to_str().unwrap(); + + dbg!(&file_path); + + let res = run_hook(repo_path, HOOK_COMMIT_MSG, &[&file_path]); + + if let HookResult::NotOk(e) = res { + let mut file = file.reopen().unwrap(); + msg.clear(); + file.read_to_string(msg).unwrap(); + HookResult::NotOk(e) + } else { + HookResult::Ok + } + } else { + HookResult::Ok + } +} + +/// +pub fn hooks_post_commit(repo_path: &str) -> HookResult { + scope_time!("hooks_post_commit"); + + if hook_runable(repo_path, HOOK_POST_COMMIT) { + run_hook(repo_path, HOOK_POST_COMMIT, &[]) + } else { + HookResult::Ok + } +} + +fn hook_runable(path: &str, hook: &str) -> bool { + let path = Path::new(path); + let path = path.join(hook); + + path.exists() && path.is_executable() +} + +/// +#[derive(Debug, PartialEq)] +pub enum HookResult { + /// Everything went fine + Ok, + /// Hook returned error + NotOk(String), +} + +fn run_hook(path: &str, cmd: &str, args: &[&str]) -> HookResult { + let output = + Command::new(cmd).args(args).current_dir(path).output(); + + let output = output.expect("general hook error"); + + if output.status.success() { + HookResult::Ok + } else { + let err = String::from_utf8(output.stderr).unwrap(); + let out = String::from_utf8(output.stdout).unwrap(); + let formatted = format!("{}{}", out, err); + + HookResult::NotOk(formatted) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::tests::repo_init; + use std::fs::File; + + #[test] + fn test_smoke() { + let (_td, repo) = repo_init(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let mut msg = String::from("test"); + let res = hooks_commit_msg(repo_path, &mut msg); + + assert_eq!(res, HookResult::Ok); + + let res = hooks_post_commit(repo_path); + + assert_eq!(res, HookResult::Ok); + } + + #[test] + #[cfg(not(windows))] + fn test_hooks_commit_msg_ok() { + let (_td, repo) = repo_init(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let hook = b" +#!/bin/sh +exit 0 + "; + + File::create(&root.join(HOOK_COMMIT_MSG)) + .unwrap() + .write_all(hook) + .unwrap(); + + Command::new("chmod") + .args(&["+x", HOOK_COMMIT_MSG]) + .current_dir(root) + .output() + .unwrap(); + + let mut msg = String::from("test"); + let res = hooks_commit_msg(repo_path, &mut msg); + + assert_eq!(res, HookResult::Ok); + + assert_eq!(msg, String::from("test")); + } + + #[test] + #[cfg(not(windows))] + fn test_hooks_commit_msg() { + let (_td, repo) = repo_init(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let hook = b" +#!/bin/sh +echo 'msg' > $1 +echo 'rejected' +exit 1 + "; + + File::create(&root.join(HOOK_COMMIT_MSG)) + .unwrap() + .write_all(hook) + .unwrap(); + + Command::new("chmod") + .args(&["+x", HOOK_COMMIT_MSG]) + .current_dir(root) + .output() + .unwrap(); + + let mut msg = String::from("test"); + let res = hooks_commit_msg(repo_path, &mut msg); + + assert_eq!( + res, + HookResult::NotOk(String::from("rejected\n")) + ); + + assert_eq!(msg, String::from("msg\n")); + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 0217a35a..ea92fb7f 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -1,11 +1,13 @@ //! sync git api pub mod diff; +mod hooks; mod hunks; mod reset; pub mod status; pub mod utils; +pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult}; pub use hunks::{stage_hunk, unstage_hunk}; pub use reset::{reset_stage, reset_workdir}; pub use utils::{commit, stage_add}; diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 918a5282..08d042e0 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -30,7 +30,7 @@ pub fn repo(repo_path: &str) -> Repository { repo } -/// +/// this does not run any git hooks pub fn commit(repo_path: &str, msg: &str) { scope_time!("commit"); diff --git a/src/components/commit.rs b/src/components/commit.rs index a906c5ca..9aa0c30d 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -5,8 +5,10 @@ use super::{ use crate::{keys, strings, ui}; use asyncgit::{sync, CWD}; use crossterm::event::{Event, KeyCode}; +use log::error; use std::borrow::Cow; use strings::commands; +use sync::HookResult; use tui::{ backend::Backend, layout::{Alignment, Rect}, @@ -121,7 +123,18 @@ impl Component for CommitComponent { impl CommitComponent { fn commit(&mut self) { + if let HookResult::NotOk(e) = + sync::hooks_commit_msg(CWD, &mut self.msg) + { + error!("commit-msg hook error: {}", e); + return; + } + sync::commit(CWD, &self.msg); + if let HookResult::NotOk(e) = sync::hooks_post_commit(CWD) { + error!("post-commit hook error: {}", e); + } + self.msg.clear(); self.hide();