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,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use chrono::Utc;
use clap::Parser;
use colored::Colorize;
@@ -8,7 +8,7 @@ use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::generator::ContentGenerator;
use crate::git::find_repo;
use crate::git::{changelog::*, CommitInfo};
use crate::git::{CommitInfo, changelog::*};
use crate::i18n::{Messages, translate_changelog_category};
/// Generate changelog
@@ -78,16 +78,20 @@ impl ChangelogCommand {
// Initialize changelog if requested
if self.init {
let path = self.output.clone()
let path = self
.output
.clone()
.unwrap_or_else(|| PathBuf::from(&config.changelog.path));
init_changelog(&path)?;
println!("{}", messages.initialized_changelog(&format!("{:?}", path)));
return Ok(());
}
// Determine output path
let output_path = self.output.clone()
let output_path = self
.output
.clone()
.unwrap_or_else(|| PathBuf::from(&config.changelog.path));
// Determine format
@@ -96,7 +100,10 @@ impl ChangelogCommand {
Some("keep") | Some("keep-a-changelog") => ChangelogFormat::KeepAChangelog,
Some("custom") => ChangelogFormat::Custom,
None => ChangelogFormat::KeepAChangelog,
Some(f) => bail!("Unknown format: {}. Use: keep-a-changelog, github-releases", f),
Some(f) => bail!(
"Unknown format: {}. Use: keep-a-changelog, github-releases",
f
),
};
// Get version
@@ -114,11 +121,11 @@ impl ChangelogCommand {
// Get commits
println!("{}", messages.fetching_commits());
let commits = generate_from_history(&repo, self.from.as_deref(), Some(&self.to))?;
if commits.is_empty() {
bail!("{}", messages.no_commits_found());
}
println!("{}", messages.found_commits(commits.len()));
// Generate changelog
@@ -170,7 +177,7 @@ impl ChangelogCommand {
} else if existing.starts_with("# Changelog") {
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;
@@ -180,10 +187,10 @@ impl ChangelogCommand {
break;
}
}
let header = lines[..header_end].join("\n");
let rest = lines[header_end..].join("\n");
format!("{}\n{}\n{}", header, changelog, rest)
} else {
format!("{}{}", CHANGELOG_HEADER, changelog)
@@ -211,7 +218,9 @@ impl ChangelogCommand {
println!("{}", messages.ai_generating_changelog());
let generator = ContentGenerator::new_with_think(&manager, self.think).await?;
generator.generate_changelog_entry(version, commits, language).await
generator
.generate_changelog_entry(version, commits, language)
.await
}
fn generate_with_template(
@@ -222,14 +231,14 @@ impl ChangelogCommand {
language: Language,
) -> Result<String> {
let manager = ConfigManager::new()?;
let generator = ChangelogGenerator::new()
.format(format)
.include_hashes(self.include_hashes)
.include_authors(self.include_authors);
let changelog = generator.generate(version, Utc::now(), commits)?;
// Translate changelog categories if configured
if !manager.keep_changelog_types_english() {
Ok(self.translate_changelog_categories(&changelog, language))
@@ -237,15 +246,15 @@ impl ChangelogCommand {
Ok(changelog)
}
}
fn translate_changelog_categories(&self, changelog: &str, language: Language) -> String {
changelog
.lines()
.map(|line| {
if line.starts_with("## ") || line.starts_with("### ") {
let category = line.trim_start_matches("## ").trim_start_matches("### ");
let translated_category = translate_changelog_category(category, language, false);
let translated_category =
translate_changelog_category(category, language, false);
if line.starts_with("## ") {
format!("## {}", translated_category)
} else {

View File

@@ -1,14 +1,14 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use clap::Parser;
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::config::CommitFormat;
use crate::config::{Language, manager::ConfigManager};
use crate::generator::ContentGenerator;
use crate::git::{find_repo, GitRepo};
use crate::git::commit::{CommitBuilder, create_date_commit_message};
use crate::git::{GitRepo, find_repo};
use crate::i18n::Messages;
use crate::utils::validators::get_commit_types;
@@ -92,7 +92,7 @@ impl CommitCommand {
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
// Find git repository
let repo = find_repo(std::env::current_dir()?.as_path())?;
// Load configuration
let manager = if let Some(ref path) = config_path {
ConfigManager::with_path(path)?
@@ -102,7 +102,7 @@ impl CommitCommand {
let config = manager.config();
let language = manager.get_language().unwrap_or(Language::English);
let messages = Messages::new(language);
// Check for changes
let status = repo.status_summary()?;
if status.clean && !self.amend {
@@ -123,7 +123,7 @@ impl CommitCommand {
println!("{}", messages.auto_stage_changes().yellow());
repo.stage_all()?;
println!("{}", messages.staged_all().green());
// Re-check status after staging to ensure changes are detected
let new_status = repo.status_summary()?;
if new_status.staged == 0 {
@@ -183,14 +183,22 @@ impl CommitCommand {
let result = if self.amend {
if self.dry_run {
println!("\n{} {}", messages.dry_run(), "- commit not amended.".yellow());
println!(
"\n{} {}",
messages.dry_run(),
"- commit not amended.".yellow()
);
return Ok(());
}
self.amend_commit(&repo, &commit_message)?;
None
} else {
if self.dry_run {
println!("\n{} {}", messages.dry_run(), "- commit not created.".yellow());
println!(
"\n{} {}",
messages.dry_run(),
"- commit not created.".yellow()
);
return Ok(());
}
CommitBuilder::new()
@@ -200,7 +208,11 @@ impl CommitCommand {
};
if let Some(commit_oid) = result {
println!("{} {}", messages.commit_created().green().bold(), commit_oid.to_string()[..8].to_string().cyan());
println!(
"{} {}",
messages.commit_created().green().bold(),
commit_oid.to_string()[..8].to_string().cyan()
);
} else {
println!("{} successfully", messages.commit_amended().green().bold());
}
@@ -232,8 +244,9 @@ impl CommitCommand {
}
fn create_manual_commit(&self, format: CommitFormat) -> Result<String> {
let description = self.message.clone()
.ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m <message>"))?;
let description = self.message.clone().ok_or_else(|| {
anyhow::anyhow!("Description required for manual commit. Use -m <message>")
})?;
// Try to extract commit type from message if not provided
let commit_type = if let Some(ref ct) = self.commit_type {
@@ -259,10 +272,16 @@ impl CommitCommand {
builder.build_message()
}
async fn generate_commit(&self, repo: &GitRepo, format: CommitFormat, messages: &Messages) -> Result<String> {
async fn generate_commit(
&self,
repo: &GitRepo,
format: CommitFormat,
messages: &Messages,
) -> Result<String> {
let manager = ConfigManager::new()?;
let generator = ContentGenerator::new_with_think(&manager, self.think).await
let generator = ContentGenerator::new_with_think(&manager, self.think)
.await
.context("Failed to initialize LLM. Use --manual for manual commit.")?;
println!("{}", messages.ai_analyzing());
@@ -270,15 +289,23 @@ impl CommitCommand {
let language = manager.get_language().unwrap_or(Language::English);
let generated = if self.yes {
generator.generate_commit_from_repo(repo, format, language).await?
generator
.generate_commit_from_repo(repo, format, language)
.await?
} else {
generator.generate_commit_interactive(repo, format, language).await?
generator
.generate_commit_interactive(repo, format, language)
.await?
};
Ok(generated.to_conventional())
}
async fn create_interactive_commit(&self, format: CommitFormat, messages: &Messages) -> Result<String> {
async fn create_interactive_commit(
&self,
format: CommitFormat,
messages: &Messages,
) -> Result<String> {
let types = get_commit_types(format == CommitFormat::Commitlint);
// Select type
@@ -356,20 +383,21 @@ impl CommitCommand {
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let error_msg = if stderr.is_empty() {
if stdout.is_empty() {
"GPG signing failed. Please check:\n\
1. GPG signing key is configured (git config --get user.signingkey)\n\
2. GPG agent is running\n\
3. You can sign commits manually (try: git commit --amend -S)".to_string()
3. You can sign commits manually (try: git commit --amend -S)"
.to_string()
} else {
stdout.to_string()
}
} else {
stderr.to_string()
};
bail!("Failed to amend commit: {}", error_msg);
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@ use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use std::path::PathBuf;
use crate::config::{GitProfile, Language};
use crate::config::manager::ConfigManager;
use crate::config::profile::{GpgConfig, SshConfig};
use crate::config::{GitProfile, Language};
use crate::i18n::Messages;
use crate::utils::keyring::{get_supported_providers, get_default_model, provider_needs_api_key};
use crate::utils::keyring::{get_default_model, get_supported_providers, provider_needs_api_key};
use crate::utils::validators::validate_email;
/// Initialize quicommit configuration
@@ -28,23 +28,25 @@ impl InitCommand {
let messages = Messages::new(Language::English);
println!("{}", messages.initializing().bold().cyan());
let config_path = config_path.unwrap_or_else(|| {
crate::config::AppConfig::default_path().unwrap()
});
let config_path =
config_path.unwrap_or_else(|| crate::config::AppConfig::default_path().unwrap());
if config_path.exists() && !self.reset {
if !self.yes {
let overwrite = Confirm::new()
.with_prompt("Configuration already exists. Overwrite?")
.default(false)
.interact()?;
if !overwrite {
println!("{}", "Initialization cancelled.".yellow());
return Ok(());
}
} else {
println!("{}", "Configuration already exists. Use --reset to overwrite.".yellow());
println!(
"{}",
"Configuration already exists. Use --reset to overwrite.".yellow()
);
return Ok(());
}
}
@@ -63,10 +65,10 @@ impl InitCommand {
}
manager.save()?;
let language = manager.get_language().unwrap_or(Language::English);
let messages = Messages::new(language);
println!("{}", messages.init_success().bold().green());
println!("\n{}: {}", messages.config_file(), config_path.display());
println!("\n{}:", messages.next_steps());
@@ -79,15 +81,15 @@ impl InitCommand {
async fn quick_setup(&self, manager: &mut ConfigManager) -> Result<()> {
let git_config = git2::Config::open_default()?;
let user_name = git_config.get_string("user.name").unwrap_or_else(|_| "User".to_string());
let user_email = git_config.get_string("user.email").unwrap_or_else(|_| "user@example.com".to_string());
let profile = GitProfile::new(
"default".to_string(),
user_name,
user_email,
);
let user_name = git_config
.get_string("user.name")
.unwrap_or_else(|_| "User".to_string());
let user_email = git_config
.get_string("user.email")
.unwrap_or_else(|_| "user@example.com".to_string());
let profile = GitProfile::new("default".to_string(), user_name, user_email);
manager.add_profile("default".to_string(), profile)?;
manager.set_default_profile(Some("default".to_string()))?;
@@ -102,18 +104,20 @@ impl InitCommand {
println!("\n{}", messages.setup_profile().bold());
println!("\n{}", messages.select_output_language().bold());
let languages = [Language::English,
let languages = [
Language::English,
Language::Chinese,
Language::Japanese,
Language::Korean,
Language::Spanish,
Language::French,
Language::German];
let language_names: Vec<String> = languages.iter().map(|l| l.display_name().to_string()).collect();
let language_idx = Select::new()
.items(&language_names)
.default(0)
.interact()?;
Language::German,
];
let language_names: Vec<String> = languages
.iter()
.map(|l| l.display_name().to_string())
.collect();
let language_idx = Select::new().items(&language_names).default(0).interact()?;
let selected_language = languages[language_idx];
manager.set_output_language(selected_language.to_code().to_string());
@@ -126,12 +130,14 @@ impl InitCommand {
.interact_text()?;
let git_config = git2::Config::open_default().ok();
let default_name = git_config.as_ref()
let default_name = git_config
.as_ref()
.and_then(|c| c.get_string("user.name").ok())
.unwrap_or_default();
let default_email = git_config.as_ref()
let default_email = git_config
.as_ref()
.and_then(|c| c.get_string("user.email").ok())
.unwrap_or_default();
@@ -143,9 +149,7 @@ impl InitCommand {
let user_email: String = Input::new()
.with_prompt(messages.git_user_email())
.default(default_email)
.validate_with(|input: &String| {
validate_email(input).map_err(|e| e.to_string())
})
.validate_with(|input: &String| validate_email(input).map_err(|e| e.to_string()))
.interact_text()?;
let description: String = Input::new()
@@ -159,9 +163,11 @@ impl InitCommand {
.interact()?;
let organization = if is_work {
Some(Input::new()
.with_prompt(messages.organization_name())
.interact_text()?)
Some(
Input::new()
.with_prompt(messages.organization_name())
.interact_text()?,
)
} else {
None
};
@@ -188,11 +194,7 @@ impl InitCommand {
None
};
let mut profile = GitProfile::new(
profile_name.clone(),
user_name,
user_email,
);
let mut profile = GitProfile::new(profile_name.clone(), user_name, user_email);
if !description.is_empty() {
profile.description = Some(description);
@@ -207,16 +209,16 @@ impl InitCommand {
manager.set_default_profile(Some(profile_name))?;
println!("\n{}", messages.select_llm_provider().bold());
let provider_display_names = vec![
"Ollama (local)",
"OpenAI",
"Anthropic Claude",
"Kimi (Moonshot AI)",
"DeepSeek",
"OpenRouter"
"OpenRouter",
];
let provider_idx = Select::new()
.items(&provider_display_names)
.default(0)
@@ -227,19 +229,28 @@ impl InitCommand {
let keyring = manager.keyring();
let keyring_available = keyring.is_available();
if !keyring_available {
println!("\n{}", "⚠ Keyring is not available on this system.".yellow());
println!(
"\n{}",
"⚠ Keyring is not available on this system.".yellow()
);
println!("{}", keyring.get_status_message().yellow());
}
let api_key = if provider_needs_api_key(&provider) {
let env_key = std::env::var("QUICOMMIT_API_KEY")
.or_else(|_| std::env::var(format!("QUICOMMIT_{}_API_KEY", provider.to_uppercase())))
.or_else(|_| {
std::env::var(format!("QUICOMMIT_{}_API_KEY", provider.to_uppercase()))
})
.ok();
if let Some(_key) = env_key {
println!("\n{} {}", "".green(), "Found API key in environment variable.".green());
println!(
"\n{} {}",
"".green(),
"Found API key in environment variable.".green()
);
None
} else if keyring_available {
let prompt = match provider.as_str() {
@@ -250,13 +261,14 @@ impl InitCommand {
"openrouter" => messages.openrouter_api_key(),
_ => "API Key",
};
let key: String = Input::new()
.with_prompt(prompt)
.interact_text()?;
let key: String = Input::new().with_prompt(prompt).interact_text()?;
Some(key)
} else {
println!("\n{}", "Please set the QUICOMMIT_API_KEY environment variable.".yellow());
println!(
"\n{}",
"Please set the QUICOMMIT_API_KEY environment variable.".yellow()
);
None
}
} else {
@@ -280,11 +292,9 @@ impl InitCommand {
.with_prompt("Use custom API base URL?")
.default(false)
.interact()?;
if use_custom_url {
let url: String = Input::new()
.with_prompt("Base URL")
.interact_text()?;
let url: String = Input::new().with_prompt("Base URL").interact_text()?;
Some(url)
} else {
None
@@ -296,10 +306,15 @@ impl InitCommand {
manager.set_llm_base_url(base_url);
if let Some(key) = api_key
&& provider_needs_api_key(&provider) {
manager.set_api_key(&key)?;
println!("\n{} {}", "".green(), "API key stored securely in system keyring.".green());
}
&& provider_needs_api_key(&provider)
{
manager.set_api_key(&key)?;
println!(
"\n{} {}",
"".green(),
"API key stored securely in system keyring.".green()
);
}
Ok(())
}

View File

@@ -1,12 +1,12 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use std::path::PathBuf;
use crate::config::manager::ConfigManager;
use crate::config::{GitProfile, TokenConfig, TokenType};
use crate::config::profile::{GpgConfig, SshConfig};
use crate::config::{GitProfile, TokenConfig, TokenType};
use crate::git::find_repo;
use crate::utils::validators::validate_profile_name;
@@ -21,74 +21,74 @@ pub struct ProfileCommand {
enum ProfileSubcommand {
/// Add a new profile
Add,
/// Remove a profile
Remove {
/// Profile name
name: String,
},
/// List all profiles
List,
/// Show profile details
Show {
/// Profile name
name: Option<String>,
},
/// Edit a profile
Edit {
/// Profile name
name: String,
},
/// Set default profile
SetDefault {
/// Profile name
name: String,
},
/// Set profile for current repository
SetRepo {
/// Profile name
name: String,
},
/// Apply profile to current repository
Apply {
/// Profile name (uses default if not specified)
name: Option<String>,
/// Apply globally instead of to current repo
#[arg(short, long)]
global: bool,
},
/// Switch between profiles interactively
Switch,
/// Copy/duplicate a profile
Copy {
/// Source profile name
from: String,
/// New profile name
to: String,
},
/// Manage tokens for a profile
Token {
#[command(subcommand)]
token_command: TokenSubcommand,
},
/// Check profile configuration against git
Check {
/// Profile name
name: Option<String>,
},
/// Show usage statistics
Stats {
/// Profile name
@@ -102,20 +102,20 @@ enum TokenSubcommand {
Add {
/// Profile name
profile: String,
/// Service name (e.g., github, gitlab)
service: String,
},
/// Remove a token from a profile
Remove {
/// Profile name
profile: String,
/// Service name
service: String,
},
/// List tokens in a profile
List {
/// Profile name
@@ -127,18 +127,35 @@ impl ProfileCommand {
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
match &self.command {
Some(ProfileSubcommand::Add) => self.add_profile(&config_path).await,
Some(ProfileSubcommand::Remove { name }) => self.remove_profile(name, &config_path).await,
Some(ProfileSubcommand::Remove { name }) => {
self.remove_profile(name, &config_path).await
}
Some(ProfileSubcommand::List) => self.list_profiles(&config_path).await,
Some(ProfileSubcommand::Show { name }) => self.show_profile(name.as_deref(), &config_path).await,
Some(ProfileSubcommand::Show { name }) => {
self.show_profile(name.as_deref(), &config_path).await
}
Some(ProfileSubcommand::Edit { name }) => self.edit_profile(name, &config_path).await,
Some(ProfileSubcommand::SetDefault { name }) => self.set_default(name, &config_path).await,
Some(ProfileSubcommand::SetDefault { name }) => {
self.set_default(name, &config_path).await
}
Some(ProfileSubcommand::SetRepo { name }) => self.set_repo(name, &config_path).await,
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global, &config_path).await,
Some(ProfileSubcommand::Apply { name, global }) => {
self.apply_profile(name.as_deref(), *global, &config_path)
.await
}
Some(ProfileSubcommand::Switch) => self.switch_profile(&config_path).await,
Some(ProfileSubcommand::Copy { from, to }) => self.copy_profile(from, to, &config_path).await,
Some(ProfileSubcommand::Token { token_command }) => self.handle_token_command(token_command, &config_path).await,
Some(ProfileSubcommand::Check { name }) => self.check_profile(name.as_deref(), &config_path).await,
Some(ProfileSubcommand::Stats { name }) => self.show_stats(name.as_deref(), &config_path).await,
Some(ProfileSubcommand::Copy { from, to }) => {
self.copy_profile(from, to, &config_path).await
}
Some(ProfileSubcommand::Token { token_command }) => {
self.handle_token_command(token_command, &config_path).await
}
Some(ProfileSubcommand::Check { name }) => {
self.check_profile(name.as_deref(), &config_path).await
}
Some(ProfileSubcommand::Stats { name }) => {
self.show_stats(name.as_deref(), &config_path).await
}
None => self.list_profiles(&config_path).await,
}
}
@@ -158,18 +175,14 @@ impl ProfileCommand {
let name: String = Input::new()
.with_prompt("Profile name")
.validate_with(|input: &String| {
validate_profile_name(input).map_err(|e| e.to_string())
})
.validate_with(|input: &String| validate_profile_name(input).map_err(|e| e.to_string()))
.interact_text()?;
if manager.has_profile(&name) {
bail!("Profile '{}' already exists", name);
}
let user_name: String = Input::new()
.with_prompt("Git user name")
.interact_text()?;
let user_name: String = Input::new().with_prompt("Git user name").interact_text()?;
let user_email: String = Input::new()
.with_prompt("Git user email")
@@ -189,15 +202,13 @@ impl ProfileCommand {
.interact()?;
let organization = if is_work {
Some(Input::new()
.with_prompt("Organization")
.interact_text()?)
Some(Input::new().with_prompt("Organization").interact_text()?)
} else {
None
};
let mut profile = GitProfile::new(name.clone(), user_name, user_email);
if !description.is_empty() {
profile.description = Some(description);
}
@@ -234,7 +245,11 @@ impl ProfileCommand {
manager.add_profile(name.clone(), profile)?;
manager.save()?;
println!("{} Profile '{}' added successfully", "".green(), name.cyan());
println!(
"{} Profile '{}' added successfully",
"".green(),
name.cyan()
);
if manager.default_profile().is_none() {
let set_default = Confirm::new()
@@ -260,7 +275,10 @@ impl ProfileCommand {
}
let confirm = Confirm::new()
.with_prompt(format!("Are you sure you want to remove profile '{}'?", name))
.with_prompt(format!(
"Are you sure you want to remove profile '{}'?",
name
))
.default(false)
.interact()?;
@@ -270,11 +288,15 @@ impl ProfileCommand {
}
manager.delete_all_pats_for_profile(name)?;
manager.remove_profile(name)?;
manager.save()?;
println!("{} Profile '{}' removed (including all stored tokens)", "".green(), name);
println!(
"{} Profile '{}' removed (including all stored tokens)",
"".green(),
name
);
Ok(())
}
@@ -283,7 +305,7 @@ impl ProfileCommand {
let manager = self.get_manager(config_path)?;
let profiles = manager.list_profiles();
if profiles.is_empty() {
println!("{}", "No profiles configured.".yellow());
println!("Run {} to create one.", "quicommit profile add".cyan());
@@ -298,17 +320,25 @@ impl ProfileCommand {
for name in profiles {
let profile = manager.get_profile(name).unwrap();
let is_default = default.map(|d| d == name).unwrap_or(false);
let marker = if is_default { "".green() } else { "".dimmed() };
let work_marker = if profile.is_work { " [work]".yellow() } else { "".normal() };
let marker = if is_default {
"".green()
} else {
"".dimmed()
};
let work_marker = if profile.is_work {
" [work]".yellow()
} else {
"".normal()
};
println!("{} {}{}", marker, name.cyan().bold(), work_marker);
println!(" {} <{}>", profile.user_name, profile.user_email);
if let Some(ref desc) = profile.description {
println!(" {}", desc.dimmed());
}
if profile.has_ssh() {
println!(" {} SSH configured", "🔑".to_string().dimmed());
}
@@ -316,13 +346,21 @@ impl ProfileCommand {
println!(" {} GPG configured", "🔒".to_string().dimmed());
}
if profile.has_tokens() {
println!(" {} {} token(s)", "🔐".to_string().dimmed(), profile.tokens.len());
println!(
" {} {} token(s)",
"🔐".to_string().dimmed(),
profile.tokens.len()
);
}
if let Some(ref usage) = profile.usage.last_used {
println!(" {} Last used: {}", "📊".to_string().dimmed(), usage.dimmed());
println!(
" {} Last used: {}",
"📊".to_string().dimmed(),
usage.dimmed()
);
}
println!();
}
@@ -333,16 +371,17 @@ impl ProfileCommand {
let manager = self.get_manager(config_path)?;
match find_repo(std::env::current_dir()?.as_path()) {
Ok(repo) => {
self.show_repo_status(&repo, &manager, name).await
}
Err(_) => {
self.show_global_status(&manager, name).await
}
Ok(repo) => self.show_repo_status(&repo, &manager, name).await,
Err(_) => self.show_global_status(&manager, name).await,
}
}
async fn show_repo_status(&self, repo: &crate::git::GitRepo, manager: &ConfigManager, name: Option<&str>) -> Result<()> {
async fn show_repo_status(
&self,
repo: &crate::git::GitRepo,
manager: &ConfigManager,
name: Option<&str>,
) -> Result<()> {
use crate::git::MergedUserConfig;
let merged_config = MergedUserConfig::from_repo(repo.inner())?;
@@ -352,7 +391,10 @@ impl ProfileCommand {
println!("{}", "".repeat(60));
println!("Repository: {}", repo_path.cyan());
println!("\n{}", "Git User Configuration (merged local/global):".bold());
println!(
"\n{}",
"Git User Configuration (merged local/global):".bold()
);
println!("{}", "".repeat(60));
self.print_config_entry("User name", &merged_config.name);
@@ -375,21 +417,43 @@ impl ProfileCommand {
match (&matching_profile, repo_profile_name) {
(Some(profile), Some(mapped_name)) => {
if profile.name == *mapped_name {
println!("{} Profile '{}' is mapped to this repository", "".green(), profile.name.cyan());
println!(
"{} Profile '{}' is mapped to this repository",
"".green(),
profile.name.cyan()
);
println!(" This repository's git config matches the saved profile.");
} else {
println!("{} Profile '{}' matches current config", "".green(), profile.name.cyan());
println!(" But repository is mapped to different profile: {}", mapped_name.yellow());
println!(
"{} Profile '{}' matches current config",
"".green(),
profile.name.cyan()
);
println!(
" But repository is mapped to different profile: {}",
mapped_name.yellow()
);
}
}
(Some(profile), None) => {
println!("{} Profile '{}' matches current config", "".green(), profile.name.cyan());
println!(" {} This repository is not mapped to any profile.", "".yellow());
println!(
"{} Profile '{}' matches current config",
"".green(),
profile.name.cyan()
);
println!(
" {} This repository is not mapped to any profile.",
"".yellow()
);
}
(None, Some(mapped_name)) => {
println!("{} Repository is mapped to profile '{}'", "".yellow(), mapped_name.cyan());
println!(
"{} Repository is mapped to profile '{}'",
"".yellow(),
mapped_name.cyan()
);
println!(" But current git config does not match this profile!");
if let Some(mapped_profile) = manager.get_profile(mapped_name) {
println!("\n Mapped profile config:");
println!(" user.name: {}", mapped_profile.user_name);
@@ -399,7 +463,7 @@ impl ProfileCommand {
(None, None) => {
println!("{} No matching profile found in QuiCommit", "".red());
println!(" Current git identity is not saved as a QuiCommit profile.");
let partial_matches = manager.find_partial_matches(&user_name, &user_email);
if !partial_matches.is_empty() {
println!("\n {} Similar profiles exist:", "".yellow());
@@ -417,14 +481,18 @@ impl ProfileCommand {
}
if merged_config.is_complete() {
println!("\n {} Would you like to save this identity as a new profile?", "💡".yellow());
println!(
"\n {} Would you like to save this identity as a new profile?",
"💡".yellow()
);
let save = Confirm::new()
.with_prompt("Save current git identity as new profile?")
.default(true)
.interact()?;
if save {
self.save_current_identity_as_profile(&merged_config, manager).await?;
self.save_current_identity_as_profile(&merged_config, manager)
.await?;
}
}
}
@@ -432,7 +500,10 @@ impl ProfileCommand {
if let Some(profile_name) = name {
if let Some(profile) = manager.get_profile(profile_name) {
println!("\n{}", format!("Requested Profile: {}", profile_name).bold());
println!(
"\n{}",
format!("Requested Profile: {}", profile_name).bold()
);
println!("{}", "".repeat(60));
self.print_profile_details(profile);
} else {
@@ -486,8 +557,16 @@ impl ProfileCommand {
Some(value) => {
println!("{} {}: {}", source_indicator, label, value);
if entry.local_value.is_some() && entry.global_value.is_some() {
println!(" {} local: {}", "".dimmed(), entry.local_value.as_ref().unwrap());
println!(" {} global: {}", "".dimmed(), entry.global_value.as_ref().unwrap());
println!(
" {} local: {}",
"".dimmed(),
entry.local_value.as_ref().unwrap()
);
println!(
" {} global: {}",
"".dimmed(),
entry.global_value.as_ref().unwrap()
);
}
}
None => {
@@ -499,13 +578,20 @@ impl ProfileCommand {
fn print_profile_details(&self, profile: &GitProfile) {
println!("User name: {}", profile.user_name);
println!("User email: {}", profile.user_email);
if let Some(ref desc) = profile.description {
println!("Description: {}", desc);
}
println!("Work profile: {}", if profile.is_work { "yes".yellow() } else { "no".normal() });
println!(
"Work profile: {}",
if profile.is_work {
"yes".yellow()
} else {
"no".normal()
}
);
if let Some(ref org) = profile.organization {
println!("Organization: {}", org);
}
@@ -543,7 +629,11 @@ impl ProfileCommand {
}
}
async fn save_current_identity_as_profile(&self, merged_config: &crate::git::MergedUserConfig, manager: &ConfigManager) -> Result<()> {
async fn save_current_identity_as_profile(
&self,
merged_config: &crate::git::MergedUserConfig,
manager: &ConfigManager,
) -> Result<()> {
let config_path = manager.path().to_path_buf();
let mut manager = ConfigManager::with_path(&config_path)?;
@@ -557,14 +647,15 @@ impl ProfileCommand {
let profile_name: String = Input::new()
.with_prompt("Profile name")
.default(default_name)
.validate_with(|input: &String| {
validate_profile_name(input).map_err(|e| e.to_string())
})
.validate_with(|input: &String| validate_profile_name(input).map_err(|e| e.to_string()))
.interact_text()?;
if manager.has_profile(&profile_name) {
let overwrite = Confirm::new()
.with_prompt(format!("Profile '{}' already exists. Overwrite?", profile_name))
.with_prompt(format!(
"Profile '{}' already exists. Overwrite?",
profile_name
))
.default(false)
.interact()?;
if !overwrite {
@@ -585,9 +676,7 @@ impl ProfileCommand {
.interact()?;
let organization = if is_work {
Some(Input::new()
.with_prompt("Organization")
.interact_text()?)
Some(Input::new().with_prompt("Organization").interact_text()?)
} else {
None
};
@@ -601,12 +690,12 @@ impl ProfileCommand {
if let Some(ref key) = merged_config.signing_key.value {
profile.signing_key = Some(key.clone());
let setup_gpg = Confirm::new()
.with_prompt("Configure GPG signing details?")
.default(true)
.interact()?;
if setup_gpg {
profile.gpg = Some(self.setup_gpg_interactive().await?);
}
@@ -617,7 +706,7 @@ impl ProfileCommand {
.with_prompt("Configure SSH key details?")
.default(false)
.interact()?;
if setup_ssh {
profile.ssh = Some(self.setup_ssh_interactive().await?);
}
@@ -635,7 +724,11 @@ impl ProfileCommand {
manager.add_profile(profile_name.clone(), profile)?;
manager.save()?;
println!("{} Profile '{}' saved successfully", "".green(), profile_name.cyan());
println!(
"{} Profile '{}' saved successfully",
"".green(),
profile_name.cyan()
);
let set_default = Confirm::new()
.with_prompt("Set as default profile?")
@@ -645,7 +738,11 @@ impl ProfileCommand {
if set_default {
manager.set_default_profile(Some(profile_name.clone()))?;
manager.save()?;
println!("{} Set '{}' as default profile", "".green(), profile_name.cyan());
println!(
"{} Set '{}' as default profile",
"".green(),
profile_name.cyan()
);
}
Ok(())
@@ -654,7 +751,8 @@ impl ProfileCommand {
async fn edit_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let profile = manager.get_profile(name)
let profile = manager
.get_profile(name)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?
.clone();
@@ -707,35 +805,51 @@ impl ProfileCommand {
let repo = find_repo(std::env::current_dir()?.as_path())?;
let repo_path = repo.path().to_string_lossy().to_string();
manager.set_repo_profile(repo_path.clone(), name.to_string())?;
// Get the profile and apply it to the repository
let profile = manager.get_profile(name)
let profile = manager
.get_profile(name)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?;
profile.apply_to_repo(repo.inner())?;
manager.record_profile_usage(name, Some(repo_path))?;
manager.save()?;
println!("{} Set '{}' for current repository", "".green(), name.cyan());
println!("{} Applied profile '{}' to current repository", "".green(), name.cyan());
println!(
"{} Set '{}' for current repository",
"".green(),
name.cyan()
);
println!(
"{} Applied profile '{}' to current repository",
"".green(),
name.cyan()
);
Ok(())
}
async fn apply_profile(&self, name: Option<&str>, global: bool, config_path: &Option<PathBuf>) -> Result<()> {
async fn apply_profile(
&self,
name: Option<&str>,
global: bool,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let profile_name = if let Some(n) = name {
n.to_string()
} else {
manager.default_profile_name()
manager
.default_profile_name()
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
.clone()
};
let profile = manager.get_profile(&profile_name)
let profile = manager
.get_profile(&profile_name)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?
.clone();
@@ -748,11 +862,19 @@ impl ProfileCommand {
if global {
profile.apply_global()?;
println!("{} Applied profile '{}' globally", "".green(), profile.name.cyan());
println!(
"{} Applied profile '{}' globally",
"".green(),
profile.name.cyan()
);
} else {
let repo = find_repo(std::env::current_dir()?.as_path())?;
profile.apply_to_repo(repo.inner())?;
println!("{} Applied profile '{}' to current repository", "".green(), profile.name.cyan());
println!(
"{} Applied profile '{}' to current repository",
"".green(),
profile.name.cyan()
);
}
manager.record_profile_usage(&profile_name, repo_path)?;
@@ -763,8 +885,9 @@ impl ProfileCommand {
async fn switch_profile(&self, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let profiles: Vec<String> = manager.list_profiles()
let profiles: Vec<String> = manager
.list_profiles()
.into_iter()
.map(|s| s.to_string())
.collect();
@@ -785,7 +908,7 @@ impl ProfileCommand {
.interact()?;
let selected = &profiles[selection];
manager.set_default_profile(Some(selected.clone()))?;
manager.save()?;
@@ -798,17 +921,24 @@ impl ProfileCommand {
.interact()?;
if apply {
self.apply_profile(Some(selected), false, config_path).await?;
self.apply_profile(Some(selected), false, config_path)
.await?;
}
}
Ok(())
}
async fn copy_profile(&self, from: &str, to: &str, config_path: &Option<PathBuf>) -> Result<()> {
async fn copy_profile(
&self,
from: &str,
to: &str,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let source = manager.get_profile(from)
let source = manager
.get_profile(from)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", from))?
.clone();
@@ -821,20 +951,38 @@ impl ProfileCommand {
manager.add_profile(to.to_string(), new_profile)?;
manager.save()?;
println!("{} Copied profile '{}' to '{}'", "".green(), from, to.cyan());
println!(
"{} Copied profile '{}' to '{}'",
"".green(),
from,
to.cyan()
);
Ok(())
}
async fn handle_token_command(&self, cmd: &TokenSubcommand, config_path: &Option<PathBuf>) -> Result<()> {
async fn handle_token_command(
&self,
cmd: &TokenSubcommand,
config_path: &Option<PathBuf>,
) -> Result<()> {
match cmd {
TokenSubcommand::Add { profile, service } => self.add_token(profile, service, config_path).await,
TokenSubcommand::Remove { profile, service } => self.remove_token(profile, service, config_path).await,
TokenSubcommand::Add { profile, service } => {
self.add_token(profile, service, config_path).await
}
TokenSubcommand::Remove { profile, service } => {
self.remove_token(profile, service, config_path).await
}
TokenSubcommand::List { profile } => self.list_tokens(profile, config_path).await,
}
}
async fn add_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
async fn add_token(
&self,
profile_name: &str,
service: &str,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if !manager.has_profile(profile_name) {
@@ -842,10 +990,15 @@ impl ProfileCommand {
}
if !manager.keyring().is_available() {
bail!("Keyring is not available. Cannot store PAT securely. Please ensure your system keyring is accessible.");
bail!(
"Keyring is not available. Cannot store PAT securely. Please ensure your system keyring is accessible."
);
}
println!("{}", format!("\nAdd token to profile '{}'", profile_name).bold());
println!(
"{}",
format!("\nAdd token to profile '{}'", profile_name).bold()
);
println!("{}", "".repeat(40));
let token_value: String = Input::new()
@@ -878,16 +1031,26 @@ impl ProfileCommand {
}
manager.store_pat_for_profile(profile_name, service, &token_value)?;
manager.add_token_to_profile(profile_name, service.to_string(), token)?;
manager.save()?;
println!("{} Token for '{}' added to profile '{}' (stored securely in keyring)", "".green(), service.cyan(), profile_name);
println!(
"{} Token for '{}' added to profile '{}' (stored securely in keyring)",
"".green(),
service.cyan(),
profile_name
);
Ok(())
}
async fn remove_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
async fn remove_token(
&self,
profile_name: &str,
service: &str,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if !manager.has_profile(profile_name) {
@@ -895,7 +1058,10 @@ impl ProfileCommand {
}
let confirm = Confirm::new()
.with_prompt(format!("Remove token '{}' from profile '{}'?", service, profile_name))
.with_prompt(format!(
"Remove token '{}' from profile '{}'?",
service, profile_name
))
.default(false)
.interact()?;
@@ -907,7 +1073,12 @@ impl ProfileCommand {
manager.remove_token_from_profile(profile_name, service)?;
manager.save()?;
println!("{} Token '{}' removed from profile '{}' (deleted from keyring)", "".green(), service, profile_name);
println!(
"{} Token '{}' removed from profile '{}' (deleted from keyring)",
"".green(),
service,
profile_name
);
Ok(())
}
@@ -915,15 +1086,23 @@ impl ProfileCommand {
async fn list_tokens(&self, profile_name: &str, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let profile = manager.get_profile(profile_name)
let profile = manager
.get_profile(profile_name)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
if profile.tokens.is_empty() {
println!("{} No tokens configured for profile '{}'", "".yellow(), profile_name);
println!(
"{} No tokens configured for profile '{}'",
"".yellow(),
profile_name
);
return Ok(());
}
println!("{}", format!("\nTokens for profile '{}':", profile_name).bold());
println!(
"{}",
format!("\nTokens for profile '{}':", profile_name).bold()
);
println!("{}", "".repeat(40));
for (service, token) in &profile.tokens {
@@ -933,8 +1112,13 @@ impl ProfileCommand {
} else {
format!("[{}]", "not stored".yellow())
};
println!("{} {} ({})", service.cyan().bold(), status, token.token_type);
println!(
"{} {} ({})",
service.cyan().bold(),
status,
token.token_type
);
if let Some(ref desc) = token.description {
println!(" {}", desc);
}
@@ -952,7 +1136,8 @@ impl ProfileCommand {
let profile_name = if let Some(n) = name {
n.to_string()
} else {
manager.default_profile_name()
manager
.default_profile_name()
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
.clone()
};
@@ -960,15 +1145,28 @@ impl ProfileCommand {
let repo = find_repo(std::env::current_dir()?.as_path())?;
let comparison = manager.check_profile_config(&profile_name, repo.inner())?;
println!("{}", format!("\nChecking profile '{}' against git configuration", profile_name).bold());
println!(
"{}",
format!(
"\nChecking profile '{}' against git configuration",
profile_name
)
.bold()
);
println!("{}", "".repeat(60));
if comparison.matches {
println!("{} Profile configuration matches git settings", "".green().bold());
println!(
"{} Profile configuration matches git settings",
"".green().bold()
);
} else {
println!("{} Profile configuration differs from git settings", "".red().bold());
println!(
"{} Profile configuration differs from git settings",
"".red().bold()
);
println!("\n{}", "Differences:".bold());
for diff in &comparison.differences {
println!("\n {}:", diff.key.cyan());
println!(" Profile: {}", diff.profile_value.green());
@@ -983,13 +1181,14 @@ impl ProfileCommand {
let manager = self.get_manager(config_path)?;
if let Some(n) = name {
let profile = manager.get_profile(n)
let profile = manager
.get_profile(n)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?;
self.show_single_profile_stats(profile);
} else {
let profiles = manager.list_profiles();
if profiles.is_empty() {
println!("{}", "No profiles configured.".yellow());
return Ok(());
@@ -1012,7 +1211,7 @@ impl ProfileCommand {
fn show_single_profile_stats(&self, profile: &GitProfile) {
println!("{}", format!("\n{}", profile.name).bold());
println!(" Total uses: {}", profile.usage.total_uses);
if let Some(ref last_used) = profile.usage.last_used {
println!(" Last used: {}", last_used);
}
@@ -1048,9 +1247,7 @@ impl ProfileCommand {
}
async fn setup_gpg_interactive(&self) -> Result<GpgConfig> {
let key_id: String = Input::new()
.with_prompt("GPG key ID")
.interact_text()?;
let key_id: String = Input::new().with_prompt("GPG key ID").interact_text()?;
Ok(GpgConfig {
key_id,
@@ -1061,9 +1258,16 @@ impl ProfileCommand {
})
}
async fn setup_token_interactive(&self, profile: &mut GitProfile, manager: &ConfigManager) -> Result<()> {
async fn setup_token_interactive(
&self,
profile: &mut GitProfile,
manager: &ConfigManager,
) -> Result<()> {
if !manager.keyring().is_available() {
println!("{} Keyring is not available. Cannot store PAT securely.", "".yellow());
println!(
"{} Keyring is not available. Cannot store PAT securely.",
"".yellow()
);
let continue_anyway = Confirm::new()
.with_prompt("Continue without secure token storage?")
.default(false)
@@ -1077,17 +1281,15 @@ impl ProfileCommand {
.with_prompt("Service name (e.g., github, gitlab)")
.interact_text()?;
let token_value: String = Input::new()
.with_prompt("Token value")
.interact_text()?;
let token_value: String = Input::new().with_prompt("Token value").interact_text()?;
let token = TokenConfig::new(TokenType::Personal);
if manager.keyring().is_available() {
manager.store_pat_for_profile(&profile.name, &service, &token_value)?;
println!("{} Token stored securely in keyring", "".green());
}
profile.add_token(service, token);
Ok(())

View File

@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use clap::Parser;
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
@@ -6,11 +6,11 @@ use semver::Version;
use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::git::{find_repo, GitRepo};
use crate::generator::ContentGenerator;
use crate::git::tag::{
bump_version, get_latest_version, suggest_version_bump, TagBuilder, VersionBump,
TagBuilder, VersionBump, bump_version, get_latest_version, suggest_version_bump,
};
use crate::git::{GitRepo, find_repo};
use crate::i18n::Messages;
/// Generate and create Git tags
@@ -83,30 +83,37 @@ impl TagCommand {
} else if let Some(bump_str) = &self.bump {
// Calculate bumped version
let prefix = &config.tag.version_prefix;
let latest = get_latest_version(&repo, prefix)?
.unwrap_or_else(|| Version::new(0, 0, 0));
let latest =
get_latest_version(&repo, prefix)?.unwrap_or_else(|| Version::new(0, 0, 0));
let bump = VersionBump::from_str(bump_str)?;
let new_version = bump_version(&latest, bump, None);
format!("{}{}", prefix, new_version)
} else {
// Interactive mode
self.select_version_interactive(&repo, &config.tag.version_prefix, &messages).await?
self.select_version_interactive(&repo, &config.tag.version_prefix, &messages)
.await?
};
// Validate tag name (if it looks like a version)
if tag_name.starts_with('v') || tag_name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
if tag_name.starts_with('v')
|| tag_name
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
let version_str = tag_name.trim_start_matches('v');
if let Err(e) = crate::utils::validators::validate_semver(version_str) {
println!("{}: {}", "Warning".yellow(), e);
if !self.yes {
let proceed = Confirm::new()
.with_prompt("Proceed with this tag name anyway?")
.default(true)
.interact()?;
if !proceed {
bail!("{}", messages.tag_cancelled());
}
@@ -120,7 +127,10 @@ impl TagCommand {
} else if let Some(msg) = &self.message {
Some(msg.clone())
} else if self.generate || (config.tag.auto_generate && !self.yes) {
Some(self.generate_tag_message(&repo, &tag_name, &messages).await?)
Some(
self.generate_tag_message(&repo, &tag_name, &messages)
.await?,
)
} else if !self.yes {
Some(self.input_message_interactive(&tag_name, &messages)?)
} else {
@@ -188,12 +198,17 @@ impl TagCommand {
Ok(())
}
async fn select_version_interactive(&self, repo: &GitRepo, prefix: &str, messages: &Messages) -> Result<String> {
async fn select_version_interactive(
&self,
repo: &GitRepo,
prefix: &str,
messages: &Messages,
) -> Result<String> {
loop {
let latest = get_latest_version(repo, prefix)?;
println!("\n{}", messages.version_selection().bold());
if let Some(ref version) = latest {
println!("{} {}{}", messages.latest_version(), prefix, version);
} else {
@@ -220,36 +235,46 @@ impl TagCommand {
// Auto-detect
let commits = repo.get_commits(50)?;
let bump = suggest_version_bump(&commits);
let version = latest.as_ref()
let version = latest
.as_ref()
.map(|v| bump_version(v, bump, None))
.unwrap_or_else(|| Version::new(0, 1, 0));
println!("{} {:?}{}{}", messages.suggested_bump(), bump, prefix, version);
println!(
"{} {:?}{}{}",
messages.suggested_bump(),
bump,
prefix,
version
);
let confirm = Confirm::new()
.with_prompt(messages.use_this_version())
.default(true)
.interact()?;
if confirm {
return Ok(format!("{}{}", prefix, version));
}
// User rejected, continue the loop
}
1 => {
let version = latest.as_ref()
let version = latest
.as_ref()
.map(|v| bump_version(v, VersionBump::Major, None))
.unwrap_or_else(|| Version::new(1, 0, 0));
return Ok(format!("{}{}", prefix, version));
}
2 => {
let version = latest.as_ref()
let version = latest
.as_ref()
.map(|v| bump_version(v, VersionBump::Minor, None))
.unwrap_or_else(|| Version::new(0, 1, 0));
return Ok(format!("{}{}", prefix, version));
}
3 => {
let version = latest.as_ref()
let version = latest
.as_ref()
.map(|v| bump_version(v, VersionBump::Patch, None))
.unwrap_or_else(|| Version::new(0, 0, 1));
return Ok(format!("{}{}", prefix, version));
@@ -272,7 +297,12 @@ impl TagCommand {
}
}
async fn generate_tag_message(&self, repo: &GitRepo, version: &str, messages: &Messages) -> Result<String> {
async fn generate_tag_message(
&self,
repo: &GitRepo,
version: &str,
messages: &Messages,
) -> Result<String> {
let manager = ConfigManager::new()?;
let language = manager.get_language().unwrap_or(Language::English);
@@ -290,17 +320,19 @@ impl TagCommand {
println!("{}", messages.ai_generating_tag(commits.len()));
let generator = ContentGenerator::new_with_think(&manager, self.think).await?;
generator.generate_tag_message(version, &commits, language).await
generator
.generate_tag_message(version, &commits, language)
.await
}
fn input_message_interactive(&self, version: &str, messages: &Messages) -> Result<String> {
let default_msg = format!("Release {}", version);
let use_editor = Confirm::new()
.with_prompt(messages.open_editor())
.default(false)
.interact()?;
if use_editor {
crate::utils::editor::edit_content(&default_msg)
} else {