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(