use anyhow::{bail, Result}; use lazy_static::lazy_static; use regex::Regex; /// Conventional commit types pub const CONVENTIONAL_TYPES: &[&str] = &[ "feat", // A new feature "fix", // A bug fix "docs", // Documentation only changes "style", // Changes that do not affect the meaning of the code "refactor", // A code change that neither fixes a bug nor adds a feature "perf", // A code change that improves performance "test", // Adding missing tests or correcting existing tests "build", // Changes that affect the build system or external dependencies "ci", // Changes to CI configuration files and scripts "chore", // Other changes that don't modify src or test files "revert", // Reverts a previous commit ]; /// Commitlint configuration types (extends conventional) pub const COMMITLINT_TYPES: &[&str] = &[ "feat", // A new feature "fix", // A bug fix "docs", // Documentation only changes "style", // Changes that do not affect the meaning of the code "refactor", // A code change that neither fixes a bug nor adds a feature "perf", // A code change that improves performance "test", // Adding missing tests or correcting existing tests "build", // Changes that affect the build system or external dependencies "ci", // Changes to CI configuration files and scripts "chore", // Other changes that don't modify src or test files "revert", // Reverts a previous commit "wip", // Work in progress "init", // Initial commit "update", // Update existing functionality "remove", // Remove functionality "security", // Security-related changes ]; lazy_static! { /// Regex for conventional commit format static ref CONVENTIONAL_COMMIT_REGEX: Regex = Regex::new( r"^(?Pfeat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\((?P[^)]+)\))?(?P!)?: (?P.+)$" ).unwrap(); /// Regex for scope validation static ref SCOPE_REGEX: Regex = Regex::new( r"^[a-z0-9-]+$" ).unwrap(); /// Regex for version tag validation (semver) static ref SEMVER_REGEX: Regex = Regex::new( r"^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" ).unwrap(); /// Regex for email validation static ref EMAIL_REGEX: Regex = Regex::new( r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" ).unwrap(); /// Regex for GPG key ID validation static ref GPG_KEY_ID_REGEX: Regex = Regex::new( r"^[A-F0-9]{16,40}$" ).unwrap(); } /// Validate conventional commit message pub fn validate_conventional_commit(message: &str) -> Result<()> { let first_line = message.lines().next().unwrap_or(""); if !CONVENTIONAL_COMMIT_REGEX.is_match(first_line) { bail!( "Invalid conventional commit format. Expected: [optional scope]: \n\ Valid types: {}", CONVENTIONAL_TYPES.join(", ") ); } if first_line.len() > 100 { bail!("Commit subject too long (max 100 characters)"); } Ok(()) } /// Validate @commitlint commit message pub fn validate_commitlint_commit(message: &str) -> Result<()> { let first_line = message.lines().next().unwrap_or(""); let parts: Vec<&str> = first_line.splitn(2, ':').collect(); if parts.len() != 2 { bail!("Invalid commit format. Expected: [optional scope]: "); } let type_part = parts[0]; let subject = parts[1].trim(); let commit_type = type_part .split('(') .next() .unwrap_or("") .trim_end_matches('!'); if !COMMITLINT_TYPES.contains(&commit_type) { bail!( "Invalid commit type: '{}'. Valid types: {}", commit_type, COMMITLINT_TYPES.join(", ") ); } if subject.is_empty() { bail!("Commit subject cannot be empty"); } if subject.len() < 4 { bail!("Commit subject too short (min 4 characters)"); } if subject.len() > 100 { bail!("Commit subject too long (max 100 characters)"); } if subject.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { bail!("Commit subject should not start with uppercase letter"); } if subject.ends_with('.') { bail!("Commit subject should not end with a period"); } Ok(()) } /// Validate scope name pub fn validate_scope(scope: &str) -> Result<()> { if scope.is_empty() { bail!("Scope cannot be empty"); } if !SCOPE_REGEX.is_match(scope) { bail!("Invalid scope format. Use lowercase letters, numbers, and hyphens only"); } Ok(()) } /// Validate semantic version tag pub fn validate_semver(version: &str) -> Result<()> { let version = version.trim_start_matches('v'); if !SEMVER_REGEX.is_match(version) { bail!( "Invalid semantic version format. Expected: MAJOR.MINOR.PATCH[-prerelease][+build]\n\ Examples: 1.0.0, 1.2.3-beta, v2.0.0+build123" ); } Ok(()) } /// Validate email address pub fn validate_email(email: &str) -> Result<()> { if !EMAIL_REGEX.is_match(email) { bail!("Invalid email address format"); } Ok(()) } /// Validate GPG key ID pub fn validate_gpg_key_id(key_id: &str) -> Result<()> { if !GPG_KEY_ID_REGEX.is_match(key_id) { bail!("Invalid GPG key ID format. Expected 16-40 hexadecimal characters"); } Ok(()) } /// Validate profile name pub fn validate_profile_name(name: &str) -> Result<()> { if name.is_empty() { bail!("Profile name cannot be empty"); } if name.len() > 50 { bail!("Profile name too long (max 50 characters)"); } if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') { bail!("Profile name can only contain letters, numbers, hyphens, and underscores"); } Ok(()) } /// Check if commit type is valid pub fn is_valid_commit_type(commit_type: &str, use_commitlint: bool) -> bool { let types = if use_commitlint { COMMITLINT_TYPES } else { CONVENTIONAL_TYPES }; types.contains(&commit_type) } /// Get available commit types pub fn get_commit_types(use_commitlint: bool) -> &'static [&'static str] { if use_commitlint { COMMITLINT_TYPES } else { CONVENTIONAL_TYPES } } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_conventional_commit() { assert!(validate_conventional_commit("feat: add new feature").is_ok()); assert!(validate_conventional_commit("fix(auth): fix login bug").is_ok()); assert!(validate_conventional_commit("feat!: breaking change").is_ok()); assert!(validate_conventional_commit("invalid: message").is_err()); } #[test] fn test_validate_semver() { assert!(validate_semver("1.0.0").is_ok()); assert!(validate_semver("v1.2.3").is_ok()); assert!(validate_semver("2.0.0-beta.1").is_ok()); assert!(validate_semver("invalid").is_err()); } #[test] fn test_validate_email() { assert!(validate_email("test@example.com").is_ok()); assert!(validate_email("invalid-email").is_err()); } #[test] fn test_validate_profile_name() { assert!(validate_profile_name("personal").is_ok()); assert!(validate_profile_name("work-company").is_ok()); assert!(validate_profile_name("").is_err()); assert!(validate_profile_name("invalid name").is_err()); } }