gitui/asyncgit/src/sync/hooks.rs
2020-06-29 12:30:10 +02:00

303 lines
7.6 KiB
Rust

use super::utils::{repo, work_dir};
use crate::error::{Error, Result};
use scopetime::scope_time;
use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
};
const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit";
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(
repo_path: &str,
msg: &mut String,
) -> Result<HookResult> {
scope_time!("hooks_commit_msg");
let work_dir = work_dir_as_string(repo_path)?;
if hook_runable(work_dir.as_str(), HOOK_COMMIT_MSG) {
let temp_file = Path::new(work_dir.as_str())
.join(HOOK_COMMIT_MSG_TEMP_FILE);
File::create(&temp_file)?.write_all(msg.as_bytes())?;
let res = run_hook(
work_dir.as_str(),
HOOK_COMMIT_MSG,
&[HOOK_COMMIT_MSG_TEMP_FILE],
);
// load possibly altered msg
msg.clear();
File::open(temp_file)?.read_to_string(msg)?;
Ok(res)
} else {
Ok(HookResult::Ok)
}
}
///
pub fn hooks_post_commit(repo_path: &str) -> Result<HookResult> {
scope_time!("hooks_post_commit");
let work_dir = work_dir_as_string(repo_path)?;
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, &[]))
} else {
Ok(HookResult::Ok)
}
}
fn work_dir_as_string(repo_path: &str) -> Result<String> {
let repo = repo(repo_path)?;
work_dir(&repo)
.to_str()
.map(|s| s.to_string())
.ok_or_else(|| {
Error::Generic(
"workdir contains invalid utf8".to_string(),
)
})
}
fn hook_runable(path: &str, hook: &str) -> bool {
let path = Path::new(path);
let path = path.join(hook);
path.exists() && is_executable(path)
}
///
#[derive(Debug, PartialEq)]
pub enum HookResult {
/// Everything went fine
Ok,
/// Hook returned error
NotOk(String),
}
/// this function calls hook scripts based on conventions documented here
/// https://git-scm.com/docs/githooks
fn run_hook(
path: &str,
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::<Vec<String>>(),
);
let output = Command::new("bash")
.args(bash_args)
.current_dir(path)
.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)
}
}
#[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
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::tests::repo_init;
use std::fs::{self, File};
#[test]
fn test_smoke() {
let (_td, repo) = repo_init().unwrap();
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).unwrap();
assert_eq!(res, HookResult::Ok);
let res = hooks_post_commit(repo_path).unwrap();
assert_eq!(res, HookResult::Ok);
}
fn create_hook(path: &Path, hook_path: &str, hook_script: &[u8]) {
File::create(&path.join(hook_path))
.unwrap()
.write_all(hook_script)
.unwrap();
#[cfg(not(windows))]
{
Command::new("chmod")
.args(&["+x", hook_path])
.current_dir(path)
.output()
.unwrap();
}
}
#[test]
fn test_hooks_commit_msg_ok() {
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_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
assert_eq!(res, HookResult::Ok);
assert_eq!(msg, String::from("test"));
}
#[test]
fn test_hooks_commit_msg_reject() {
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 'msg' > $1
echo 'rejected'
exit 1
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_hooks_commit_msg_reject_in_subfolder() {
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 'msg' > $1
echo 'rejected'
exit 1
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let subfolder = root.join("foo/");
fs::create_dir_all(&subfolder).unwrap();
let mut msg = String::from("test");
let res =
hooks_commit_msg(subfolder.to_str().unwrap(), &mut msg)
.unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_commit_msg_no_block_but_alter() {
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 'msg' > $1
exit 0
";
create_hook(root, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
assert_eq!(res, HookResult::Ok);
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_post_commit_hook_reject_in_subfolder() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let hook = b"
#!/bin/sh
echo 'rejected'
exit 1
";
create_hook(root, HOOK_POST_COMMIT, hook);
let subfolder = root.join("foo/");
fs::create_dir_all(&subfolder).unwrap();
let res =
hooks_post_commit(subfolder.to_str().unwrap()).unwrap();
assert_eq!(
res,
HookResult::NotOk(String::from("rejected\n"))
);
}
}