mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
230 lines
5.7 KiB
Rust
230 lines
5.7 KiB
Rust
use crate::error::Result;
|
|
use scopetime::scope_time;
|
|
use std::fs::File;
|
|
use std::path::PathBuf;
|
|
use std::{
|
|
io::{Read, Write},
|
|
path::Path,
|
|
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");
|
|
|
|
if hook_runable(repo_path, HOOK_COMMIT_MSG) {
|
|
let temp_file =
|
|
Path::new(repo_path).join(HOOK_COMMIT_MSG_TEMP_FILE);
|
|
File::create(&temp_file)?.write_all(msg.as_bytes())?;
|
|
|
|
let res = run_hook(
|
|
repo_path,
|
|
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");
|
|
|
|
if hook_runable(repo_path, HOOK_POST_COMMIT) {
|
|
Ok(run_hook(repo_path, HOOK_POST_COMMIT, &[]))
|
|
} else {
|
|
Ok(HookResult::Ok)
|
|
}
|
|
}
|
|
|
|
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(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sync::tests::repo_init;
|
|
use std::fs::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() {
|
|
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_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"));
|
|
}
|
|
}
|
|
|
|
#[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
|
|
}
|