⬆️ chore(Cargo.toml):升级版本号至0.1.7
♻️ refactor(changelog.rs):移除prepend参数,改为自动前置到现有changelog ♻️ refactor(formatter.rs):移除未使用的日期和格式化函数 ♻️ refactor(validators.rs):移除未使用的SSH密钥验证功能
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "quicommit"
|
name = "quicommit"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
||||||
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ pub struct ChangelogCommand {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
generate: bool,
|
generate: bool,
|
||||||
|
|
||||||
/// Prepend to existing changelog
|
|
||||||
#[arg(short, long)]
|
|
||||||
prepend: bool,
|
|
||||||
|
|
||||||
/// Include commit hashes
|
/// Include commit hashes
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
include_hashes: bool,
|
include_hashes: bool,
|
||||||
@@ -162,13 +158,34 @@ impl ChangelogCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to file
|
// Write to file (always prepend to preserve history)
|
||||||
if self.prepend && output_path.exists() {
|
if output_path.exists() {
|
||||||
let existing = std::fs::read_to_string(&output_path)?;
|
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)?;
|
std::fs::write(&output_path, new_content)?;
|
||||||
} else {
|
} 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);
|
println!("{} {:?}", messages.changelog_written(), output_path);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use chrono::{DateTime, Local, Utc};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
/// Format commit message with conventional commit format
|
/// Format commit message with conventional commit format
|
||||||
@@ -12,7 +11,6 @@ pub fn format_conventional_commit(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
|
|
||||||
// Type and scope
|
|
||||||
message.push_str(commit_type);
|
message.push_str(commit_type);
|
||||||
if let Some(s) = scope {
|
if let Some(s) = scope {
|
||||||
message.push_str(&format!("({})", s));
|
message.push_str(&format!("({})", s));
|
||||||
@@ -22,12 +20,10 @@ pub fn format_conventional_commit(
|
|||||||
}
|
}
|
||||||
message.push_str(&format!(": {}", description));
|
message.push_str(&format!(": {}", description));
|
||||||
|
|
||||||
// Body
|
|
||||||
if let Some(b) = body {
|
if let Some(b) = body {
|
||||||
message.push_str(&format!("\n\n{}", b));
|
message.push_str(&format!("\n\n{}", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
|
||||||
if let Some(f) = footer {
|
if let Some(f) = footer {
|
||||||
message.push_str(&format!("\n\n{}", f));
|
message.push_str(&format!("\n\n{}", f));
|
||||||
}
|
}
|
||||||
@@ -46,26 +42,22 @@ pub fn format_commitlint_commit(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
|
|
||||||
// Header
|
|
||||||
message.push_str(commit_type);
|
message.push_str(commit_type);
|
||||||
if let Some(s) = scope {
|
if let Some(s) = scope {
|
||||||
message.push_str(&format!("({})", s));
|
message.push_str(&format!("({})", s));
|
||||||
}
|
}
|
||||||
message.push_str(&format!(": {}", subject));
|
message.push_str(&format!(": {}", subject));
|
||||||
|
|
||||||
// References
|
|
||||||
if let Some(refs) = references {
|
if let Some(refs) = references {
|
||||||
for reference in refs {
|
for reference in refs {
|
||||||
message.push_str(&format!(" #{}", reference));
|
message.push_str(&format!(" #{}", reference));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body
|
|
||||||
if let Some(b) = body {
|
if let Some(b) = body {
|
||||||
message.push_str(&format!("\n\n{}", b));
|
message.push_str(&format!("\n\n{}", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
|
||||||
if let Some(f) = footer {
|
if let Some(f) = footer {
|
||||||
message.push_str(&format!("\n\n{}", f));
|
message.push_str(&format!("\n\n{}", f));
|
||||||
}
|
}
|
||||||
@@ -73,38 +65,11 @@ pub fn format_commitlint_commit(
|
|||||||
message
|
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
|
/// Wrap text at specified width
|
||||||
pub fn wrap_text(text: &str, width: usize) -> String {
|
pub fn wrap_text(text: &str, width: usize) -> String {
|
||||||
textwrap::fill(text, width)
|
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)
|
/// Clean commit message (remove comments, extra whitespace)
|
||||||
pub fn clean_message(message: &str) -> String {
|
pub fn clean_message(message: &str) -> String {
|
||||||
let comment_regex = Regex::new(r"^#.*$").unwrap();
|
let comment_regex = Regex::new(r"^#.*$").unwrap();
|
||||||
@@ -118,44 +83,6 @@ pub fn clean_message(message: &str) -> String {
|
|||||||
.to_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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -189,10 +116,4 @@ mod tests {
|
|||||||
|
|
||||||
assert!(msg.starts_with("feat!: change API response format"));
|
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...");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ lazy_static! {
|
|||||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
).unwrap();
|
).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
|
/// Regex for GPG key ID validation
|
||||||
static ref GPG_KEY_ID_REGEX: Regex = Regex::new(
|
static ref GPG_KEY_ID_REGEX: Regex = Regex::new(
|
||||||
r"^[A-F0-9]{16,40}$"
|
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 {
|
if first_line.len() > 100 {
|
||||||
bail!("Commit subject too long (max 100 characters)");
|
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<()> {
|
pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
||||||
let first_line = message.lines().next().unwrap_or("");
|
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();
|
let parts: Vec<&str> = first_line.splitn(2, ':').collect();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
bail!("Invalid commit format. Expected: <type>[optional scope]: <subject>");
|
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 type_part = parts[0];
|
||||||
let subject = parts[1].trim();
|
let subject = parts[1].trim();
|
||||||
|
|
||||||
// Extract type (handle scope and breaking indicator)
|
|
||||||
let commit_type = type_part
|
let commit_type = type_part
|
||||||
.split('(')
|
.split('(')
|
||||||
.next()
|
.next()
|
||||||
@@ -117,7 +109,6 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate subject
|
|
||||||
if subject.is_empty() {
|
if subject.is_empty() {
|
||||||
bail!("Commit subject cannot be 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)");
|
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) {
|
if subject.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
|
||||||
bail!("Commit subject should not start with uppercase letter");
|
bail!("Commit subject should not start with uppercase letter");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject should not end with period
|
|
||||||
if subject.ends_with('.') {
|
if subject.ends_with('.') {
|
||||||
bail!("Commit subject should not end with a period");
|
bail!("Commit subject should not end with a period");
|
||||||
}
|
}
|
||||||
@@ -179,15 +168,6 @@ pub fn validate_email(email: &str) -> Result<()> {
|
|||||||
Ok(())
|
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
|
/// Validate GPG key ID
|
||||||
pub fn validate_gpg_key_id(key_id: &str) -> Result<()> {
|
pub fn validate_gpg_key_id(key_id: &str) -> Result<()> {
|
||||||
if !GPG_KEY_ID_REGEX.is_match(key_id) {
|
if !GPG_KEY_ID_REGEX.is_match(key_id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user