style: 格式化代码并优化导入顺序
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user