From 99c3277e940c0a36804541ffa6c48ca6e0067f42 Mon Sep 17 00:00:00 2001 From: pm100 Date: Sat, 31 Oct 2020 17:06:48 -0700 Subject: [PATCH 1/2] adding pre-commit hook (#386) see #313 --- asyncgit/src/sync/hooks.rs | 102 ++++++++++++++++++++++++++++++++++--- asyncgit/src/sync/mod.rs | 4 +- src/components/commit.rs | 10 ++++ src/components/msg.rs | 33 ++++++++---- 4 files changed, 131 insertions(+), 18 deletions(-) diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs index 50f2ed6ee1..021fd2573a 100644 --- a/asyncgit/src/sync/hooks.rs +++ b/asyncgit/src/sync/hooks.rs @@ -9,6 +9,7 @@ use std::{ }; const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit"; +const HOOK_PRE_COMMIT: &str = ".git/hooks/pre-commit"; const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg"; const HOOK_COMMIT_MSG_TEMP_FILE: &str = ".git/COMMIT_EDITMSG"; @@ -45,6 +46,21 @@ pub fn hooks_commit_msg( } } +/// this hook is documented here https://git-scm.com/docs/githooks#_pre_commit +/// +pub fn hooks_pre_commit(repo_path: &str) -> Result { + scope_time!("hooks_pre_commit"); + + let work_dir = work_dir_as_string(repo_path)?; + + if hook_runable(work_dir.as_str(), HOOK_PRE_COMMIT) { + let res = run_hook(work_dir.as_str(), HOOK_PRE_COMMIT, &[]); + + Ok(res) + } else { + Ok(HookResult::Ok) + } +} /// pub fn hooks_post_commit(repo_path: &str) -> Result { scope_time!("hooks_post_commit"); @@ -94,13 +110,8 @@ fn run_hook( hook_script: &str, args: &[&str], ) -> HookResult { - let mut bash_args = vec![hook_script.to_string()]; - bash_args.extend_from_slice( - &args - .iter() - .map(|x| (*x).to_string()) - .collect::>(), - ); + let arg_str = format!("{} {}", hook_script, args.join(" ")); + let bash_args = vec!["-c".to_string(), arg_str]; let output = Command::new("bash") .args(bash_args) @@ -204,6 +215,83 @@ exit 0 assert_eq!(msg, String::from("test")); } + #[test] + fn test_pre_commit_sh() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let hook = b"#!/bin/sh +exit 0 + "; + + create_hook(root, HOOK_PRE_COMMIT, hook); + let res = hooks_pre_commit(repo_path).unwrap(); + assert_eq!(res, HookResult::Ok); + } + + #[test] + fn test_pre_commit_fail_sh() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let hook = b"#!/bin/sh +echo 'rejected' +exit 1 + "; + + create_hook(root, HOOK_PRE_COMMIT, hook); + let res = hooks_pre_commit(repo_path).unwrap(); + assert!(res != HookResult::Ok); + } + + #[test] + fn test_pre_commit_py() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + // mirror how python pre-commmit sets itself up + #[cfg(not(windows))] + let hook = b"#!/usr/bin/env python +import sys +sys.exit(0) + "; + #[cfg(windows)] + let hook = b"#!/bin/env python.exe +import sys +sys.exit(0) + "; + + create_hook(root, HOOK_PRE_COMMIT, hook); + let res = hooks_pre_commit(repo_path).unwrap(); + assert_eq!(res, HookResult::Ok); + } + + #[test] + fn test_pre_commit_fail_py() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + // mirror how python pre-commmit sets itself up + #[cfg(not(windows))] + let hook = b"#!/usr/bin/env python +import sys +sys.exit(1) + "; + #[cfg(windows)] + let hook = b"#!/bin/env python.exe +import sys +sys.exit(1) + "; + + create_hook(root, HOOK_PRE_COMMIT, hook); + let res = hooks_pre_commit(repo_path).unwrap(); + assert!(res != HookResult::Ok); + } + #[test] fn test_hooks_commit_msg_reject() { let (_td, repo) = repo_init().unwrap(); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 0425381fef..f9baea63f1 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -31,7 +31,9 @@ pub use commit_details::{ pub use commit_files::get_commit_files; pub use commits_info::{get_commits_info, CommitId, CommitInfo}; pub use diff::get_diff_commit; -pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult}; +pub use hooks::{ + hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult, +}; pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::LogWalker; diff --git a/src/components/commit.rs b/src/components/commit.rs index 650f28d484..708aeb1c40 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -194,6 +194,16 @@ impl CommitComponent { } fn commit_msg(&mut self, msg: String) -> Result<()> { + if let HookResult::NotOk(e) = sync::hooks_pre_commit(CWD)? { + log::error!("pre-commit hook error: {}", e); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "pre-commit hook error:\n{}", + e + )), + ); + return Ok(()); + } let mut msg = msg; if let HookResult::NotOk(e) = sync::hooks_commit_msg(CWD, &mut msg)? diff --git a/src/components/msg.rs b/src/components/msg.rs index 9f40436459..3a50327d05 100644 --- a/src/components/msg.rs +++ b/src/components/msg.rs @@ -4,15 +4,15 @@ use super::{ }; use crate::{keys::SharedKeyConfig, strings, ui}; use crossterm::event::Event; +use std::convert::TryFrom; use tui::{ backend::Backend, layout::{Alignment, Rect}, - text::{Span, Spans}, + text::Span, widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, Frame, }; use ui::style::SharedTheme; - pub struct MsgComponent { title: String, msg: String, @@ -32,17 +32,30 @@ impl DrawableComponent for MsgComponent { if !self.visible { return Ok(()); } - let txt = Spans::from( - self.msg - .split('\n') - .map(|string| Span::raw::(string.to_string())) - .collect::>(), - ); - let area = ui::centered_rect_absolute(65, 25, f.size()); + // determine the maximum width of text block + let lens = self + .msg + .split('\n') + .map(str::len) + .collect::>(); + let mut max = lens.iter().max().expect("max") + 2; + if max > std::u16::MAX as usize { + max = std::u16::MAX as usize; + } + let mut width = + u16::try_from(max).expect("cant fail due to check above"); + // dont overflow screen, and dont get too narrow + if width > f.size().width { + width = f.size().width + } else if width < 60 { + width = 60 + } + + let area = ui::centered_rect_absolute(width, 25, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(txt) + Paragraph::new(self.msg.clone()) .block( Block::default() .title(Span::styled( From 2e362626cb85a7e1653b83cc0d7929688201867c Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Sun, 1 Nov 2020 01:30:09 +0100 Subject: [PATCH 2/2] fix hook panicking (#393) --- asyncgit/src/sync/hooks.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs index 021fd2573a..d4569f5b8f 100644 --- a/asyncgit/src/sync/hooks.rs +++ b/asyncgit/src/sync/hooks.rs @@ -34,7 +34,7 @@ pub fn hooks_commit_msg( work_dir.as_str(), HOOK_COMMIT_MSG, &[HOOK_COMMIT_MSG_TEMP_FILE], - ); + )?; // load possibly altered msg msg.clear(); @@ -54,9 +54,7 @@ pub fn hooks_pre_commit(repo_path: &str) -> Result { let work_dir = work_dir_as_string(repo_path)?; if hook_runable(work_dir.as_str(), HOOK_PRE_COMMIT) { - let res = run_hook(work_dir.as_str(), HOOK_PRE_COMMIT, &[]); - - Ok(res) + Ok(run_hook(work_dir.as_str(), HOOK_PRE_COMMIT, &[])?) } else { Ok(HookResult::Ok) } @@ -69,7 +67,7 @@ pub fn hooks_post_commit(repo_path: &str) -> Result { let work_dir_str = work_dir.as_str(); if hook_runable(work_dir_str, HOOK_POST_COMMIT) { - Ok(run_hook(work_dir_str, HOOK_POST_COMMIT, &[])) + Ok(run_hook(work_dir_str, HOOK_POST_COMMIT, &[])?) } else { Ok(HookResult::Ok) } @@ -109,7 +107,7 @@ fn run_hook( path: &str, hook_script: &str, args: &[&str], -) -> HookResult { +) -> Result { let arg_str = format!("{} {}", hook_script, args.join(" ")); let bash_args = vec!["-c".to_string(), arg_str]; @@ -123,18 +121,16 @@ fn run_hook( "DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS", "FixPathHandlingOnWindows", ) - .output(); - - let output = output.expect("general hook error"); + .output()?; if output.status.success() { - HookResult::Ok + Ok(HookResult::Ok) } else { let err = String::from_utf8_lossy(&output.stderr); let out = String::from_utf8_lossy(&output.stdout); let formatted = format!("{}{}", out, err); - HookResult::NotOk(formatted) + Ok(HookResult::NotOk(formatted)) } }