support hooks: post-commit,commit-msg (#15)

* support hooks: post-commit and commit-msg
* some unittests
* exclude tests on windows for now
This commit is contained in:
Stephan Dilly 2020-04-09 09:23:59 +02:00 committed by GitHub
parent d0fce2dce4
commit 36ff0be9d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 205 additions and 6 deletions

10
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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"

176
asyncgit/src/sync/hooks.rs Normal file
View file

@ -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"));
}
}

View file

@ -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};

View file

@ -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");

View file

@ -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();