Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ impl CommitMessage {

// Footer starts with BREAKING CHANGE or a token like "Closes:", "Fixes:", etc.
if line.starts_with("BREAKING CHANGE:")
|| Regex::new(r"^[A-Z][a-z]+(?:-[A-Z][a-z]+)*:").unwrap().is_match(line) {
|| Regex::new(r"^[A-Z][a-z]+(?:-[A-Z][a-z]+)*:")
.unwrap()
.is_match(line)
{
in_footer = true;
}

Expand Down Expand Up @@ -70,13 +73,15 @@ impl CommitMessage {
let re = Regex::new(pattern)?;

if let Some(caps) = re.captures(&self.header) {
let r#type = caps.name("type")
let r#type = caps
.name("type")
.map(|m| m.as_str().to_string())
.ok_or_else(|| anyhow::anyhow!("Missing 'type' in commit message"))?;

let scope = caps.name("scope").map(|m| m.as_str().to_string());
let breaking = caps.name("breaking").is_some();
let subject = caps.name("subject")
let subject = caps
.name("subject")
.map(|m| m.as_str().to_string())
.ok_or_else(|| anyhow::anyhow!("Missing 'subject' in commit message"))?;

Expand All @@ -85,8 +90,13 @@ impl CommitMessage {
if let Some(ref footer) = self.footer {
for line in footer.lines() {
if line.starts_with("BREAKING CHANGE:") {
footer_map.insert("BREAKING CHANGE".to_string(),
line.strip_prefix("BREAKING CHANGE:").unwrap_or("").trim().to_string());
footer_map.insert(
"BREAKING CHANGE".to_string(),
line.strip_prefix("BREAKING CHANGE:")
.unwrap_or("")
.trim()
.to_string(),
);
} else if let Some((key, value)) = line.split_once(':') {
footer_map.insert(key.trim().to_string(), value.trim().to_string());
}
Expand All @@ -102,7 +112,11 @@ impl CommitMessage {
breaking: breaking || breaking_from_footer,
subject,
body: self.body.clone(),
footer: if footer_map.is_empty() { None } else { Some(footer_map) },
footer: if footer_map.is_empty() {
None
} else {
Some(footer_map)
},
})
} else {
anyhow::bail!("Commit message does not match conventional commit format")
Expand Down Expand Up @@ -135,4 +149,3 @@ mod tests {
assert_eq!(msg.body, Some("This is the body".to_string()));
}
}

1 change: 0 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,3 @@ impl Config {
Ok(Config::default())
}
}

29 changes: 17 additions & 12 deletions src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ impl HookInstaller {

// Create hooks directory if it doesn't exist
if !hooks_dir.exists() {
fs::create_dir_all(&hooks_dir)
.context("Failed to create .git/hooks directory")?;
fs::create_dir_all(&hooks_dir).context("Failed to create .git/hooks directory")?;
}

// Find cargo-commitlint binary
Expand All @@ -24,8 +23,7 @@ impl HookInstaller {
let hook_path = hooks_dir.join("commit-msg");
let hook_content = Self::generate_hook_script(&binary_path);

fs::write(&hook_path, hook_content)
.context("Failed to write commit-msg hook")?;
fs::write(&hook_path, hook_content).context("Failed to write commit-msg hook")?;

// Make hook executable
#[cfg(unix)]
Expand All @@ -36,7 +34,10 @@ impl HookInstaller {
fs::set_permissions(&hook_path, perms)?;
}

println!("✓ Git hook installed successfully at {}", hook_path.display());
println!(
"✓ Git hook installed successfully at {}",
hook_path.display()
);
println!(" Commit messages will now be validated using cargo-commitlint");

Ok(())
Expand Down Expand Up @@ -92,19 +93,24 @@ impl HookInstaller {
.output()?;

if output.status.success() {
let workspace_root = String::from_utf8(output.stdout)?
.trim()
.to_string();
let workspace_path = Path::new(&workspace_root).parent()
let workspace_root = String::from_utf8(output.stdout)?.trim().to_string();
let workspace_path = Path::new(&workspace_root)
.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid workspace path"))?;

// Check target/release or target/debug
let release_path = workspace_path.join("target").join("release").join("cargo-commitlint");
let release_path = workspace_path
.join("target")
.join("release")
.join("cargo-commitlint");
if release_path.exists() {
return Ok(release_path);
}

let debug_path = workspace_path.join("target").join("debug").join("cargo-commitlint");
let debug_path = workspace_path
.join("target")
.join("debug")
.join("cargo-commitlint");
if debug_path.exists() {
return Ok(debug_path);
}
Expand Down Expand Up @@ -153,4 +159,3 @@ mod tests {
assert!(script.contains("/usr/local/bin/cargo-commitlint"));
}
}

20 changes: 8 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod config;
mod commit;
mod config;
mod hook;
mod validator;

Expand All @@ -10,7 +10,9 @@ use std::process;
#[derive(Parser)]
#[command(name = "cargo-commitlint")]
#[command(bin_name = "cargo commitlint")]
#[command(about = "A Rust-based commit message linter following Conventional Commits specification")]
#[command(
about = "A Rust-based commit message linter following Conventional Commits specification"
)]
#[command(version)]
struct Cli {
#[command(subcommand)]
Expand All @@ -36,24 +38,18 @@ enum Commands {

fn main() {
// Filter out "commitlint" argument if passed by cargo
let args: Vec<String> = std::env::args()
.filter(|arg| arg != "commitlint")
.collect();
let args: Vec<String> = std::env::args().filter(|arg| arg != "commitlint").collect();

let cli = Cli::parse_from(args);

let result = match cli.command {
Commands::Install => {
hook::HookInstaller::install()
.map_err(|e| format!("Failed to install hook: {}", e))
hook::HookInstaller::install().map_err(|e| format!("Failed to install hook: {}", e))
}
Commands::Uninstall => {
hook::HookInstaller::uninstall()
.map_err(|e| format!("Failed to uninstall hook: {}", e))
}
Commands::Check { message, config } => {
validate_commit_message(message, config)
hook::HookInstaller::uninstall().map_err(|e| format!("Failed to uninstall hook: {}", e))
}
Commands::Check { message, config } => validate_commit_message(message, config),
};

match result {
Expand Down
29 changes: 15 additions & 14 deletions src/validator.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::config::Config;
use crate::commit::{CommitMessage, ConventionalCommit};
use crate::config::Config;
use regex::Regex;
use std::collections::HashSet;

Expand All @@ -23,9 +23,11 @@ impl Validator {

// Check if commit should be ignored
for ignore_pattern in &self.config.ignores {
if Regex::new(ignore_pattern).ok()
if Regex::new(ignore_pattern)
.ok()
.and_then(|re| Some(re.is_match(commit_msg)))
.unwrap_or(false) {
.unwrap_or(false)
{
return Ok(()); // Skip validation for ignored commits
}
}
Expand Down Expand Up @@ -110,10 +112,7 @@ impl Validator {
if !self.validate_case(&commit.r#type, &self.config.rules.r#type.case) {
errors.push(ValidationError {
rule: "type-case".to_string(),
message: format!(
"type must be {}",
self.config.rules.r#type.case
),
message: format!("type must be {}", self.config.rules.r#type.case),
});
}

Expand All @@ -135,10 +134,7 @@ impl Validator {
if !self.validate_case(scope, &self.config.rules.scope.case) {
errors.push(ValidationError {
rule: "scope-case".to_string(),
message: format!(
"scope must be {}",
self.config.rules.scope.case
),
message: format!("scope must be {}", self.config.rules.scope.case),
});
}
}
Expand Down Expand Up @@ -173,7 +169,10 @@ impl Validator {

// Validate subject full stop
if !self.config.rules.subject_full_stop.is_empty() {
if commit.subject.ends_with(&self.config.rules.subject_full_stop) {
if commit
.subject
.ends_with(&self.config.rules.subject_full_stop)
{
errors.push(ValidationError {
rule: "subject-full-stop".to_string(),
message: format!(
Expand Down Expand Up @@ -281,7 +280,10 @@ impl Validator {
"start-case" => {
// Start Case: Each Word Starts With Capital
subject.split_whitespace().all(|word| {
word.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
word.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
})
}
_ => true, // Unknown case, skip validation
Expand Down Expand Up @@ -310,4 +312,3 @@ mod tests {
assert!(result.is_err());
}
}

Loading