⬆️ chore(Cargo.toml):升级版本号至0.1.7

♻️ refactor(changelog.rs):移除prepend参数,改为自动前置到现有changelog
♻️ refactor(formatter.rs):移除未使用的日期和格式化函数
♻️ refactor(validators.rs):移除未使用的SSH密钥验证功能
This commit is contained in:
2026-02-14 15:00:59 +08:00
parent e822ba1f54
commit a514cdc69f
4 changed files with 26 additions and 108 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "quicommit"
version = "0.1.5"
version = "0.1.7"
edition = "2024"
authors = ["Sidney Zhang <zly@lyzhang.me>"]
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"

View File

@@ -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);

View File

@@ -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<Local>) -> String {
date.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// Format date for changelog
pub fn format_changelog_date(date: &DateTime<Utc>) -> 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::<Vec<_>>()
.join("\n")
}
/// Format changelog section
pub fn format_changelog_section(
version: &str,
date: &str,
changes: &[(String, Vec<String>)],
) -> 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...");
}
}

View File

@@ -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: <type>[optional scope]: <subject>");
@@ -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) {