#!/usr/bin/env runrs //! ```cargo //! [dependencies] //! regex = "1.7.1" //! rust_util = "0.6" //! ``` use std::{env, fs}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use rust_util::{debugging, failure_and_exit, information, success, warning}; use rust_util::util_file::get_home_path; const BOLD: &str = "\x1B[1m"; const UNDER: &str = "\x1B[4m"; const END: &str = "\x1B[0m"; const DEFAULT_COMMIT_MSG_REGEXP: &str = "^(feat|fix|docs|style|refactor|test|chore)(\\([\\w\\-_.]+\\))?:\\s*.*"; /// Git commit message format check util /// /// Commit format MUST be (): subject /// /// Usage: /// `commit-msg.rs usage` /// /// Install: /// `commit-msg.rs install` fn main() { debugging!("Arguments:\n- {}", env::args().collect::>().join("\n- ")); let arg1 = env::args().nth(1).unwrap_or_else(|| { failure_and_exit!("Commit message is EMPTY!"); }); if arg1 == "usage" || arg1 == "USAGE" || arg1 == "--help" || arg1 == "-h" { print_usage(); return; } let lower_arg1 = arg1.to_ascii_lowercase(); let is_install = lower_arg1 == "install"; let is_force_install = lower_arg1 == "forceinstall" || lower_arg1 == "force-install" || lower_arg1 == "installforce" || lower_arg1 == "install-force"; if is_install || is_force_install { install_commit_msg(is_force_install); return; } let commit_message = fs::read_to_string(arg1).unwrap_or_else(|e| { failure_and_exit!("Read commit message failed: {}", e); }); let re_str = get_commit_msg_regexp(); let re = match regex::Regex::new(re_str.trim()) { Ok(re) => re, Err(e) => failure_and_exit!("Parse regexp: {}, failed: {}", re_str, e), }; let is_commit_message_matches = re.is_match(&commit_message); information!("Commit message: {}{}{}", UNDER, commit_message.trim(), END); if is_commit_message_matches { success!("Commit message rule matches"); } else { print_usage(); failure_and_exit!("Commit message rule is NOT matched"); } } fn print_usage() { if let Some(usage) = get_commit_msg_usage() { information!("{}",&usage); return; } information!(r#"Please follow the commit spec: Format: {b}{e}(){b}: {e} is optional {b}feat{e}: add hat wobble ^--^ ^------------^ | | | +-> Summary in present tense. | +-------> Type: chore, docs, feat, fix, refactor, style, or test. {b}feat{e} : new feature for the user, not a new feature for build script {b}fix{e} : bug fix for the user, not a fix to a build script {b}docs{e} : changes to the documentation {b}style{e} : formatting, missing semi colons, etc; no production code change {b}refactor{e} : refactoring production code, eg. renaming a variable {b}test{e} : adding missing tests, refactoring tests; no production code change {b}chore{e} : updating grunt tasks etc; no production code change Reference: {u}https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716{e} or mirror: {u}https://hatter.ink/wiki/view_raw.action?__access_token=PUBLIC&id=42{e} "#, b = BOLD, e = END, u = UNDER); } fn search_git_root_path() -> Option { let mut p = PathBuf::from(".").canonicalize().unwrap_or_else(|e| failure_and_exit!("Get current dir failed: {}", e) ); loop { debugging!("Check git path: {:?}", p); if p.join(".git").is_dir() { debugging!("Found git path: {:?}", p); return Some(p); } if !p.pop() { return None; } } } fn search_git_root_path_e() -> PathBuf { search_git_root_path().unwrap_or_else(|| failure_and_exit!("Git root path not found!")) } fn get_home_e() -> PathBuf { get_home_path().unwrap_or_else(|| failure_and_exit!("Get user home failed!")) } fn get_commit_msg_file(file: &str) -> Option { let settings_regexp = search_git_root_path_e().join("settings").join(file); debugging!("Check file: {:?}, exists: {}", settings_regexp, settings_regexp.exists()); if settings_regexp.exists() { information!("Found file: {:?}", settings_regexp); return Some(fs::read_to_string(settings_regexp).unwrap()); } let path_regexp = search_git_root_path_e().join(".git").join("hooks").join(file); debugging!("Check file: {:?}, exists: {}", path_regexp, path_regexp.exists()); if path_regexp.exists() { information!("Found file: {:?}", path_regexp); return Some(fs::read_to_string(path_regexp).unwrap()); } let home_path_regexp = get_home_e().join(&(".".to_owned() + file)); debugging!("Check file: {:?}, exists: {}", home_path_regexp, home_path_regexp.exists()); if home_path_regexp.exists() { information!("Found file: {:?}", home_path_regexp); return Some(fs::read_to_string(home_path_regexp).unwrap()); } None } fn get_commit_msg_regexp() -> String { get_commit_msg_file("commit-msg-regexp").unwrap_or_else(|| DEFAULT_COMMIT_MSG_REGEXP.to_owned()) } fn get_commit_msg_usage() -> Option { get_commit_msg_file("commit-msg-regexp-usage") } fn copy_file_and_apply_executable_permission(from: &PathBuf, to: &PathBuf) { information!("Copy file: {:?} to: {:?}", from, to); let from_content = fs::read(&from).unwrap_or_else(|e| failure_and_exit!("Read file: {:?}, failed: {}", from, e) ); fs::write(to, from_content).unwrap_or_else(|e| failure_and_exit!("Write file: {:?}, failed: {}", to, e) ); fs::set_permissions(to, PermissionsExt::from_mode(0o755)).unwrap_or_else(|e| failure_and_exit!("Apply executable permission on file: {:?}, failed: {}", to, e) ); } fn install_commit_msg(force: bool) { let commit_msg_crs = get_home_e().join("bin").join("commit-msg.rs"); let commig_msg_exec_file = if commit_msg_crs.exists() { commit_msg_crs } else { warning!("File {:?} NOT exists!", commit_msg_crs); PathBuf::from(env::args().next().unwrap()) }; let git_hooks = search_git_root_path_e().join(".git").join("hooks"); if !git_hooks.exists() { failure_and_exit!("Path {:?} NOT exists!", git_hooks); } let git_hooks_commit_msg = git_hooks.join("commit-msg"); if git_hooks_commit_msg.exists() && !force { failure_and_exit!("File {:?} exists! or try {}forceinstall{}.", git_hooks_commit_msg, BOLD, END); } copy_file_and_apply_executable_permission(&commig_msg_exec_file, &git_hooks_commit_msg); success!("Install commit-msg to repo succeed!"); }