Compare commits
4 Commits
88324c21c2
...
v0.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
a514cdc69f
|
|||
|
e822ba1f54
|
|||
|
3c925d8268
|
|||
|
c9073ff4a7
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "quicommit"
|
name = "quicommit"
|
||||||
version = "0.1.4"
|
version = "0.1.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
||||||
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ use crate::i18n::{Messages, translate_changelog_category};
|
|||||||
|
|
||||||
/// Generate changelog
|
/// Generate changelog
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
#[command(disable_version_flag = true, disable_help_flag = false)]
|
||||||
pub struct ChangelogCommand {
|
pub struct ChangelogCommand {
|
||||||
/// Output file path
|
/// Output file path
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
|
|
||||||
/// Version to generate changelog for
|
/// Version to generate changelog for
|
||||||
#[arg(short, long)]
|
#[arg(long)]
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
|
|
||||||
/// Generate from specific tag
|
/// Generate from specific tag
|
||||||
@@ -38,10 +39,6 @@ pub struct ChangelogCommand {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
generate: bool,
|
generate: bool,
|
||||||
|
|
||||||
/// Prepend to existing changelog
|
|
||||||
#[arg(short, long)]
|
|
||||||
prepend: bool,
|
|
||||||
|
|
||||||
/// Include commit hashes
|
/// Include commit hashes
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
include_hashes: bool,
|
include_hashes: bool,
|
||||||
@@ -51,7 +48,7 @@ pub struct ChangelogCommand {
|
|||||||
include_authors: bool,
|
include_authors: bool,
|
||||||
|
|
||||||
/// Format (keep-a-changelog, github-releases)
|
/// Format (keep-a-changelog, github-releases)
|
||||||
#[arg(short, long)]
|
#[arg(long)]
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
|
|
||||||
/// Dry run (output to stdout)
|
/// Dry run (output to stdout)
|
||||||
@@ -64,9 +61,13 @@ pub struct ChangelogCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChangelogCommand {
|
impl ChangelogCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||||
let manager = ConfigManager::new()?;
|
let manager = if let Some(ref path) = config_path {
|
||||||
|
ConfigManager::with_path(path)?
|
||||||
|
} else {
|
||||||
|
ConfigManager::new()?
|
||||||
|
};
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
let language = manager.get_language().unwrap_or(Language::English);
|
let language = manager.get_language().unwrap_or(Language::English);
|
||||||
let messages = Messages::new(language);
|
let messages = Messages::new(language);
|
||||||
@@ -157,13 +158,34 @@ impl ChangelogCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to file
|
// Write to file (always prepend to preserve history)
|
||||||
if self.prepend && output_path.exists() {
|
if output_path.exists() {
|
||||||
let existing = std::fs::read_to_string(&output_path)?;
|
let existing = std::fs::read_to_string(&output_path)?;
|
||||||
let new_content = format!("{}\n{}", changelog, existing);
|
let new_content = if existing.is_empty() {
|
||||||
|
format!("# Changelog\n\n{}", changelog)
|
||||||
|
} else {
|
||||||
|
let lines: Vec<&str> = existing.lines().collect();
|
||||||
|
let mut header_end = 0;
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if i == 0 && line.starts_with('#') {
|
||||||
|
header_end = i + 1;
|
||||||
|
} else if line.trim().is_empty() {
|
||||||
|
header_end = i + 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = lines[..header_end].join("\n");
|
||||||
|
let rest = lines[header_end..].join("\n");
|
||||||
|
|
||||||
|
format!("{}\n{}\n{}", header, changelog, rest)
|
||||||
|
};
|
||||||
std::fs::write(&output_path, new_content)?;
|
std::fs::write(&output_path, new_content)?;
|
||||||
} else {
|
} else {
|
||||||
std::fs::write(&output_path, changelog)?;
|
let content = format!("# Changelog\n\n{}", changelog);
|
||||||
|
std::fs::write(&output_path, content)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{} {:?}", messages.changelog_written(), output_path);
|
println!("{} {:?}", messages.changelog_written(), output_path);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use anyhow::{bail, Context, Result};
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::{Language, manager::ConfigManager};
|
use crate::config::{Language, manager::ConfigManager};
|
||||||
use crate::config::CommitFormat;
|
use crate::config::CommitFormat;
|
||||||
@@ -84,12 +85,16 @@ pub struct CommitCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CommitCommand {
|
impl CommitCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
// Find git repository
|
// Find git repository
|
||||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
let manager = ConfigManager::new()?;
|
let manager = if let Some(ref path) = config_path {
|
||||||
|
ConfigManager::with_path(path)?
|
||||||
|
} else {
|
||||||
|
ConfigManager::new()?
|
||||||
|
};
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
let language = manager.get_language().unwrap_or(Language::English);
|
let language = manager.get_language().unwrap_or(Language::English);
|
||||||
let messages = Messages::new(language);
|
let messages = Messages::new(language);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use anyhow::{bail, Result};
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::{Language, manager::ConfigManager};
|
use crate::config::{Language, manager::ConfigManager};
|
||||||
use crate::config::CommitFormat;
|
use crate::config::CommitFormat;
|
||||||
@@ -191,43 +192,60 @@ enum ConfigSubcommand {
|
|||||||
|
|
||||||
/// Test LLM connection
|
/// Test LLM connection
|
||||||
TestLlm,
|
TestLlm,
|
||||||
|
|
||||||
|
/// Show config file path
|
||||||
|
Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigCommand {
|
impl ConfigCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
match &self.command {
|
match &self.command {
|
||||||
Some(ConfigSubcommand::Show) => self.show_config().await,
|
Some(ConfigSubcommand::Show) => self.show_config(&config_path).await,
|
||||||
Some(ConfigSubcommand::List) => self.list_config().await,
|
Some(ConfigSubcommand::List) => self.list_config(&config_path).await,
|
||||||
Some(ConfigSubcommand::Edit) => self.edit_config().await,
|
Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await,
|
||||||
Some(ConfigSubcommand::Set { key, value }) => self.set_value(key, value).await,
|
Some(ConfigSubcommand::Set { key, value }) => self.set_value(key, value, &config_path).await,
|
||||||
Some(ConfigSubcommand::Get { key }) => self.get_value(key).await,
|
Some(ConfigSubcommand::Get { key }) => self.get_value(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetLlm { provider }) => self.set_llm(provider.as_deref()).await,
|
Some(ConfigSubcommand::SetLlm { provider }) => self.set_llm(provider.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetOpenAiKey { key }) => self.set_openai_key(key).await,
|
Some(ConfigSubcommand::SetOpenAiKey { key }) => self.set_openai_key(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetAnthropicKey { key }) => self.set_anthropic_key(key).await,
|
Some(ConfigSubcommand::SetAnthropicKey { key }) => self.set_anthropic_key(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetKimiKey { key }) => self.set_kimi_key(key).await,
|
Some(ConfigSubcommand::SetKimiKey { key }) => self.set_kimi_key(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetDeepSeekKey { key }) => self.set_deepseek_key(key).await,
|
Some(ConfigSubcommand::SetDeepSeekKey { key }) => self.set_deepseek_key(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetOpenRouterKey { key }) => self.set_openrouter_key(key).await,
|
Some(ConfigSubcommand::SetOpenRouterKey { key }) => self.set_openrouter_key(key, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetOllama { url, model }) => self.set_ollama(url.as_deref(), model.as_deref()).await,
|
Some(ConfigSubcommand::SetOllama { url, model }) => self.set_ollama(url.as_deref(), model.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetKimi { base_url, model }) => self.set_kimi(base_url.as_deref(), model.as_deref()).await,
|
Some(ConfigSubcommand::SetKimi { base_url, model }) => self.set_kimi(base_url.as_deref(), model.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetDeepSeek { base_url, model }) => self.set_deepseek(base_url.as_deref(), model.as_deref()).await,
|
Some(ConfigSubcommand::SetDeepSeek { base_url, model }) => self.set_deepseek(base_url.as_deref(), model.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetOpenRouter { base_url, model }) => self.set_openrouter(base_url.as_deref(), model.as_deref()).await,
|
Some(ConfigSubcommand::SetOpenRouter { base_url, model }) => self.set_openrouter(base_url.as_deref(), model.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetCommitFormat { format }) => self.set_commit_format(format).await,
|
Some(ConfigSubcommand::SetCommitFormat { format }) => self.set_commit_format(format, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => self.set_version_prefix(prefix).await,
|
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => self.set_version_prefix(prefix, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetChangelogPath { path }) => self.set_changelog_path(path).await,
|
Some(ConfigSubcommand::SetChangelogPath { path }) => self.set_changelog_path(path, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetLanguage { language }) => self.set_language(language.as_deref()).await,
|
Some(ConfigSubcommand::SetLanguage { language }) => self.set_language(language.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep).await,
|
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep).await,
|
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep, &config_path).await,
|
||||||
Some(ConfigSubcommand::Reset { force }) => self.reset(*force).await,
|
Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await,
|
||||||
Some(ConfigSubcommand::Export { output }) => self.export_config(output.as_deref()).await,
|
Some(ConfigSubcommand::Export { output }) => self.export_config(output.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::Import { file }) => self.import_config(file).await,
|
Some(ConfigSubcommand::Import { file }) => self.import_config(file, &config_path).await,
|
||||||
Some(ConfigSubcommand::ListModels) => self.list_models().await,
|
Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await,
|
||||||
Some(ConfigSubcommand::TestLlm) => self.test_llm().await,
|
Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await,
|
||||||
None => self.show_config().await,
|
Some(ConfigSubcommand::Path) => self.show_path(&config_path).await,
|
||||||
|
None => self.show_config(&config_path).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_config(&self) -> Result<()> {
|
fn get_manager(&self, config_path: &Option<PathBuf>) -> Result<ConfigManager> {
|
||||||
let manager = ConfigManager::new()?;
|
match config_path {
|
||||||
|
Some(path) => ConfigManager::with_path(path),
|
||||||
|
None => ConfigManager::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_path(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
|
let manager = self.get_manager(config_path)?;
|
||||||
|
println!("{}", manager.path().display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
|
let manager = self.get_manager(config_path)?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
println!("{}", "\nQuiCommit Configuration".bold());
|
println!("{}", "\nQuiCommit Configuration".bold());
|
||||||
@@ -306,8 +324,8 @@ impl ConfigCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// List all configuration information with masked API keys
|
/// List all configuration information with masked API keys
|
||||||
async fn list_config(&self) -> Result<()> {
|
async fn list_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
println!("{}", "\nQuiCommit Configuration".bold());
|
println!("{}", "\nQuiCommit Configuration".bold());
|
||||||
@@ -404,15 +422,15 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit_config(&self) -> Result<()> {
|
async fn edit_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
crate::utils::editor::edit_file(manager.path())?;
|
crate::utils::editor::edit_file(manager.path())?;
|
||||||
println!("{} Configuration updated", "✓".green());
|
println!("{} Configuration updated", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_value(&self, key: &str, value: &str) -> Result<()> {
|
async fn set_value(&self, key: &str, value: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"llm.provider" => manager.set_llm_provider(value.to_string()),
|
"llm.provider" => manager.set_llm_provider(value.to_string()),
|
||||||
@@ -450,8 +468,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_value(&self, key: &str) -> Result<()> {
|
async fn get_value(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
let value = match key {
|
let value = match key {
|
||||||
@@ -469,8 +487,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_llm(&self, provider: Option<&str>) -> Result<()> {
|
async fn set_llm(&self, provider: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let provider = if let Some(p) = provider {
|
let provider = if let Some(p) = provider {
|
||||||
p.to_string()
|
p.to_string()
|
||||||
@@ -602,48 +620,48 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_openai_key(&self, key: &str) -> Result<()> {
|
async fn set_openai_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_openai_api_key(key.to_string());
|
manager.set_openai_api_key(key.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} OpenAI API key set", "✓".green());
|
println!("{} OpenAI API key set", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_anthropic_key(&self, key: &str) -> Result<()> {
|
async fn set_anthropic_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_anthropic_api_key(key.to_string());
|
manager.set_anthropic_api_key(key.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} Anthropic API key set", "✓".green());
|
println!("{} Anthropic API key set", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_kimi_key(&self, key: &str) -> Result<()> {
|
async fn set_kimi_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_kimi_api_key(key.to_string());
|
manager.set_kimi_api_key(key.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} Kimi API key set", "✓".green());
|
println!("{} Kimi API key set", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_deepseek_key(&self, key: &str) -> Result<()> {
|
async fn set_deepseek_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_deepseek_api_key(key.to_string());
|
manager.set_deepseek_api_key(key.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} DeepSeek API key set", "✓".green());
|
println!("{} DeepSeek API key set", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_openrouter_key(&self, key: &str) -> Result<()> {
|
async fn set_openrouter_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_openrouter_api_key(key.to_string());
|
manager.set_openrouter_api_key(key.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} OpenRouter API key set", "✓".green());
|
println!("{} OpenRouter API key set", "✓".green());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_kimi(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
|
async fn set_kimi(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if let Some(url) = base_url {
|
if let Some(url) = base_url {
|
||||||
manager.set_kimi_base_url(url.to_string());
|
manager.set_kimi_base_url(url.to_string());
|
||||||
@@ -657,8 +675,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_deepseek(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
|
async fn set_deepseek(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if let Some(url) = base_url {
|
if let Some(url) = base_url {
|
||||||
manager.set_deepseek_base_url(url.to_string());
|
manager.set_deepseek_base_url(url.to_string());
|
||||||
@@ -672,8 +690,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_openrouter(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
|
async fn set_openrouter(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if let Some(url) = base_url {
|
if let Some(url) = base_url {
|
||||||
manager.set_openrouter_base_url(url.to_string());
|
manager.set_openrouter_base_url(url.to_string());
|
||||||
@@ -687,8 +705,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_ollama(&self, url: Option<&str>, model: Option<&str>) -> Result<()> {
|
async fn set_ollama(&self, url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if let Some(u) = url {
|
if let Some(u) = url {
|
||||||
manager.config_mut().llm.ollama.url = u.to_string();
|
manager.config_mut().llm.ollama.url = u.to_string();
|
||||||
@@ -702,8 +720,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_commit_format(&self, format: &str) -> Result<()> {
|
async fn set_commit_format(&self, format: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let format = match format {
|
let format = match format {
|
||||||
"conventional" => CommitFormat::Conventional,
|
"conventional" => CommitFormat::Conventional,
|
||||||
@@ -717,24 +735,24 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_version_prefix(&self, prefix: &str) -> Result<()> {
|
async fn set_version_prefix(&self, prefix: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_version_prefix(prefix.to_string());
|
manager.set_version_prefix(prefix.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} Set version prefix to '{}'", "✓".green(), prefix);
|
println!("{} Set version prefix to '{}'", "✓".green(), prefix);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_changelog_path(&self, path: &str) -> Result<()> {
|
async fn set_changelog_path(&self, path: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_changelog_path(path.to_string());
|
manager.set_changelog_path(path.to_string());
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
println!("{} Set changelog path to {}", "✓".green(), path);
|
println!("{} Set changelog path to {}", "✓".green(), path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_language(&self, language: Option<&str>) -> Result<()> {
|
async fn set_language(&self, language: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let language_code = if let Some(lang) = language {
|
let language_code = if let Some(lang) = language {
|
||||||
lang.to_string()
|
lang.to_string()
|
||||||
@@ -763,8 +781,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_keep_types_english(&self, keep: bool) -> Result<()> {
|
async fn set_keep_types_english(&self, keep: bool, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_keep_types_english(keep);
|
manager.set_keep_types_english(keep);
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
let status = if keep { "enabled" } else { "disabled" };
|
let status = if keep { "enabled" } else { "disabled" };
|
||||||
@@ -772,8 +790,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_keep_changelog_types_english(&self, keep: bool) -> Result<()> {
|
async fn set_keep_changelog_types_english(&self, keep: bool, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.set_keep_changelog_types_english(keep);
|
manager.set_keep_changelog_types_english(keep);
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
let status = if keep { "enabled" } else { "disabled" };
|
let status = if keep { "enabled" } else { "disabled" };
|
||||||
@@ -781,7 +799,7 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reset(&self, force: bool) -> Result<()> {
|
async fn reset(&self, force: bool, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
if !force {
|
if !force {
|
||||||
let confirm = Confirm::new()
|
let confirm = Confirm::new()
|
||||||
.with_prompt("Are you sure you want to reset all configuration?")
|
.with_prompt("Are you sure you want to reset all configuration?")
|
||||||
@@ -794,7 +812,7 @@ impl ConfigCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.reset();
|
manager.reset();
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
|
|
||||||
@@ -802,8 +820,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn export_config(&self, output: Option<&str>) -> Result<()> {
|
async fn export_config(&self, output: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let toml = manager.export()?;
|
let toml = manager.export()?;
|
||||||
|
|
||||||
if let Some(path) = output {
|
if let Some(path) = output {
|
||||||
@@ -816,10 +834,10 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_config(&self, file: &str) -> Result<()> {
|
async fn import_config(&self, file: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let toml = std::fs::read_to_string(file)?;
|
let toml = std::fs::read_to_string(file)?;
|
||||||
|
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.import(&toml)?;
|
manager.import(&toml)?;
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
|
|
||||||
@@ -827,8 +845,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_models(&self) -> Result<()> {
|
async fn list_models(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
match config.llm.provider.as_str() {
|
match config.llm.provider.as_str() {
|
||||||
@@ -984,8 +1002,8 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_llm(&self) -> Result<()> {
|
async fn test_llm(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
println!("Testing LLM connection ({})...", config.llm.provider.cyan());
|
println!("Testing LLM connection ({})...", config.llm.provider.cyan());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::{GitProfile, Language};
|
use crate::config::{GitProfile, Language};
|
||||||
use crate::config::manager::ConfigManager;
|
use crate::config::manager::ConfigManager;
|
||||||
@@ -22,12 +23,13 @@ pub struct InitCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InitCommand {
|
impl InitCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
// Start with English messages for initialization
|
|
||||||
let messages = Messages::new(Language::English);
|
let messages = Messages::new(Language::English);
|
||||||
println!("{}", messages.initializing().bold().cyan());
|
println!("{}", messages.initializing().bold().cyan());
|
||||||
|
|
||||||
let config_path = crate::config::AppConfig::default_path()?;
|
let config_path = config_path.unwrap_or_else(|| {
|
||||||
|
crate::config::AppConfig::default_path().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
// Check if config already exists
|
// Check if config already exists
|
||||||
if config_path.exists() && !self.reset {
|
if config_path.exists() && !self.reset {
|
||||||
@@ -41,20 +43,24 @@ impl InitCommand {
|
|||||||
println!("{}", "Initialization cancelled.".yellow());
|
println!("{}", "Initialization cancelled.".yellow());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println!("{}", "Configuration already exists. Use --reset to overwrite.".yellow());
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut manager = if self.reset {
|
// Create parent directory if needed
|
||||||
ConfigManager::new()?
|
if let Some(parent) = config_path.parent() {
|
||||||
} else {
|
std::fs::create_dir_all(parent)
|
||||||
ConfigManager::new().or_else(|_| Ok::<_, anyhow::Error>(ConfigManager::default()))?
|
.map_err(|e| anyhow::anyhow!("Failed to create config directory: {}", e))?;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Create new config manager with fresh config
|
||||||
|
let mut manager = ConfigManager::with_path_fresh(&config_path)?;
|
||||||
|
|
||||||
if self.yes {
|
if self.yes {
|
||||||
// Quick setup with defaults
|
|
||||||
self.quick_setup(&mut manager).await?;
|
self.quick_setup(&mut manager).await?;
|
||||||
} else {
|
} else {
|
||||||
// Interactive setup
|
|
||||||
self.interactive_setup(&mut manager).await?;
|
self.interactive_setup(&mut manager).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use anyhow::{bail, Result};
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::manager::ConfigManager;
|
use crate::config::manager::ConfigManager;
|
||||||
use crate::config::{GitProfile, TokenConfig, TokenType};
|
use crate::config::{GitProfile, TokenConfig, TokenType};
|
||||||
@@ -123,27 +124,34 @@ enum TokenSubcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileCommand {
|
impl ProfileCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
match &self.command {
|
match &self.command {
|
||||||
Some(ProfileSubcommand::Add) => self.add_profile().await,
|
Some(ProfileSubcommand::Add) => self.add_profile(&config_path).await,
|
||||||
Some(ProfileSubcommand::Remove { name }) => self.remove_profile(name).await,
|
Some(ProfileSubcommand::Remove { name }) => self.remove_profile(name, &config_path).await,
|
||||||
Some(ProfileSubcommand::List) => self.list_profiles().await,
|
Some(ProfileSubcommand::List) => self.list_profiles(&config_path).await,
|
||||||
Some(ProfileSubcommand::Show { name }) => self.show_profile(name.as_deref()).await,
|
Some(ProfileSubcommand::Show { name }) => self.show_profile(name.as_deref(), &config_path).await,
|
||||||
Some(ProfileSubcommand::Edit { name }) => self.edit_profile(name).await,
|
Some(ProfileSubcommand::Edit { name }) => self.edit_profile(name, &config_path).await,
|
||||||
Some(ProfileSubcommand::SetDefault { name }) => self.set_default(name).await,
|
Some(ProfileSubcommand::SetDefault { name }) => self.set_default(name, &config_path).await,
|
||||||
Some(ProfileSubcommand::SetRepo { name }) => self.set_repo(name).await,
|
Some(ProfileSubcommand::SetRepo { name }) => self.set_repo(name, &config_path).await,
|
||||||
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global).await,
|
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global, &config_path).await,
|
||||||
Some(ProfileSubcommand::Switch) => self.switch_profile().await,
|
Some(ProfileSubcommand::Switch) => self.switch_profile(&config_path).await,
|
||||||
Some(ProfileSubcommand::Copy { from, to }) => self.copy_profile(from, to).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).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()).await,
|
Some(ProfileSubcommand::Check { name }) => self.check_profile(name.as_deref(), &config_path).await,
|
||||||
Some(ProfileSubcommand::Stats { name }) => self.show_stats(name.as_deref()).await,
|
Some(ProfileSubcommand::Stats { name }) => self.show_stats(name.as_deref(), &config_path).await,
|
||||||
None => self.list_profiles().await,
|
None => self.list_profiles(&config_path).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_profile(&self) -> Result<()> {
|
fn get_manager(&self, config_path: &Option<PathBuf>) -> Result<ConfigManager> {
|
||||||
let mut manager = ConfigManager::new()?;
|
match config_path {
|
||||||
|
Some(path) => ConfigManager::with_path(path),
|
||||||
|
None => ConfigManager::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_profile(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
println!("{}", "\nAdd new profile".bold());
|
println!("{}", "\nAdd new profile".bold());
|
||||||
println!("{}", "─".repeat(40));
|
println!("{}", "─".repeat(40));
|
||||||
@@ -244,8 +252,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_profile(&self, name: &str) -> Result<()> {
|
async fn remove_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if !manager.has_profile(name) {
|
if !manager.has_profile(name) {
|
||||||
bail!("Profile '{}' not found", name);
|
bail!("Profile '{}' not found", name);
|
||||||
@@ -269,8 +277,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_profiles(&self) -> Result<()> {
|
async fn list_profiles(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let profiles = manager.list_profiles();
|
let profiles = manager.list_profiles();
|
||||||
|
|
||||||
@@ -319,8 +327,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_profile(&self, name: Option<&str>) -> Result<()> {
|
async fn show_profile(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let profile = if let Some(n) = name {
|
let profile = if let Some(n) = name {
|
||||||
manager.get_profile(n)
|
manager.get_profile(n)
|
||||||
@@ -380,8 +388,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit_profile(&self, name: &str) -> Result<()> {
|
async fn edit_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
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))?
|
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?
|
||||||
@@ -420,8 +428,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_default(&self, name: &str) -> Result<()> {
|
async fn set_default(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
manager.set_default_profile(Some(name.to_string()))?;
|
manager.set_default_profile(Some(name.to_string()))?;
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
@@ -431,22 +439,30 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_repo(&self, name: &str) -> Result<()> {
|
async fn set_repo(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||||
|
|
||||||
let repo_path = repo.path().to_string_lossy().to_string();
|
let repo_path = repo.path().to_string_lossy().to_string();
|
||||||
|
|
||||||
manager.set_repo_profile(repo_path, name.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)
|
||||||
|
.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()?;
|
manager.save()?;
|
||||||
|
|
||||||
println!("{} Set '{}' for current repository", "✓".green(), name.cyan());
|
println!("{} Set '{}' for current repository", "✓".green(), name.cyan());
|
||||||
|
println!("{} Applied profile '{}' to current repository", "✓".green(), name.cyan());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_profile(&self, name: Option<&str>, global: bool) -> Result<()> {
|
async fn apply_profile(&self, name: Option<&str>, global: bool, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let profile_name = if let Some(n) = name {
|
let profile_name = if let Some(n) = name {
|
||||||
n.to_string()
|
n.to_string()
|
||||||
@@ -482,8 +498,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn switch_profile(&self) -> Result<()> {
|
async fn switch_profile(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let profiles: Vec<String> = manager.list_profiles()
|
let profiles: Vec<String> = manager.list_profiles()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -519,15 +535,15 @@ impl ProfileCommand {
|
|||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
if apply {
|
if apply {
|
||||||
self.apply_profile(Some(selected), false).await?;
|
self.apply_profile(Some(selected), false, config_path).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn copy_profile(&self, from: &str, to: &str) -> Result<()> {
|
async fn copy_profile(&self, from: &str, to: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
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))?
|
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", from))?
|
||||||
@@ -547,16 +563,16 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_token_command(&self, cmd: &TokenSubcommand) -> Result<()> {
|
async fn handle_token_command(&self, cmd: &TokenSubcommand, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
TokenSubcommand::Add { profile, service } => self.add_token(profile, service).await,
|
TokenSubcommand::Add { profile, service } => self.add_token(profile, service, config_path).await,
|
||||||
TokenSubcommand::Remove { profile, service } => self.remove_token(profile, service).await,
|
TokenSubcommand::Remove { profile, service } => self.remove_token(profile, service, config_path).await,
|
||||||
TokenSubcommand::List { profile } => self.list_tokens(profile).await,
|
TokenSubcommand::List { profile } => self.list_tokens(profile, config_path).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_token(&self, profile_name: &str, service: &str) -> Result<()> {
|
async fn add_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if !manager.has_profile(profile_name) {
|
if !manager.has_profile(profile_name) {
|
||||||
bail!("Profile '{}' not found", profile_name);
|
bail!("Profile '{}' not found", profile_name);
|
||||||
@@ -602,8 +618,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_token(&self, profile_name: &str, service: &str) -> Result<()> {
|
async fn remove_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let mut manager = ConfigManager::new()?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if !manager.has_profile(profile_name) {
|
if !manager.has_profile(profile_name) {
|
||||||
bail!("Profile '{}' not found", profile_name);
|
bail!("Profile '{}' not found", profile_name);
|
||||||
@@ -627,8 +643,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_tokens(&self, profile_name: &str) -> Result<()> {
|
async fn list_tokens(&self, profile_name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
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))?;
|
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
||||||
@@ -654,8 +670,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_profile(&self, name: Option<&str>) -> Result<()> {
|
async fn check_profile(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
let profile_name = if let Some(n) = name {
|
let profile_name = if let Some(n) = name {
|
||||||
n.to_string()
|
n.to_string()
|
||||||
@@ -687,8 +703,8 @@ impl ProfileCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_stats(&self, name: Option<&str>) -> Result<()> {
|
async fn show_stats(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = self.get_manager(config_path)?;
|
||||||
|
|
||||||
if let Some(n) = name {
|
if let Some(n) = name {
|
||||||
let profile = manager.get_profile(n)
|
let profile = manager.get_profile(n)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use clap::Parser;
|
|||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::{Language, manager::ConfigManager};
|
use crate::config::{Language, manager::ConfigManager};
|
||||||
use crate::git::{find_repo, GitRepo};
|
use crate::git::{find_repo, GitRepo};
|
||||||
@@ -61,9 +62,13 @@ pub struct TagCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TagCommand {
|
impl TagCommand {
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||||
let manager = ConfigManager::new()?;
|
let manager = if let Some(ref path) = config_path {
|
||||||
|
ConfigManager::with_path(path)?
|
||||||
|
} else {
|
||||||
|
ConfigManager::new()?
|
||||||
|
};
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
let language = manager.get_language().unwrap_or(Language::English);
|
let language = manager.get_language().unwrap_or(Language::English);
|
||||||
let messages = Messages::new(language);
|
let messages = Messages::new(language);
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ impl ConfigManager {
|
|||||||
|
|
||||||
/// Create config manager with specific path
|
/// Create config manager with specific path
|
||||||
pub fn with_path(path: &Path) -> Result<Self> {
|
pub fn with_path(path: &Path) -> Result<Self> {
|
||||||
let config = AppConfig::load(path)?;
|
let config = if path.exists() {
|
||||||
|
AppConfig::load(path)?
|
||||||
|
} else {
|
||||||
|
AppConfig::default()
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
config_path: path.to_path_buf(),
|
config_path: path.to_path_buf(),
|
||||||
@@ -27,6 +31,15 @@ impl ConfigManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create config manager with fresh config (ignoring existing)
|
||||||
|
pub fn with_path_fresh(path: &Path) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
config: AppConfig::default(),
|
||||||
|
config_path: path.to_path_buf(),
|
||||||
|
modified: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Get configuration reference
|
/// Get configuration reference
|
||||||
pub fn config(&self) -> &AppConfig {
|
pub fn config(&self) -> &AppConfig {
|
||||||
&self.config
|
&self.config
|
||||||
|
|||||||
@@ -177,8 +177,17 @@ impl GitProfile {
|
|||||||
|
|
||||||
if let Some(ref ssh) = self.ssh {
|
if let Some(ref ssh) = self.ssh {
|
||||||
if let Some(ref key_path) = ssh.private_key_path {
|
if let Some(ref key_path) = ssh.private_key_path {
|
||||||
config.set_str("core.sshCommand",
|
let path_str = key_path.display().to_string();
|
||||||
&format!("ssh -i {}", key_path.display()))?;
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
config.set_str("core.sshCommand",
|
||||||
|
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")))?;
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
config.set_str("core.sshCommand",
|
||||||
|
&format!("ssh -i '{}'", path_str))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +203,30 @@ impl GitProfile {
|
|||||||
|
|
||||||
if let Some(key) = self.signing_key() {
|
if let Some(key) = self.signing_key() {
|
||||||
config.set_str("user.signingkey", key)?;
|
config.set_str("user.signingkey", key)?;
|
||||||
|
|
||||||
|
if self.settings.auto_sign_commits {
|
||||||
|
config.set_bool("commit.gpgsign", true)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.settings.auto_sign_tags {
|
||||||
|
config.set_bool("tag.gpgsign", true)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref ssh) = self.ssh {
|
||||||
|
if let Some(ref key_path) = ssh.private_key_path {
|
||||||
|
let path_str = key_path.display().to_string();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
config.set_str("core.sshCommand",
|
||||||
|
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")))?;
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
config.set_str("core.sshCommand",
|
||||||
|
&format!("ssh -i '{}'", path_str))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -336,7 +369,15 @@ impl SshConfig {
|
|||||||
if let Some(ref cmd) = self.ssh_command {
|
if let Some(ref cmd) = self.ssh_command {
|
||||||
Some(cmd.clone())
|
Some(cmd.clone())
|
||||||
} else if let Some(ref key_path) = self.private_key_path {
|
} else if let Some(ref key_path) = self.private_key_path {
|
||||||
Some(format!("ssh -i '{}'", key_path.display()))
|
let path_str = key_path.display().to_string();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
Some(format!("ssh -i \"{}\"", path_str.replace('\\', "/")))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Some(format!("ssh -i '{}'", path_str))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -496,7 +537,11 @@ pub struct ConfigDifference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_gpg_program() -> String {
|
fn default_gpg_program() -> String {
|
||||||
"gpg".to_string()
|
if cfg!(target_os = "windows") {
|
||||||
|
"gpg.exe".to_string()
|
||||||
|
} else {
|
||||||
|
"gpg".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::config::{CommitFormat, LlmConfig, Language};
|
|||||||
use crate::git::{CommitInfo, GitRepo};
|
use crate::git::{CommitInfo, GitRepo};
|
||||||
use crate::llm::{GeneratedCommit, LlmClient};
|
use crate::llm::{GeneratedCommit, LlmClient};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::Utc;
|
|
||||||
|
|
||||||
/// Content generator using LLM
|
/// Content generator using LLM
|
||||||
pub struct ContentGenerator {
|
pub struct ContentGenerator {
|
||||||
@@ -114,8 +113,7 @@ impl ContentGenerator {
|
|||||||
format: CommitFormat,
|
format: CommitFormat,
|
||||||
language: Language,
|
language: Language,
|
||||||
) -> Result<GeneratedCommit> {
|
) -> Result<GeneratedCommit> {
|
||||||
use dialoguer::{Confirm, Select};
|
use dialoguer::Select;
|
||||||
use console::Term;
|
|
||||||
|
|
||||||
let diff = repo.get_staged_diff()?;
|
let diff = repo.get_staged_diff()?;
|
||||||
|
|
||||||
@@ -145,7 +143,6 @@ impl ContentGenerator {
|
|||||||
"✓ Accept and commit",
|
"✓ Accept and commit",
|
||||||
"🔄 Regenerate",
|
"🔄 Regenerate",
|
||||||
"✏️ Edit",
|
"✏️ Edit",
|
||||||
"📋 Copy to clipboard",
|
|
||||||
"❌ Cancel",
|
"❌ Cancel",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -165,24 +162,13 @@ impl ContentGenerator {
|
|||||||
let edited = crate::utils::editor::edit_content(&generated.to_conventional())?;
|
let edited = crate::utils::editor::edit_content(&generated.to_conventional())?;
|
||||||
generated = self.parse_edited_commit(&edited, format)?;
|
generated = self.parse_edited_commit(&edited, format)?;
|
||||||
}
|
}
|
||||||
3 => {
|
3 => anyhow::bail!("Cancelled by user"),
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
{
|
|
||||||
arboard::Clipboard::new()?.set_text(generated.to_conventional())?;
|
|
||||||
println!("Copied to clipboard!");
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "clipboard"))]
|
|
||||||
{
|
|
||||||
println!("Clipboard feature not enabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4 => anyhow::bail!("Cancelled by user"),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_edited_commit(&self, edited: &str, format: CommitFormat) -> Result<GeneratedCommit> {
|
fn parse_edited_commit(&self, edited: &str, _format: CommitFormat) -> Result<GeneratedCommit> {
|
||||||
let parsed = crate::git::commit::parse_commit_message(edited);
|
let parsed = crate::git::commit::parse_commit_message(edited);
|
||||||
|
|
||||||
Ok(GeneratedCommit {
|
Ok(GeneratedCommit {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{CommitInfo, GitRepo};
|
use super::{CommitInfo, GitRepo};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -232,8 +232,6 @@ impl ChangelogGenerator {
|
|||||||
let mut breaking = vec![];
|
let mut breaking = vec![];
|
||||||
|
|
||||||
for commit in commits {
|
for commit in commits {
|
||||||
let msg = commit.subject();
|
|
||||||
|
|
||||||
if commit.message.contains("BREAKING CHANGE") {
|
if commit.message.contains("BREAKING CHANGE") {
|
||||||
breaking.push(commit);
|
breaking.push(commit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::GitRepo;
|
use super::GitRepo;
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Result};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
/// Commit builder for creating commits
|
/// Commit builder for creating commits
|
||||||
@@ -47,6 +47,12 @@ impl CommitBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set scope (optional)
|
||||||
|
pub fn scope_opt(mut self, scope: Option<String>) -> Self {
|
||||||
|
self.scope = scope;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set description
|
/// Set description
|
||||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||||
self.description = Some(description.into());
|
self.description = Some(description.into());
|
||||||
@@ -59,6 +65,12 @@ impl CommitBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set body (optional)
|
||||||
|
pub fn body_opt(mut self, body: Option<String>) -> Self {
|
||||||
|
self.body = body;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set footer
|
/// Set footer
|
||||||
pub fn footer(mut self, footer: impl Into<String>) -> Self {
|
pub fn footer(mut self, footer: impl Into<String>) -> Self {
|
||||||
self.footer = Some(footer.into());
|
self.footer = Some(footer.into());
|
||||||
|
|||||||
296
src/git/mod.rs
296
src/git/mod.rs
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use git2::{Repository, Signature, StatusOptions, Config, Oid, ObjectType};
|
use git2::{Repository, Signature, StatusOptions, Config, Oid, ObjectType};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf, Component};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tempfile;
|
use tempfile;
|
||||||
|
|
||||||
@@ -8,11 +8,166 @@ pub mod changelog;
|
|||||||
pub mod commit;
|
pub mod commit;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
|
|
||||||
pub use changelog::ChangelogGenerator;
|
#[cfg(target_os = "windows")]
|
||||||
pub use commit::CommitBuilder;
|
use std::os::windows::ffi::OsStringExt;
|
||||||
pub use tag::TagBuilder;
|
|
||||||
|
fn normalize_path_for_git2(path: &Path) -> PathBuf {
|
||||||
|
let mut normalized = path.to_path_buf();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
if path_str.starts_with(r"\\?\") {
|
||||||
|
if let Some(stripped) = path_str.strip_prefix(r"\\?\") {
|
||||||
|
normalized = PathBuf::from(stripped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path_str.starts_with(r"\\?\UNC\") {
|
||||||
|
if let Some(stripped) = path_str.strip_prefix(r"\\?\UNC\") {
|
||||||
|
normalized = PathBuf::from(format!(r"\\{}", stripped));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_absolute_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
if path.is_absolute() {
|
||||||
|
return Ok(normalize_path_for_git2(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_dir = std::env::current_dir()
|
||||||
|
.with_context(|| "Failed to get current directory")?;
|
||||||
|
|
||||||
|
let absolute = current_dir.join(path);
|
||||||
|
Ok(normalize_path_for_git2(&absolute))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_path_without_canonicalize(path: &Path) -> PathBuf {
|
||||||
|
let mut components = Vec::new();
|
||||||
|
|
||||||
|
for component in path.components() {
|
||||||
|
match component {
|
||||||
|
Component::ParentDir => {
|
||||||
|
if !components.is_empty() && components.last() != Some(&Component::ParentDir) {
|
||||||
|
components.pop();
|
||||||
|
} else {
|
||||||
|
components.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::CurDir => {}
|
||||||
|
_ => components.push(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = PathBuf::new();
|
||||||
|
for component in components {
|
||||||
|
result.push(component.as_os_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_path_for_git2(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_open_repo_with_git2(path: &Path) -> Result<Repository> {
|
||||||
|
let normalized = normalize_path_for_git2(path);
|
||||||
|
|
||||||
|
let discover_opts = git2::RepositoryOpenFlags::empty();
|
||||||
|
let ceiling_dirs: [&str; 0] = [];
|
||||||
|
|
||||||
|
let repo = Repository::open_ext(&normalized, discover_opts, &ceiling_dirs)
|
||||||
|
.or_else(|_| Repository::discover(&normalized))
|
||||||
|
.or_else(|_| Repository::open(&normalized));
|
||||||
|
|
||||||
|
repo.map_err(|e| anyhow::anyhow!("git2 failed: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_open_repo_with_git_cli(path: &Path) -> Result<Repository> {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(&["rev-parse", "--show-toplevel"])
|
||||||
|
.current_dir(path)
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute git command")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("git CLI failed to find repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let git_root = stdout.trim();
|
||||||
|
|
||||||
|
if git_root.is_empty() {
|
||||||
|
bail!("git CLI returned empty path");
|
||||||
|
}
|
||||||
|
|
||||||
|
let git_root_path = PathBuf::from(git_root);
|
||||||
|
let normalized = normalize_path_for_git2(&git_root_path);
|
||||||
|
|
||||||
|
Repository::open(&normalized)
|
||||||
|
.with_context(|| format!("Failed to open repo from git CLI path: {:?}", normalized))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnose_repo_issue(path: &Path) -> String {
|
||||||
|
let mut issues = Vec::new();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
issues.push(format!("Path does not exist: {:?}", path));
|
||||||
|
} else if !path.is_dir() {
|
||||||
|
issues.push(format!("Path is not a directory: {:?}", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let git_dir = path.join(".git");
|
||||||
|
if git_dir.exists() {
|
||||||
|
if git_dir.is_dir() {
|
||||||
|
issues.push("Found .git directory".to_string());
|
||||||
|
let config_file = git_dir.join("config");
|
||||||
|
if config_file.exists() {
|
||||||
|
issues.push("Git config file exists".to_string());
|
||||||
|
} else {
|
||||||
|
issues.push("WARNING: Git config file missing".to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
issues.push("Found .git file (submodule or worktree)".to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
issues.push("No .git found in current directory".to_string());
|
||||||
|
|
||||||
|
let mut current = path;
|
||||||
|
let mut depth = 0;
|
||||||
|
while let Some(parent) = current.parent() {
|
||||||
|
depth += 1;
|
||||||
|
if depth > 20 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let parent_git = parent.join(".git");
|
||||||
|
if parent_git.exists() {
|
||||||
|
issues.push(format!("Found .git in parent directory: {:?}", parent));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
if path_str.starts_with(r"\\?\") {
|
||||||
|
issues.push("Path has Windows extended-length prefix (\\\\?\\)".to_string());
|
||||||
|
}
|
||||||
|
if path_str.contains('\\') && path_str.contains('/') {
|
||||||
|
issues.push("WARNING: Path has mixed path separators".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(current_dir) = std::env::current_dir() {
|
||||||
|
issues.push(format!("Current working directory: {:?}", current_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
issues.join("\n ")
|
||||||
|
}
|
||||||
|
|
||||||
/// Git repository wrapper with enhanced cross-platform support
|
|
||||||
pub struct GitRepo {
|
pub struct GitRepo {
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -20,54 +175,45 @@ pub struct GitRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GitRepo {
|
impl GitRepo {
|
||||||
/// Open a git repository
|
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
// Enhanced cross-platform path handling
|
let absolute_path = get_absolute_path(path)?;
|
||||||
let absolute_path = if let Ok(canonical) = path.canonicalize() {
|
let resolved_path = resolve_path_without_canonicalize(&absolute_path);
|
||||||
canonical
|
|
||||||
} else {
|
|
||||||
// Fallback: convert to absolute path without canonicalization
|
|
||||||
if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::current_dir()?.join(path)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try multiple git repository discovery strategies for cross-platform compatibility
|
let repo = try_open_repo_with_git2(&resolved_path)
|
||||||
let repo = Repository::discover(&absolute_path)
|
.or_else(|git2_err| {
|
||||||
.or_else(|discover_err| {
|
try_open_repo_with_git_cli(&resolved_path)
|
||||||
// Try direct open as fallback
|
.map_err(|cli_err| {
|
||||||
Repository::open(&absolute_path).map_err(|open_err| {
|
let diagnosis = diagnose_repo_issue(&resolved_path);
|
||||||
// Provide detailed error information for debugging
|
anyhow::anyhow!(
|
||||||
anyhow::anyhow!(
|
"Failed to open git repository:\n\
|
||||||
"Git repository discovery failed:\n\
|
\n\
|
||||||
Discovery error: {}\n\
|
=== git2 Error ===\n {}\n\
|
||||||
Direct open error: {}\n\
|
\n\
|
||||||
Path attempted: {:?}\n\
|
=== git CLI Error ===\n {}\n\
|
||||||
Current directory: {:?}",
|
\n\
|
||||||
discover_err, open_err, absolute_path, std::env::current_dir()
|
=== Diagnosis ===\n {}\n\
|
||||||
)
|
\n\
|
||||||
})
|
=== Suggestions ===\n\
|
||||||
})
|
1. Ensure you are inside a git repository\n\
|
||||||
.with_context(|| {
|
2. Run: git status (to verify git works)\n\
|
||||||
format!(
|
3. Run: git config --global --add safe.directory \"*\"\n\
|
||||||
"Failed to open git repository at '{:?}'. Please ensure:\n\
|
4. Check file permissions",
|
||||||
1. The directory contains a valid '.git' folder\n\
|
git2_err, cli_err, diagnosis
|
||||||
2. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
|
)
|
||||||
3. You have proper permissions to access the repository",
|
})
|
||||||
absolute_path,
|
|
||||||
absolute_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let repo_path = repo.workdir()
|
||||||
|
.map(|p| p.to_path_buf())
|
||||||
|
.unwrap_or_else(|| resolved_path.clone());
|
||||||
|
|
||||||
let config = repo.config().ok();
|
let config = repo.config().ok();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
repo,
|
repo,
|
||||||
path: absolute_path,
|
path: normalize_path_for_git2(&repo_path),
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -722,20 +868,28 @@ impl StatusSummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find git repository starting from path and walking up
|
|
||||||
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
||||||
let start_path = start_path.as_ref();
|
let start_path = start_path.as_ref();
|
||||||
|
|
||||||
// Try the starting path first
|
let absolute_start = get_absolute_path(start_path)?;
|
||||||
if let Ok(repo) = GitRepo::open(start_path) {
|
let resolved_start = resolve_path_without_canonicalize(&absolute_start);
|
||||||
|
|
||||||
|
if let Ok(repo) = GitRepo::open(&resolved_start) {
|
||||||
return Ok(repo);
|
return Ok(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk up the directory tree to find a git repository
|
let mut current = resolved_start.as_path();
|
||||||
let mut current = start_path;
|
|
||||||
let mut attempted_paths = vec![current.to_string_lossy().to_string()];
|
let mut attempted_paths = vec![current.to_string_lossy().to_string()];
|
||||||
|
|
||||||
|
let max_depth = 50;
|
||||||
|
let mut depth = 0;
|
||||||
|
|
||||||
while let Some(parent) = current.parent() {
|
while let Some(parent) = current.parent() {
|
||||||
|
depth += 1;
|
||||||
|
if depth > max_depth {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
attempted_paths.push(parent.to_string_lossy().to_string());
|
attempted_paths.push(parent.to_string_lossy().to_string());
|
||||||
|
|
||||||
if let Ok(repo) = GitRepo::open(parent) {
|
if let Ok(repo) = GitRepo::open(parent) {
|
||||||
@@ -744,18 +898,44 @@ pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
|||||||
current = parent;
|
current = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide detailed error information for debugging
|
if let Ok(output) = std::process::Command::new("git")
|
||||||
|
.args(&["rev-parse", "--show-toplevel"])
|
||||||
|
.current_dir(&resolved_start)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let git_root = stdout.trim();
|
||||||
|
if !git_root.is_empty() {
|
||||||
|
if let Ok(repo) = GitRepo::open(git_root) {
|
||||||
|
return Ok(repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagnosis = diagnose_repo_issue(&resolved_start);
|
||||||
|
|
||||||
bail!(
|
bail!(
|
||||||
"No git repository found starting from {:?}.\n\
|
"No git repository found.\n\
|
||||||
Paths attempted:\n {}\n\
|
\n\
|
||||||
Current directory: {:?}\n\
|
=== Starting Path ===\n {:?}\n\
|
||||||
Please ensure:\n\
|
\n\
|
||||||
1. You are in a git repository or its subdirectory\n\
|
=== Paths Attempted ===\n {}\n\
|
||||||
2. The repository has a valid .git folder\n\
|
\n\
|
||||||
3. You have proper permissions to access the repository",
|
=== Current Directory ===\n {:?}\n\
|
||||||
start_path,
|
\n\
|
||||||
|
=== Diagnosis ===\n {}\n\
|
||||||
|
\n\
|
||||||
|
=== Suggestions ===\n\
|
||||||
|
1. Ensure you are inside a git repository (run: git status)\n\
|
||||||
|
2. Initialize a new repo: git init\n\
|
||||||
|
3. Clone an existing repo: git clone <url>\n\
|
||||||
|
4. Check if .git directory exists and is accessible",
|
||||||
|
resolved_start,
|
||||||
attempted_paths.join("\n "),
|
attempted_paths.join("\n "),
|
||||||
std::env::current_dir()
|
std::env::current_dir().unwrap_or_default(),
|
||||||
|
diagnosis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::GitRepo;
|
use super::GitRepo;
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Result};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
|
||||||
/// Tag builder for creating tags
|
/// Tag builder for creating tags
|
||||||
@@ -281,8 +281,9 @@ pub fn delete_tag(repo: &GitRepo, name: &str, remote: Option<&str>) -> Result<()
|
|||||||
if let Some(remote) = remote {
|
if let Some(remote) = remote {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
let refspec = format!(":refs/tags/{}", name);
|
||||||
let output = Command::new("git")
|
let output = Command::new("git")
|
||||||
.args(&["push", remote, ":refs/tags/{}"])
|
.args(&["push", remote, &refspec])
|
||||||
.current_dir(repo.path())
|
.current_dir(repo.path())
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ pub mod messages;
|
|||||||
pub mod translator;
|
pub mod translator;
|
||||||
|
|
||||||
pub use messages::Messages;
|
pub use messages::Messages;
|
||||||
pub use translator::Translator;
|
|
||||||
pub use translator::translate_commit_type;
|
|
||||||
pub use translator::translate_changelog_category;
|
pub use translator::translate_changelog_category;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use crate::config::Language;
|
use crate::config::Language;
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ pub const OPENROUTER_MODELS: &[&str] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Check if a model name is valid
|
/// Check if a model name is valid
|
||||||
pub fn is_valid_model(model: &str) -> bool {
|
pub fn is_valid_model(_model: &str) -> bool {
|
||||||
// Since OpenRouter supports many models, we'll allow any model name
|
// Since OpenRouter supports many models, we'll allow any model name
|
||||||
// but provide some popular ones as suggestions
|
// but provide some popular ones as suggestions
|
||||||
true
|
true
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
@@ -74,7 +75,6 @@ enum Commands {
|
|||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Initialize logging
|
|
||||||
let log_level = match cli.verbose {
|
let log_level = match cli.verbose {
|
||||||
0 => "warn",
|
0 => "warn",
|
||||||
1 => "info",
|
1 => "info",
|
||||||
@@ -89,13 +89,14 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
debug!("Starting quicommit v{}", env!("CARGO_PKG_VERSION"));
|
debug!("Starting quicommit v{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
// Execute command
|
let config_path: Option<PathBuf> = cli.config.map(PathBuf::from);
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Init(cmd) => cmd.execute().await,
|
Commands::Init(cmd) => cmd.execute(config_path).await,
|
||||||
Commands::Commit(cmd) => cmd.execute().await,
|
Commands::Commit(cmd) => cmd.execute(config_path).await,
|
||||||
Commands::Tag(cmd) => cmd.execute().await,
|
Commands::Tag(cmd) => cmd.execute(config_path).await,
|
||||||
Commands::Changelog(cmd) => cmd.execute().await,
|
Commands::Changelog(cmd) => cmd.execute(config_path).await,
|
||||||
Commands::Profile(cmd) => cmd.execute().await,
|
Commands::Profile(cmd) => cmd.execute(config_path).await,
|
||||||
Commands::Config(cmd) => cmd.execute().await,
|
Commands::Config(cmd) => cmd.execute(config_path).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,22 @@ pub fn get_editor() -> String {
|
|||||||
.or_else(|_| std::env::var("VISUAL"))
|
.or_else(|_| std::env::var("VISUAL"))
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
|
if let Ok(code) = which::which("code") {
|
||||||
|
return "code --wait".to_string();
|
||||||
|
}
|
||||||
|
if let Ok(notepad) = which::which("notepad") {
|
||||||
|
return "notepad".to_string();
|
||||||
|
}
|
||||||
"notepad".to_string()
|
"notepad".to_string()
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
if which::which("code").is_ok() {
|
||||||
|
return "code --wait".to_string();
|
||||||
|
}
|
||||||
|
"vi".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
if which::which("nano").is_ok() {
|
||||||
|
return "nano".to_string();
|
||||||
|
}
|
||||||
"vi".to_string()
|
"vi".to_string()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use chrono::{DateTime, Local, Utc};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
/// Format commit message with conventional commit format
|
/// Format commit message with conventional commit format
|
||||||
@@ -12,7 +11,6 @@ pub fn format_conventional_commit(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
|
|
||||||
// Type and scope
|
|
||||||
message.push_str(commit_type);
|
message.push_str(commit_type);
|
||||||
if let Some(s) = scope {
|
if let Some(s) = scope {
|
||||||
message.push_str(&format!("({})", s));
|
message.push_str(&format!("({})", s));
|
||||||
@@ -22,12 +20,10 @@ pub fn format_conventional_commit(
|
|||||||
}
|
}
|
||||||
message.push_str(&format!(": {}", description));
|
message.push_str(&format!(": {}", description));
|
||||||
|
|
||||||
// Body
|
|
||||||
if let Some(b) = body {
|
if let Some(b) = body {
|
||||||
message.push_str(&format!("\n\n{}", b));
|
message.push_str(&format!("\n\n{}", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
|
||||||
if let Some(f) = footer {
|
if let Some(f) = footer {
|
||||||
message.push_str(&format!("\n\n{}", f));
|
message.push_str(&format!("\n\n{}", f));
|
||||||
}
|
}
|
||||||
@@ -46,26 +42,22 @@ pub fn format_commitlint_commit(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
|
|
||||||
// Header
|
|
||||||
message.push_str(commit_type);
|
message.push_str(commit_type);
|
||||||
if let Some(s) = scope {
|
if let Some(s) = scope {
|
||||||
message.push_str(&format!("({})", s));
|
message.push_str(&format!("({})", s));
|
||||||
}
|
}
|
||||||
message.push_str(&format!(": {}", subject));
|
message.push_str(&format!(": {}", subject));
|
||||||
|
|
||||||
// References
|
|
||||||
if let Some(refs) = references {
|
if let Some(refs) = references {
|
||||||
for reference in refs {
|
for reference in refs {
|
||||||
message.push_str(&format!(" #{}", reference));
|
message.push_str(&format!(" #{}", reference));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body
|
|
||||||
if let Some(b) = body {
|
if let Some(b) = body {
|
||||||
message.push_str(&format!("\n\n{}", b));
|
message.push_str(&format!("\n\n{}", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
|
||||||
if let Some(f) = footer {
|
if let Some(f) = footer {
|
||||||
message.push_str(&format!("\n\n{}", f));
|
message.push_str(&format!("\n\n{}", f));
|
||||||
}
|
}
|
||||||
@@ -73,38 +65,11 @@ pub fn format_commitlint_commit(
|
|||||||
message
|
message
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format date for commit message
|
|
||||||
pub fn format_commit_date(date: &DateTime<Local>) -> String {
|
|
||||||
date.format("%Y-%m-%d %H:%M:%S").to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format date for changelog
|
|
||||||
pub fn format_changelog_date(date: &DateTime<Utc>) -> String {
|
|
||||||
date.format("%Y-%m-%d").to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format tag name with version
|
|
||||||
pub fn format_tag_name(version: &str, prefix: Option<&str>) -> String {
|
|
||||||
match prefix {
|
|
||||||
Some(p) => format!("{}{}", p, version),
|
|
||||||
None => version.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap text at specified width
|
/// Wrap text at specified width
|
||||||
pub fn wrap_text(text: &str, width: usize) -> String {
|
pub fn wrap_text(text: &str, width: usize) -> String {
|
||||||
textwrap::fill(text, width)
|
textwrap::fill(text, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Truncate text with ellipsis
|
|
||||||
pub fn truncate(text: &str, max_len: usize) -> String {
|
|
||||||
if text.len() <= max_len {
|
|
||||||
text.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}...", &text[..max_len.saturating_sub(3)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clean commit message (remove comments, extra whitespace)
|
/// Clean commit message (remove comments, extra whitespace)
|
||||||
pub fn clean_message(message: &str) -> String {
|
pub fn clean_message(message: &str) -> String {
|
||||||
let comment_regex = Regex::new(r"^#.*$").unwrap();
|
let comment_regex = Regex::new(r"^#.*$").unwrap();
|
||||||
@@ -118,44 +83,6 @@ pub fn clean_message(message: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format list as markdown bullet points
|
|
||||||
pub fn format_markdown_list(items: &[String]) -> String {
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.map(|item| format!("- {}", item))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format changelog section
|
|
||||||
pub fn format_changelog_section(
|
|
||||||
version: &str,
|
|
||||||
date: &str,
|
|
||||||
changes: &[(String, Vec<String>)],
|
|
||||||
) -> String {
|
|
||||||
let mut section = format!("## [{}] - {}\n\n", version, date);
|
|
||||||
|
|
||||||
for (category, items) in changes {
|
|
||||||
if !items.is_empty() {
|
|
||||||
section.push_str(&format!("### {}\n\n", category));
|
|
||||||
for item in items {
|
|
||||||
section.push_str(&format!("- {}\n", item));
|
|
||||||
}
|
|
||||||
section.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format git config key
|
|
||||||
pub fn format_git_config_key(section: &str, subsection: Option<&str>, key: &str) -> String {
|
|
||||||
match subsection {
|
|
||||||
Some(sub) => format!("{}.{}.{}", section, sub, key),
|
|
||||||
None => format!("{}.{}", section, key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -189,10 +116,4 @@ mod tests {
|
|||||||
|
|
||||||
assert!(msg.starts_with("feat!: change API response format"));
|
assert!(msg.starts_with("feat!: change API response format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_truncate() {
|
|
||||||
assert_eq!(truncate("hello", 10), "hello");
|
|
||||||
assert_eq!(truncate("hello world", 8), "hello...");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ lazy_static! {
|
|||||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
/// Regex for SSH key validation (basic)
|
|
||||||
static ref SSH_KEY_REGEX: Regex = Regex::new(
|
|
||||||
r"^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+[A-Za-z0-9+/]+={0,2}\s+.*$"
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
/// Regex for GPG key ID validation
|
/// Regex for GPG key ID validation
|
||||||
static ref GPG_KEY_ID_REGEX: Regex = Regex::new(
|
static ref GPG_KEY_ID_REGEX: Regex = Regex::new(
|
||||||
r"^[A-F0-9]{16,40}$"
|
r"^[A-F0-9]{16,40}$"
|
||||||
@@ -81,7 +76,6 @@ pub fn validate_conventional_commit(message: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check description length (max 100 chars for first line)
|
|
||||||
if first_line.len() > 100 {
|
if first_line.len() > 100 {
|
||||||
bail!("Commit subject too long (max 100 characters)");
|
bail!("Commit subject too long (max 100 characters)");
|
||||||
}
|
}
|
||||||
@@ -93,7 +87,6 @@ pub fn validate_conventional_commit(message: &str) -> Result<()> {
|
|||||||
pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
||||||
let first_line = message.lines().next().unwrap_or("");
|
let first_line = message.lines().next().unwrap_or("");
|
||||||
|
|
||||||
// Commitlint is more lenient but still requires type prefix
|
|
||||||
let parts: Vec<&str> = first_line.splitn(2, ':').collect();
|
let parts: Vec<&str> = first_line.splitn(2, ':').collect();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
bail!("Invalid commit format. Expected: <type>[optional scope]: <subject>");
|
bail!("Invalid commit format. Expected: <type>[optional scope]: <subject>");
|
||||||
@@ -102,7 +95,6 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
|||||||
let type_part = parts[0];
|
let type_part = parts[0];
|
||||||
let subject = parts[1].trim();
|
let subject = parts[1].trim();
|
||||||
|
|
||||||
// Extract type (handle scope and breaking indicator)
|
|
||||||
let commit_type = type_part
|
let commit_type = type_part
|
||||||
.split('(')
|
.split('(')
|
||||||
.next()
|
.next()
|
||||||
@@ -117,7 +109,6 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate subject
|
|
||||||
if subject.is_empty() {
|
if subject.is_empty() {
|
||||||
bail!("Commit subject cannot be empty");
|
bail!("Commit subject cannot be empty");
|
||||||
}
|
}
|
||||||
@@ -130,12 +121,10 @@ pub fn validate_commitlint_commit(message: &str) -> Result<()> {
|
|||||||
bail!("Commit subject too long (max 100 characters)");
|
bail!("Commit subject too long (max 100 characters)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject should not start with uppercase
|
|
||||||
if subject.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
|
if subject.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
|
||||||
bail!("Commit subject should not start with uppercase letter");
|
bail!("Commit subject should not start with uppercase letter");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject should not end with period
|
|
||||||
if subject.ends_with('.') {
|
if subject.ends_with('.') {
|
||||||
bail!("Commit subject should not end with a period");
|
bail!("Commit subject should not end with a period");
|
||||||
}
|
}
|
||||||
@@ -179,15 +168,6 @@ pub fn validate_email(email: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate SSH key format
|
|
||||||
pub fn validate_ssh_key(key: &str) -> Result<()> {
|
|
||||||
if !SSH_KEY_REGEX.is_match(key.trim()) {
|
|
||||||
bail!("Invalid SSH public key format");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate GPG key ID
|
/// Validate GPG key ID
|
||||||
pub fn validate_gpg_key_id(key_id: &str) -> Result<()> {
|
pub fn validate_gpg_key_id(key_id: &str) -> Result<()> {
|
||||||
if !GPG_KEY_ID_REGEX.is_match(key_id) {
|
if !GPG_KEY_ID_REGEX.is_match(key_id) {
|
||||||
|
|||||||
@@ -1,64 +1,642 @@
|
|||||||
use assert_cmd::Command;
|
use assert_cmd::Command;
|
||||||
use predicates::prelude::*;
|
use predicates::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[test]
|
fn create_git_repo(dir: &PathBuf) -> std::process::Output {
|
||||||
fn test_cli_help() {
|
std::process::Command::new("git")
|
||||||
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
.args(&["init"])
|
||||||
cmd.arg("--help");
|
.current_dir(dir)
|
||||||
cmd.assert()
|
.output()
|
||||||
.success()
|
.expect("Failed to init git repo")
|
||||||
.stdout(predicate::str::contains("QuiCommit"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn configure_git_user(dir: &PathBuf) {
|
||||||
fn test_version() {
|
std::process::Command::new("git")
|
||||||
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
.args(&["config", "user.name", "Test User"])
|
||||||
cmd.arg("--version");
|
.current_dir(dir)
|
||||||
cmd.assert()
|
.output()
|
||||||
.success()
|
.expect("Failed to configure git user name");
|
||||||
.stdout(predicate::str::contains("0.1.0"));
|
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(&["config", "user.email", "test@example.com"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to configure git user email");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn create_test_file(dir: &PathBuf, name: &str, content: &str) {
|
||||||
fn test_config_show() {
|
let file_path = dir.join(name);
|
||||||
let temp_dir = TempDir::new().unwrap();
|
fs::write(&file_path, content).expect("Failed to create test file");
|
||||||
let config_dir = temp_dir.path().join("config");
|
|
||||||
fs::create_dir(&config_dir).unwrap();
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
|
||||||
cmd.env("QUICOMMIT_CONFIG", config_dir.join("config.toml"))
|
|
||||||
.arg("config")
|
|
||||||
.arg("show");
|
|
||||||
|
|
||||||
cmd.assert().success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn stage_file(dir: &PathBuf, name: &str) {
|
||||||
fn test_profile_list_empty() {
|
std::process::Command::new("git")
|
||||||
let temp_dir = TempDir::new().unwrap();
|
.args(&["add", name])
|
||||||
|
.current_dir(dir)
|
||||||
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
.output()
|
||||||
cmd.env("QUICOMMIT_CONFIG", temp_dir.path().join("config.toml"))
|
.expect("Failed to stage file");
|
||||||
.arg("profile")
|
|
||||||
.arg("list");
|
|
||||||
|
|
||||||
cmd.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(predicate::str::contains("No profiles configured"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn create_commit(dir: &PathBuf, message: &str) {
|
||||||
fn test_init_quick() {
|
std::process::Command::new("git")
|
||||||
let temp_dir = TempDir::new().unwrap();
|
.args(&["commit", "-m", message])
|
||||||
|
.current_dir(dir)
|
||||||
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
.output()
|
||||||
cmd.env("QUICOMMIT_CONFIG", temp_dir.path().join("config.toml"))
|
.expect("Failed to create commit");
|
||||||
.arg("init")
|
}
|
||||||
.arg("--yes");
|
|
||||||
|
mod cli_basic {
|
||||||
cmd.assert()
|
use super::*;
|
||||||
.success()
|
|
||||||
.stdout(predicate::str::contains("initialized successfully"));
|
#[test]
|
||||||
|
fn test_help() {
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.arg("--help");
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("QuiCommit"))
|
||||||
|
.stdout(predicate::str::contains("AI-powered Git assistant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version() {
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.arg("--version");
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("quicommit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_args_shows_help() {
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("Usage:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verbose_flag() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["-vv", "init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod init_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_quick() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("initialized successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_creates_config_file() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
assert!(config_path.exists(), "Config file should be created");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_in_git_repo() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let config_path = repo_path.join("test_config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_reset() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--reset", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("initialized successfully"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod profile_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_profile_list_empty() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["profile", "list", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("No profiles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_profile_list_with_profile() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["profile", "list", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("default"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod config_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_show() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "show", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Configuration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_path() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "path", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("config.toml"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod commit_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_no_repo() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(temp_dir.path());
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("git").or(predicate::str::contains("repository")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_no_changes() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--manual", "-m", "test: empty", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Dry run"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_with_staged_changes() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "Hello, World!");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--manual", "-m", "test: add test file", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Dry run"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_date_mode() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "daily.txt", "Daily update");
|
||||||
|
stage_file(&repo_path, "daily.txt");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--date", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Dry run"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tag_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tag_no_repo() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["tag", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(temp_dir.path());
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("git").or(predicate::str::contains("repository")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tag_list_empty() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
create_commit(&repo_path, "feat: initial commit");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["tag", "--name", "v0.1.0", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("v0.1.0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod changelog_command {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_changelog_init() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
let changelog_path = repo_path.join("CHANGELOG.md");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["changelog", "--init", "--output", changelog_path.to_str().unwrap(), "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
assert!(changelog_path.exists(), "Changelog file should be created");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_changelog_dry_run() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
create_commit(&repo_path, "feat: add feature");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["changelog", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cross_platform {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_handling_windows_style() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("subdir").join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
assert!(config_path.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_with_spaces_in_path() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let space_dir = temp_dir.path().join("path with spaces");
|
||||||
|
fs::create_dir_all(&space_dir).unwrap();
|
||||||
|
let config_path = space_dir.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
assert!(config_path.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_with_unicode_path() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let unicode_dir = temp_dir.path().join("路径测试");
|
||||||
|
fs::create_dir_all(&unicode_dir).unwrap();
|
||||||
|
let config_path = unicode_dir.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert().success();
|
||||||
|
assert!(config_path.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod git_operations {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_git_repo_detection() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let git_dir = repo_path.join(".git");
|
||||||
|
assert!(git_dir.exists(), ".git directory should exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_git_status_clean() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(&["status", "--porcelain"])
|
||||||
|
.current_dir(&repo_path)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run git status");
|
||||||
|
|
||||||
|
assert!(output.status.success());
|
||||||
|
assert!(String::from_utf8_lossy(&output.stdout).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_git_commit_creation() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
create_commit(&repo_path, "feat: initial commit");
|
||||||
|
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(&["log", "--oneline"])
|
||||||
|
.current_dir(&repo_path)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run git log");
|
||||||
|
|
||||||
|
assert!(output.status.success());
|
||||||
|
let log = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(log.contains("initial commit"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod validators {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_message_validation() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--manual", "-m", "invalid commit message without type", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("Invalid").or(predicate::str::contains("format")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_conventional_commit() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["commit", "--manual", "-m", "feat: add new feature", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Dry run"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod subcommands {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commit_alias() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let repo_path = temp_dir.path().to_path_buf();
|
||||||
|
create_git_repo(&repo_path);
|
||||||
|
configure_git_user(&repo_path);
|
||||||
|
|
||||||
|
create_test_file(&repo_path, "test.txt", "content");
|
||||||
|
stage_file(&repo_path, "test.txt");
|
||||||
|
|
||||||
|
let config_path = repo_path.join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["c", "--manual", "-m", "fix: test", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()])
|
||||||
|
.current_dir(&repo_path);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Dry run"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_alias() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["i", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("initialized successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_profile_alias() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["p", "list", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("default"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user