diff --git a/Cargo.toml b/Cargo.toml index 5039552..9fc6f9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quicommit" -version = "0.1.5" +version = "0.1.7" edition = "2024" authors = ["Sidney Zhang "] description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)" diff --git a/src/commands/changelog.rs b/src/commands/changelog.rs index 130bd19..31351fa 100644 --- a/src/commands/changelog.rs +++ b/src/commands/changelog.rs @@ -39,10 +39,6 @@ pub struct ChangelogCommand { #[arg(short, long)] generate: bool, - /// Prepend to existing changelog - #[arg(short, long)] - prepend: bool, - /// Include commit hashes #[arg(long)] include_hashes: bool, @@ -162,13 +158,34 @@ impl ChangelogCommand { } } - // Write to file - if self.prepend && output_path.exists() { + // Write to file (always prepend to preserve history) + if output_path.exists() { let existing = std::fs::read_to_string(&output_path)?; - let new_content = format!("{}\n{}", changelog, existing); + let new_content = if existing.is_empty() { + format!("# Changelog\n\n{}", changelog) + } else { + let lines: Vec<&str> = existing.lines().collect(); + let mut header_end = 0; + + for (i, line) in lines.iter().enumerate() { + if i == 0 && line.starts_with('#') { + header_end = i + 1; + } else if line.trim().is_empty() { + header_end = i + 1; + } else { + break; + } + } + + let header = lines[..header_end].join("\n"); + let rest = lines[header_end..].join("\n"); + + format!("{}\n{}\n{}", header, changelog, rest) + }; std::fs::write(&output_path, new_content)?; } else { - std::fs::write(&output_path, changelog)?; + let content = format!("# Changelog\n\n{}", changelog); + std::fs::write(&output_path, content)?; } println!("{} {:?}", messages.changelog_written(), output_path); diff --git a/src/utils/formatter.rs b/src/utils/formatter.rs index 7594b5c..a8ba245 100644 --- a/src/utils/formatter.rs +++ b/src/utils/formatter.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Local, Utc}; use regex::Regex; /// Format commit message with conventional commit format @@ -12,7 +11,6 @@ pub fn format_conventional_commit( ) -> String { let mut message = String::new(); - // Type and scope message.push_str(commit_type); if let Some(s) = scope { message.push_str(&format!("({})", s)); @@ -22,12 +20,10 @@ pub fn format_conventional_commit( } message.push_str(&format!(": {}", description)); - // Body if let Some(b) = body { message.push_str(&format!("\n\n{}", b)); } - // Footer if let Some(f) = footer { message.push_str(&format!("\n\n{}", f)); } @@ -46,26 +42,22 @@ pub fn format_commitlint_commit( ) -> String { let mut message = String::new(); - // Header message.push_str(commit_type); if let Some(s) = scope { message.push_str(&format!("({})", s)); } message.push_str(&format!(": {}", subject)); - // References if let Some(refs) = references { for reference in refs { message.push_str(&format!(" #{}", reference)); } } - // Body if let Some(b) = body { message.push_str(&format!("\n\n{}", b)); } - // Footer if let Some(f) = footer { message.push_str(&format!("\n\n{}", f)); } @@ -73,38 +65,11 @@ pub fn format_commitlint_commit( message } -/// Format date for commit message -pub fn format_commit_date(date: &DateTime) -> String { - date.format("%Y-%m-%d %H:%M:%S").to_string() -} - -/// Format date for changelog -pub fn format_changelog_date(date: &DateTime) -> String { - date.format("%Y-%m-%d").to_string() -} - -/// Format tag name with version -pub fn format_tag_name(version: &str, prefix: Option<&str>) -> String { - match prefix { - Some(p) => format!("{}{}", p, version), - None => version.to_string(), - } -} - /// Wrap text at specified width pub fn wrap_text(text: &str, width: usize) -> String { textwrap::fill(text, width) } -/// Truncate text with ellipsis -pub fn truncate(text: &str, max_len: usize) -> String { - if text.len() <= max_len { - text.to_string() - } else { - format!("{}...", &text[..max_len.saturating_sub(3)]) - } -} - /// Clean commit message (remove comments, extra whitespace) pub fn clean_message(message: &str) -> String { let comment_regex = Regex::new(r"^#.*$").unwrap(); @@ -118,44 +83,6 @@ pub fn clean_message(message: &str) -> String { .to_string() } -/// Format list as markdown bullet points -pub fn format_markdown_list(items: &[String]) -> String { - items - .iter() - .map(|item| format!("- {}", item)) - .collect::>() - .join("\n") -} - -/// Format changelog section -pub fn format_changelog_section( - version: &str, - date: &str, - changes: &[(String, Vec)], -) -> String { - let mut section = format!("## [{}] - {}\n\n", version, date); - - for (category, items) in changes { - if !items.is_empty() { - section.push_str(&format!("### {}\n\n", category)); - for item in items { - section.push_str(&format!("- {}\n", item)); - } - section.push('\n'); - } - } - - section -} - -/// Format git config key -pub fn format_git_config_key(section: &str, subsection: Option<&str>, key: &str) -> String { - match subsection { - Some(sub) => format!("{}.{}.{}", section, sub, key), - None => format!("{}.{}", section, key), - } -} - #[cfg(test)] mod tests { use super::*; @@ -189,10 +116,4 @@ mod tests { assert!(msg.starts_with("feat!: change API response format")); } - - #[test] - fn test_truncate() { - assert_eq!(truncate("hello", 10), "hello"); - assert_eq!(truncate("hello world", 8), "hello..."); - } } diff --git a/src/utils/validators.rs b/src/utils/validators.rs index e3e3b5a..75e3dba 100644 --- a/src/utils/validators.rs +++ b/src/utils/validators.rs @@ -58,11 +58,6 @@ lazy_static! { r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" ).unwrap(); - /// Regex for SSH key validation (basic) - static ref SSH_KEY_REGEX: Regex = Regex::new( - r"^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+[A-Za-z0-9+/]+={0,2}\s+.*$" - ).unwrap(); - /// Regex for GPG key ID validation static ref GPG_KEY_ID_REGEX: Regex = Regex::new( r"^[A-F0-9]{16,40}$" @@ -81,7 +76,6 @@ pub fn validate_conventional_commit(message: &str) -> Result<()> { ); } - // Check description length (max 100 chars for first line) if first_line.len() > 100 { bail!("Commit subject too long (max 100 characters)"); } @@ -93,7 +87,6 @@ pub fn validate_conventional_commit(message: &str) -> Result<()> { pub fn validate_commitlint_commit(message: &str) -> Result<()> { let first_line = message.lines().next().unwrap_or(""); - // Commitlint is more lenient but still requires type prefix let parts: Vec<&str> = first_line.splitn(2, ':').collect(); if parts.len() != 2 { bail!("Invalid commit format. Expected: [optional scope]: "); @@ -102,7 +95,6 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> { let type_part = parts[0]; let subject = parts[1].trim(); - // Extract type (handle scope and breaking indicator) let commit_type = type_part .split('(') .next() @@ -117,7 +109,6 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> { ); } - // Validate subject if subject.is_empty() { bail!("Commit subject cannot be empty"); } @@ -130,12 +121,10 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> { bail!("Commit subject too long (max 100 characters)"); } - // Subject should not start with uppercase if subject.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { bail!("Commit subject should not start with uppercase letter"); } - // Subject should not end with period if subject.ends_with('.') { bail!("Commit subject should not end with a period"); } @@ -179,15 +168,6 @@ pub fn validate_email(email: &str) -> Result<()> { Ok(()) } -/// Validate SSH key format -pub fn validate_ssh_key(key: &str) -> Result<()> { - if !SSH_KEY_REGEX.is_match(key.trim()) { - bail!("Invalid SSH public key 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) {