Implement git hook support on windows

closes #14
This commit is contained in:
Mehran Kordi 2020-05-16 19:08:44 +02:00 committed by GitHub
parent 7761336963
commit 19a0cb2db6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 52 deletions

View file

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- introduced proper changelog - introduced proper changelog
- hook support on windows ([#14](https://github.com/extrawurst/gitui/issues/14))
### Changed ### Changed
- show longer commit messages in log view - show longer commit messages in log view

10
Cargo.lock generated
View file

@ -33,7 +33,6 @@ version = "0.2.3"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"git2", "git2",
"is_executable",
"log", "log",
"rayon-core", "rayon-core",
"scopetime", "scopetime",
@ -335,15 +334,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.9.0" version = "0.9.0"

View file

@ -15,7 +15,8 @@ git2 = { version = "0.13.5", default-features = false }
rayon-core = "1.7" rayon-core = "1.7"
crossbeam-channel = "0.4" crossbeam-channel = "0.4"
log = "0.4" log = "0.4"
is_executable = "0.1"
scopetime = { path = "../scopetime", version = "0.1" } scopetime = { path = "../scopetime", version = "0.1" }
tempfile = "3.1" thiserror = "1.0"
thiserror = "1.0"
[dev-dependencies]
tempfile = "3.1"

View file

@ -1,17 +1,21 @@
use crate::error::{Error, Result}; use crate::error::Result;
use is_executable::IsExecutable;
use scopetime::scope_time; use scopetime::scope_time;
use std::fs::File;
use std::path::PathBuf;
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
path::Path, path::Path,
process::Command, process::Command,
}; };
use tempfile::NamedTempFile;
const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit"; const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit";
const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg"; const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = ".git/COMMIT_EDITMSG";
/// /// this hook is documented here https://git-scm.com/docs/githooks#_commit_msg
/// we use the same convention as other git clients to create a temp file containing
/// the commit message at `.git/COMMIT_EDITMSG` and pass it's relative path as the only
/// parameter to the hook script.
pub fn hooks_commit_msg( pub fn hooks_commit_msg(
repo_path: &str, repo_path: &str,
msg: &mut String, msg: &mut String,
@ -19,23 +23,19 @@ pub fn hooks_commit_msg(
scope_time!("hooks_commit_msg"); scope_time!("hooks_commit_msg");
if hook_runable(repo_path, HOOK_COMMIT_MSG) { if hook_runable(repo_path, HOOK_COMMIT_MSG) {
let mut file = NamedTempFile::new()?; let temp_file =
Path::new(repo_path).join(HOOK_COMMIT_MSG_TEMP_FILE);
File::create(&temp_file)?.write_all(msg.as_bytes())?;
write!(file, "{}", msg)?; let res = run_hook(
repo_path,
let file_path = file.path().to_str().ok_or_else(|| { HOOK_COMMIT_MSG,
Error::Generic( &[HOOK_COMMIT_MSG_TEMP_FILE],
"temp file path contains invalid unicode sequences." );
.to_string(),
)
})?;
let res = run_hook(repo_path, HOOK_COMMIT_MSG, &[&file_path]);
// load possibly altered msg // load possibly altered msg
let mut file = file.reopen()?;
msg.clear(); msg.clear();
file.read_to_string(msg)?; File::open(temp_file)?.read_to_string(msg)?;
Ok(res) Ok(res)
} else { } else {
@ -58,7 +58,7 @@ fn hook_runable(path: &str, hook: &str) -> bool {
let path = Path::new(path); let path = Path::new(path);
let path = path.join(hook); let path = path.join(hook);
path.exists() && path.is_executable() path.exists() && is_executable(path)
} }
/// ///
@ -70,20 +70,36 @@ pub enum HookResult {
NotOk(String), NotOk(String),
} }
fn run_hook(path: &str, cmd: &str, args: &[&str]) -> HookResult { /// this function calls hook scripts based on conventions documented here
match Command::new(cmd).args(args).current_dir(path).output() { /// https://git-scm.com/docs/githooks
Ok(output) => { fn run_hook(
if output.status.success() { path: &str,
HookResult::Ok hook_script: &str,
} else { args: &[&str],
let err = String::from_utf8_lossy(&output.stderr); ) -> HookResult {
let out = String::from_utf8_lossy(&output.stdout); let mut bash_args = vec![hook_script.to_string()];
let formatted = format!("{}{}", out, err); bash_args.extend_from_slice(
&args
.iter()
.map(|x| (*x).to_string())
.collect::<Vec<String>>(),
);
HookResult::NotOk(formatted) let output = Command::new("bash")
} .args(bash_args)
} .current_dir(path)
Err(e) => HookResult::NotOk(format!("{}", e)), .output();
let output = output.expect("general hook error");
if output.status.success() {
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)
} }
} }
@ -115,15 +131,17 @@ mod tests {
.write_all(hook_script) .write_all(hook_script)
.unwrap(); .unwrap();
Command::new("chmod") #[cfg(not(windows))]
.args(&["+x", hook_path]) {
.current_dir(path) Command::new("chmod")
.output() .args(&["+x", hook_path])
.unwrap(); .current_dir(path)
.output()
.unwrap();
}
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_hooks_commit_msg_ok() { fn test_hooks_commit_msg_ok() {
let (_td, repo) = repo_init().unwrap(); let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap(); let root = repo.path().parent().unwrap();
@ -145,7 +163,6 @@ exit 0
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_hooks_commit_msg() { fn test_hooks_commit_msg() {
let (_td, repo) = repo_init().unwrap(); let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap(); let root = repo.path().parent().unwrap();
@ -172,7 +189,6 @@ exit 1
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_commit_msg_no_block_but_alter() { fn test_commit_msg_no_block_but_alter() {
let (_td, repo) = repo_init().unwrap(); let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap(); let root = repo.path().parent().unwrap();
@ -193,3 +209,22 @@ exit 0
assert_eq!(msg, String::from("msg\n")); assert_eq!(msg, String::from("msg\n"));
} }
} }
#[cfg(not(windows))]
fn is_executable(path: PathBuf) -> bool {
use std::os::unix::fs::PermissionsExt;
let metadata = match path.metadata() {
Ok(metadata) => metadata,
Err(_) => return false,
};
let permissions = metadata.permissions();
permissions.mode() & 0o111 != 0
}
#[cfg(windows)]
/// windows does not consider bash scripts to be executable so we consider everything
/// to be executable (which is not far from the truth for windows platform.)
fn is_executable(_: PathBuf) -> bool {
true
}