From 370be7df181edfd3c69dd37ed172bd655652166a Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Thu, 23 Feb 2023 00:49:37 +0800 Subject: [PATCH] feat: add commit-msg-rs --- commit-msg-rs/Cargo.lock | 241 ++++++++++++++++++++++++++++++++++++++ commit-msg-rs/Cargo.toml | 10 ++ commit-msg-rs/src/main.rs | 190 ++++++++++++++++++++++++++++++ 3 files changed, 441 insertions(+) create mode 100644 commit-msg-rs/Cargo.lock create mode 100644 commit-msg-rs/Cargo.toml create mode 100644 commit-msg-rs/src/main.rs diff --git a/commit-msg-rs/Cargo.lock b/commit-msg-rs/Cargo.lock new file mode 100644 index 0000000..db70635 --- /dev/null +++ b/commit-msg-rs/Cargo.lock @@ -0,0 +1,241 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "commit-msg-rs" +version = "0.1.0" +dependencies = [ + "regex", + "rust_util", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rust_util" +version = "0.6.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df24005feacce81f4ae340464b39c380f7e01e7225bfdef62d40cb44cb1c11d7" +dependencies = [ + "lazy_static", + "libc", + "term", + "term_size", +] + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/commit-msg-rs/Cargo.toml b/commit-msg-rs/Cargo.toml new file mode 100644 index 0000000..626aeee --- /dev/null +++ b/commit-msg-rs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "commit-msg-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "1.7.1" +rust_util = "0.6" diff --git a/commit-msg-rs/src/main.rs b/commit-msg-rs/src/main.rs new file mode 100644 index 0000000..c1682c3 --- /dev/null +++ b/commit-msg-rs/src/main.rs @@ -0,0 +1,190 @@ +#!/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!"); +} +