style: 格式化代码并优化导入顺序

This commit is contained in:
2026-05-27 15:15:15 +08:00
parent b8182e7538
commit 90074e6e32
34 changed files with 2931 additions and 1648 deletions

View File

@@ -1,9 +1,9 @@
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
aead::{Aead, KeyInit},
};
use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rand::Rng;
use std::fs;
use std::path::Path;
@@ -18,63 +18,62 @@ pub fn encrypt(data: &[u8], password: &str) -> Result<String> {
rand::thread_rng().fill(&mut salt);
let mut nonce_bytes = [0u8; NONCE_LEN];
rand::thread_rng().fill(&mut nonce_bytes);
let key = derive_key(password, &salt)?;
let cipher = Aes256Gcm::new_from_slice(&key)
.context("Failed to create cipher")?;
let cipher = Aes256Gcm::new_from_slice(&key).context("Failed to create cipher")?;
let nonce = Nonce::from_slice(&nonce_bytes);
let encrypted = cipher
.encrypt(nonce, data)
.map_err(|e| anyhow::anyhow!("Encryption failed: {:?}", e))?;
// Combine salt + nonce + encrypted data
let mut result = Vec::with_capacity(SALT_LEN + NONCE_LEN + encrypted.len());
result.extend_from_slice(&salt);
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&encrypted);
Ok(BASE64.encode(&result))
}
/// Decrypt data with password
pub fn decrypt(encrypted_data: &str, password: &str) -> Result<Vec<u8>> {
let data = BASE64.decode(encrypted_data)
let data = BASE64
.decode(encrypted_data)
.context("Invalid base64 encoding")?;
if data.len() < SALT_LEN + NONCE_LEN {
anyhow::bail!("Invalid encrypted data format");
}
let salt = &data[..SALT_LEN];
let nonce_bytes = &data[SALT_LEN..SALT_LEN + NONCE_LEN];
let encrypted = &data[SALT_LEN + NONCE_LEN..];
let key = derive_key(password, salt)?;
let cipher = Aes256Gcm::new_from_slice(&key)
.context("Failed to create cipher")?;
let cipher = Aes256Gcm::new_from_slice(&key).context("Failed to create cipher")?;
let nonce = Nonce::from_slice(nonce_bytes);
let decrypted = cipher
.decrypt(nonce, encrypted)
.map_err(|e| anyhow::anyhow!("Decryption failed: {:?}", e))?;
Ok(decrypted)
}
/// Derive key from password using simple method
fn derive_key(password: &str, salt: &[u8]) -> Result<[u8; KEY_LEN]> {
use sha2::{Sha256, Digest};
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(password.as_bytes());
hasher.update(b"quicommit_key_derivation_v1");
let hash = hasher.finalize();
let mut key = [0u8; KEY_LEN];
key.copy_from_slice(&hash[..KEY_LEN]);
Ok(key)
}
@@ -97,7 +96,7 @@ pub fn decrypt_from_file(path: &Path, password: &str) -> Result<Vec<u8>> {
pub fn generate_token(length: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::thread_rng();
(0..length)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
@@ -122,10 +121,10 @@ mod tests {
fn test_encrypt_decrypt() {
let data = b"Hello, World!";
let password = "my_secret_password";
let encrypted = encrypt(data, password).unwrap();
let decrypted = decrypt(&encrypted, password).unwrap();
assert_eq!(data.to_vec(), decrypted);
}
@@ -133,7 +132,7 @@ mod tests {
fn test_wrong_password() {
let data = b"Hello, World!";
let encrypted = encrypt(data, "correct_password").unwrap();
assert!(decrypt(&encrypted, "wrong_password").is_err());
}
}

View File

@@ -9,15 +9,12 @@ pub fn edit_content(initial_content: &str) -> Result<String> {
/// Edit file in user's default editor
pub fn edit_file(path: &Path) -> Result<String> {
let content = fs::read_to_string(path)
.unwrap_or_default();
let edited = edit::edit(&content)
.context("Failed to open editor")?;
fs::write(path, &edited)
.with_context(|| format!("Failed to write file: {:?}", path))?;
let content = fs::read_to_string(path).unwrap_or_default();
let edited = edit::edit(&content).context("Failed to open editor")?;
fs::write(path, &edited).with_context(|| format!("Failed to write file: {:?}", path))?;
Ok(edited)
}
@@ -27,11 +24,10 @@ pub fn edit_temp(initial_content: &str, extension: &str) -> Result<String> {
.suffix(&format!(".{}", extension))
.tempfile()
.context("Failed to create temp file")?;
let path = temp_file.path();
fs::write(path, initial_content)
.context("Failed to write temp file")?;
fs::write(path, initial_content).context("Failed to write temp file")?;
edit_file(path)
}
@@ -65,7 +61,6 @@ pub fn get_editor() -> String {
/// Check if editor is available
pub fn check_editor() -> Result<()> {
let editor = get_editor();
which::which(&editor)
.with_context(|| format!("Editor '{}' not found in PATH", editor))?;
which::which(&editor).with_context(|| format!("Editor '{}' not found in PATH", editor))?;
Ok(())
}

View File

@@ -10,7 +10,7 @@ pub fn format_conventional_commit(
breaking: bool,
) -> String {
let mut message = String::new();
message.push_str(commit_type);
if let Some(s) = scope {
message.push_str(&format!("({})", s));
@@ -19,15 +19,15 @@ pub fn format_conventional_commit(
message.push('!');
}
message.push_str(&format!(": {}", description));
if let Some(b) = body {
message.push_str(&format!("\n\n{}", b));
}
if let Some(f) = footer {
message.push_str(&format!("\n\n{}", f));
}
message
}
@@ -41,27 +41,27 @@ pub fn format_commitlint_commit(
references: Option<&[&str]>,
) -> String {
let mut message = String::new();
message.push_str(commit_type);
if let Some(s) = scope {
message.push_str(&format!("({})", s));
}
message.push_str(&format!(": {}", subject));
if let Some(refs) = references {
for reference in refs {
message.push_str(&format!(" #{}", reference));
}
}
if let Some(b) = body {
message.push_str(&format!("\n\n{}", b));
}
if let Some(f) = footer {
message.push_str(&format!("\n\n{}", f));
}
message
}
@@ -73,7 +73,7 @@ pub fn wrap_text(text: &str, width: usize) -> String {
/// Clean commit message (remove comments, extra whitespace)
pub fn clean_message(message: &str) -> String {
let comment_regex = Regex::new(r"^#.*$").unwrap();
message
.lines()
.filter(|line| !comment_regex.is_match(line.trim()))
@@ -97,7 +97,7 @@ mod tests {
Some("Closes #123"),
false,
);
assert!(msg.contains("feat(auth): add login functionality"));
assert!(msg.contains("This adds OAuth2 login support."));
assert!(msg.contains("Closes #123"));
@@ -113,7 +113,7 @@ mod tests {
Some("BREAKING CHANGE: response format changed"),
true,
);
assert!(msg.starts_with("feat!: change API response format"));
}
}

View File

@@ -1,4 +1,4 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use std::env;
const SERVICE_NAME: &str = "quicommit";
@@ -78,7 +78,8 @@ impl KeyringManager {
let entry = keyring::Entry::new(SERVICE_NAME, provider)
.context("Failed to create keyring entry")?;
entry.set_password(api_key)
entry
.set_password(api_key)
.context("Failed to store API key")?;
Ok(())
@@ -86,9 +87,10 @@ impl KeyringManager {
pub fn get_api_key(&self, provider: &str) -> Result<Option<String>> {
if let Ok(key) = env::var(ENV_API_KEY)
&& !key.is_empty() {
return Ok(Some(key));
}
&& !key.is_empty()
{
return Ok(Some(key));
}
if !self.is_available() {
return Ok(None);
@@ -112,7 +114,8 @@ impl KeyringManager {
let entry = keyring::Entry::new(SERVICE_NAME, provider)
.context("Failed to create keyring entry")?;
entry.delete_credential()
entry
.delete_credential()
.context("Failed to delete API key")?;
Ok(())
@@ -126,7 +129,13 @@ impl KeyringManager {
format!("{}/{}", PAT_SERVICE_PREFIX, profile_name)
}
pub fn store_pat(&self, profile_name: &str, user_email: &str, service: &str, token: &str) -> Result<()> {
pub fn store_pat(
&self,
profile_name: &str,
user_email: &str,
service: &str,
token: &str,
) -> Result<()> {
if !self.is_available() {
bail!("Keyring is not available on this system");
}
@@ -137,15 +146,24 @@ impl KeyringManager {
let entry = keyring::Entry::new(&keyring_service, &keyring_user)
.context("Failed to create keyring entry for PAT")?;
entry.set_password(token)
entry
.set_password(token)
.context("Failed to store PAT in keyring")?;
eprintln!("[DEBUG] PAT stored in keyring: service={}, user={}", keyring_service, keyring_user);
eprintln!(
"[DEBUG] PAT stored in keyring: service={}, user={}",
keyring_service, keyring_user
);
Ok(())
}
pub fn get_pat(&self, profile_name: &str, user_email: &str, service: &str) -> Result<Option<String>> {
pub fn get_pat(
&self,
profile_name: &str,
user_email: &str,
service: &str,
) -> Result<Option<String>> {
if !self.is_available() {
return Ok(None);
}
@@ -158,11 +176,17 @@ impl KeyringManager {
match entry.get_password() {
Ok(token) => {
eprintln!("[DEBUG] PAT retrieved from keyring: service={}, user={}", keyring_service, keyring_user);
eprintln!(
"[DEBUG] PAT retrieved from keyring: service={}, user={}",
keyring_service, keyring_user
);
Ok(Some(token))
}
Err(keyring::Error::NoEntry) => {
eprintln!("[DEBUG] PAT not found in keyring: service={}, user={}", keyring_service, keyring_user);
eprintln!(
"[DEBUG] PAT not found in keyring: service={}, user={}",
keyring_service, keyring_user
);
Ok(None)
}
Err(e) => Err(e.into()),
@@ -180,22 +204,36 @@ impl KeyringManager {
let entry = keyring::Entry::new(&keyring_service, &keyring_user)
.context("Failed to create keyring entry for PAT")?;
entry.delete_credential()
entry
.delete_credential()
.context("Failed to delete PAT from keyring")?;
eprintln!("[DEBUG] PAT deleted from keyring: service={}, user={}", keyring_service, keyring_user);
eprintln!(
"[DEBUG] PAT deleted from keyring: service={}, user={}",
keyring_service, keyring_user
);
Ok(())
}
pub fn has_pat(&self, profile_name: &str, user_email: &str, service: &str) -> bool {
self.get_pat(profile_name, user_email, service).unwrap_or(None).is_some()
self.get_pat(profile_name, user_email, service)
.unwrap_or(None)
.is_some()
}
pub fn delete_all_pats_for_profile(&self, profile_name: &str, user_email: &str, services: &[String]) -> Result<()> {
pub fn delete_all_pats_for_profile(
&self,
profile_name: &str,
user_email: &str,
services: &[String],
) -> Result<()> {
for service in services {
if let Err(e) = self.delete_pat(profile_name, user_email, service) {
eprintln!("[DEBUG] Failed to delete PAT for service '{}': {}", service, e);
eprintln!(
"[DEBUG] Failed to delete PAT for service '{}': {}",
service, e
);
}
}
Ok(())
@@ -259,7 +297,14 @@ pub fn get_default_model(provider: &str) -> &'static str {
}
pub fn get_supported_providers() -> &'static [&'static str] {
&["ollama", "openai", "anthropic", "kimi", "deepseek", "openrouter"]
&[
"ollama",
"openai",
"anthropic",
"kimi",
"deepseek",
"openrouter",
]
}
pub fn provider_needs_api_key(provider: &str) -> bool {
@@ -273,10 +318,19 @@ mod tests {
#[test]
fn test_get_default_base_url() {
assert_eq!(get_default_base_url("openai"), "https://api.openai.com/v1");
assert_eq!(get_default_base_url("anthropic"), "https://api.anthropic.com/v1");
assert_eq!(
get_default_base_url("anthropic"),
"https://api.anthropic.com/v1"
);
assert_eq!(get_default_base_url("kimi"), "https://api.moonshot.cn/v1");
assert_eq!(get_default_base_url("deepseek"), "https://api.deepseek.com/v1");
assert_eq!(get_default_base_url("openrouter"), "https://openrouter.ai/api/v1");
assert_eq!(
get_default_base_url("deepseek"),
"https://api.deepseek.com/v1"
);
assert_eq!(
get_default_base_url("openrouter"),
"https://openrouter.ai/api/v1"
);
assert_eq!(get_default_base_url("ollama"), "http://localhost:11434");
}

View File

@@ -32,10 +32,10 @@ pub fn print_info(msg: &str) {
pub fn confirm(prompt: &str) -> Result<bool> {
print!("{} [y/N] ", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_lowercase().starts_with('y'))
}
@@ -43,17 +43,17 @@ pub fn confirm(prompt: &str) -> Result<bool> {
pub fn input(prompt: &str) -> Result<String> {
print!("{}: ", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
/// Get password input (hidden)
pub fn password_input(prompt: &str) -> Result<String> {
use dialoguer::Password;
Password::new()
.with_prompt(prompt)
.interact()

View File

@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use lazy_static::lazy_static;
use regex::Regex;
@@ -67,7 +67,7 @@ lazy_static! {
/// 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\
@@ -75,32 +75,32 @@ pub fn validate_conventional_commit(message: &str) -> Result<()> {
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: {}",
@@ -108,27 +108,32 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> {
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) {
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(())
}
@@ -137,25 +142,25 @@ 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(())
}
@@ -164,7 +169,7 @@ pub fn validate_email(email: &str) -> Result<()> {
if !EMAIL_REGEX.is_match(email) {
bail!("Invalid email address format");
}
Ok(())
}
@@ -173,7 +178,7 @@ 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(())
}
@@ -182,15 +187,18 @@ 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 == '_') {
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
bail!("Profile name can only contain letters, numbers, hyphens, and underscores");
}
Ok(())
}
@@ -201,7 +209,7 @@ pub fn is_valid_commit_type(commit_type: &str, use_commitlint: bool) -> bool {
} else {
CONVENTIONAL_TYPES
};
types.contains(&commit_type)
}