Files
QuiCommit/src/utils/validators.rs
SidneyZhang a514cdc69f ⬆️ chore(Cargo.toml):升级版本号至0.1.7
♻️ refactor(changelog.rs):移除prepend参数,改为自动前置到现有changelog
♻️ refactor(formatter.rs):移除未使用的日期和格式化函数
♻️ refactor(validators.rs):移除未使用的SSH密钥验证功能
2026-02-14 15:00:59 +08:00

251 lines
7.7 KiB
Rust

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"^(?P<type>feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?: (?P<description>.+)$"
).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?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?: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<buildmetadata>[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: <type>[optional scope]: <description>\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: <type>[optional scope]: <subject>");
}
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());
}
}