From 405d3f50ca610cb15426340967321b79ad9daca1 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 6 Aug 2022 02:12:55 +0800 Subject: [PATCH] feat: add scripts/commit-msg.crs --- scripts/commit-msg.crs | 194 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100755 scripts/commit-msg.crs diff --git a/scripts/commit-msg.crs b/scripts/commit-msg.crs new file mode 100755 index 0000000..1af7467 --- /dev/null +++ b/scripts/commit-msg.crs @@ -0,0 +1,194 @@ +#!/usr/bin/env runrs +// cargo-deps: regex="1.3.9" +use std::{ env, fs, process }; +use std::path::PathBuf; +use std::os::unix::fs::PermissionsExt; + +const RED: &str = "\x1B[91m"; +const GREEN: &str = "\x1B[92m"; +const YELLOW: &str = "\x1B[93m"; +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.crs usage` +/// +/// Install: +/// `commit-msg.crs install` +fn main() { + if is_verbose() { print_info(&format!("Arguments:\n- {}", env::args().collect::>().join("\n- "))); } + let arg1 = env::args().nth(1).unwrap_or_else(|| { + exit_with_error_message("Commit message is EMPTY!"); + }); + + if arg1 == "usage" || arg1 == "USAGE" { + print_usage(); return; + } + + let is_install = arg1 == "install" || arg1 == "INSTALL"; + let is_force_install = arg1 == "forceinstall" || arg1 == "FORCEINSTALL" + || arg1 == "force-install" || arg1 == "FORCE-INSTALL" + || arg1 == "installforce" || arg1 == "INSTALLFORCE" + || arg1 == "install-force" || 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| { + exit_with_error_message(&format!("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) => exit_with_error_message(&format!("Parse regexp: {}, failed: {}", re_str, e)), + }; + let is_commit_message_matches = re.is_match(&commit_message); + + print_info(&format!("Commit message: {}{}{}", UNDER, commit_message.trim(), END)); + if is_commit_message_matches { + print_ok("Commit message rule matches"); + } else { + print_usage(); + exit_with_error_message("Commit message rule is NOT matched"); + } +} + +fn is_verbose() -> bool { + if let Ok(v) = env::var("VERBOSE") { v == "1" } else { false } +} + +fn print_usage() { + if let Some(usage) = get_commit_msg_usage() { + print_info(&usage); return; + } + print_info(&format!(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| + exit_with_error_message(&format!("Get current dir failed: {}", e)) + ); + loop { + if is_verbose() { print_info(&format!("Check git path: {:?}", p)); } + if p.join(".git").is_dir() { + if is_verbose() { print_ok(&format!("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(|| exit_with_error_message("Git root path not found!")) +} + +fn get_home_e() -> PathBuf { + PathBuf::from(env::var("HOME").unwrap_or_else(|e| + exit_with_error_message(&format!("Get env HOME failed: {}!", e)) + )) +} + +fn get_commit_msg_file(file: &str) -> Option { + let settings_regexp = search_git_root_path_e().join("settings").join(file); + if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", settings_regexp, settings_regexp.exists())); } + if settings_regexp.exists() { + print_info(&format!("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); + if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", path_regexp, path_regexp.exists())); } + if path_regexp.exists() { + print_info(&format!("Found file: {:?}", path_regexp)); + return Some(fs::read_to_string(path_regexp).unwrap()); + } + + let home_path_regexp = get_home_e().join(&(".".to_owned() + file)); + if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", home_path_regexp, home_path_regexp.exists())); } + if home_path_regexp.exists() { + print_info(&format!("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) { + print_info(&format!("Copy file: {:?} to: {:?}", from, to)); + let from_content = fs::read(&from).unwrap_or_else(|e| + exit_with_error_message(&format!("Read file: {:?}, failed: {}", from, e)) + ); + fs::write(to, from_content).unwrap_or_else(|e| + exit_with_error_message(&format!("Write file: {:?}, failed: {}", to, e)) + ); + fs::set_permissions(to, PermissionsExt::from_mode(0o755)).unwrap_or_else(|e| + exit_with_error_message(&format!("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.crs"); + let commig_msg_exec_file = if commit_msg_crs.exists() { + commit_msg_crs + } else { + print_warn(&format!("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() { + exit_with_error_message(&format!("Path {:?} NOT exists!", git_hooks)); + } + let git_hooks_commit_msg = git_hooks.join("commit-msg"); + if git_hooks_commit_msg.exists() && !force { + exit_with_error_message(&format!("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); + + print_ok("Install commit-msg to repo successed!"); +} + +fn exit_with_error() -> ! { process::exit(1) } +fn exit_with_error_message(msg: &str) -> ! { print_error(msg); exit_with_error() } + +fn print_info(msg: &str) { println!("{b}[INFO ]{e} {m}", b = BOLD, e = END, m = msg); } +fn print_ok(msg: &str) { println!("{g}{b}[OK ]{e} {g}{m}{e}", g = GREEN, b = BOLD, e = END, m = msg); } +fn print_warn(msg: &str) { println!("{y}{b}[WARN ]{e} {y}{m}{e}", y = YELLOW, b = BOLD, e = END, m = msg); } +fn print_error(msg: &str) { println!("{r}{b}[ERROR]{e} {r}{m}{e}", r = RED, b = BOLD, e = END, m = msg); }