feat(commit): 添加提交消息模板支持

- 移除 config 命令中未使用的 List 子命令及相关显示字段
- 统一 ChangelogCommand 和 CommitCommand 的 ContentGenerator 初始化方式
This commit is contained in:
2026-06-03 15:20:50 +08:00
parent 459670f363
commit 14ebb6857a
17 changed files with 494 additions and 653 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "quicommit" name = "quicommit"
version = "0.3.1" version = "0.3.2"
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)"

View File

@@ -88,10 +88,10 @@
提升 AI 生成提交信息、标签说明和变更日志时的用户体验。 提升 AI 生成提交信息、标签说明和变更日志时的用户体验。
- [x] **流式输出与实时反馈** - [ ] **流式输出与实时反馈**
- 支持 SSEServer-Sent Events流式生成 - [x] 支持 SSEServer-Sent Events流式生成
- 终端打字机效果实时显示生成内容 - [ ]终端打字机效果实时显示生成内容
- 流式生成过程中支持 `Ctrl+C` 中断 - [ ]流式生成过程中支持 `Ctrl+C` 中断
- [ ] **生成质量提升** - [ ] **生成质量提升**
- 基于 commitlint 规则的后校验与自动修正 - 基于 commitlint 规则的后校验与自动修正

View File

@@ -332,58 +332,25 @@ use_agent = true
[llm] [llm]
provider = "ollama" provider = "ollama"
model = "llama2"
# base_url = "http://localhost:11434"
max_tokens = 500 max_tokens = 500
temperature = 0.7 temperature = 0.7
timeout = 30 timeout = 30
api_key_storage = "keyring"
[llm.ollama] thinking_enabled = false
url = "http://localhost:11434"
model = "llama2"
[llm.openai]
model = "gpt-4"
base_url = "https://api.openai.com/v1"
[llm.anthropic]
model = "claude-3-sonnet-20240229"
[llm.kimi]
model = "moonshot-v1-8k"
[llm.deepseek]
model = "deepseek-chat"
[llm.openrouter]
model = "openai/gpt-4"
[commit] [commit]
format = "conventional" format = "conventional"
auto_generate = true auto_generate = true
allow_empty = false
gpg_sign = false
max_subject_length = 100
require_scope = false
require_body = false
body_required_types = ["feat", "fix"]
[tag] [tag]
version_prefix = "v" version_prefix = "v"
auto_generate = true auto_generate = true
gpg_sign = false
include_changelog = true
[changelog] [changelog]
path = "CHANGELOG.md" path = "CHANGELOG.md"
auto_generate = true auto_generate = true
format = "keep-a-changelog"
include_hashes = false
include_authors = false
group_by_type = true
[theme]
colors = true
icons = true
date_format = "%Y-%m-%d"
[repo_profiles] [repo_profiles]
"/path/to/work/project" = "work" "/path/to/work/project" = "work"
@@ -402,9 +369,6 @@ date_format = "%Y-%m-%d"
```bash ```bash
# View current configuration # View current configuration
quicommit config list
# Show configuration details
quicommit config show quicommit config show
# Edit configuration file # Edit configuration file

View File

@@ -46,61 +46,35 @@ use_agent = true
# LLM Configuration # LLM Configuration
[llm] [llm]
# Provider: ollama, openai, or anthropic # Provider: ollama, openai, anthropic, kimi, deepseek, openrouter
provider = "ollama" provider = "ollama"
# Model name (provider-appropriate)
model = "llama2"
# API base URL (optional, provider default will be used if not set)
# base_url = "http://localhost:11434"
max_tokens = 500 max_tokens = 500
temperature = 0.7 temperature = 0.7
timeout = 30 timeout = 30
# API key storage: keyring, config, environment
# Ollama settings (local LLM) api_key_storage = "keyring"
[llm.ollama] # Enable thinking/reasoning mode (deepseek, kimi, anthropic)
url = "http://localhost:11434" thinking_enabled = false
model = "llama2"
# OpenAI settings
[llm.openai]
# api_key = "sk-..." # Set via: quicommit config set-openai-key
model = "gpt-4"
base_url = "https://api.openai.com/v1"
# Anthropic settings
[llm.anthropic]
# api_key = "sk-ant-..." # Set via: quicommit config set-anthropic-key
model = "claude-3-sonnet-20240229"
# Commit settings # Commit settings
[commit] [commit]
# Format: conventional or commitlint # Format: conventional or commitlint
format = "conventional" format = "conventional"
auto_generate = true auto_generate = true
allow_empty = false
gpg_sign = false
max_subject_length = 100
require_scope = false
require_body = false
body_required_types = ["feat", "fix"]
# Tag settings # Tag settings
[tag] [tag]
version_prefix = "v" version_prefix = "v"
auto_generate = true auto_generate = true
gpg_sign = false
include_changelog = true
# Changelog settings # Changelog settings
[changelog] [changelog]
path = "CHANGELOG.md" path = "CHANGELOG.md"
auto_generate = true auto_generate = true
format = "keep-a-changelog" # or "github-releases"
include_hashes = false
include_authors = false
group_by_type = true
# Theme settings
[theme]
colors = true
icons = true
date_format = "%Y-%m-%d"
# Repository-specific profile mappings # Repository-specific profile mappings
# [repo_profiles] # [repo_profiles]

View File

@@ -331,58 +331,25 @@ use_agent = true
[llm] [llm]
provider = "ollama" provider = "ollama"
model = "llama2"
# base_url = "http://localhost:11434"
max_tokens = 500 max_tokens = 500
temperature = 0.7 temperature = 0.7
timeout = 30 timeout = 30
api_key_storage = "keyring"
[llm.ollama] thinking_enabled = false
url = "http://localhost:11434"
model = "llama2"
[llm.openai]
model = "gpt-4"
base_url = "https://api.openai.com/v1"
[llm.anthropic]
model = "claude-3-sonnet-20240229"
[llm.kimi]
model = "moonshot-v1-8k"
[llm.deepseek]
model = "deepseek-chat"
[llm.openrouter]
model = "openai/gpt-4"
[commit] [commit]
format = "conventional" format = "conventional"
auto_generate = true auto_generate = true
allow_empty = false
gpg_sign = false
max_subject_length = 100
require_scope = false
require_body = false
body_required_types = ["feat", "fix"]
[tag] [tag]
version_prefix = "v" version_prefix = "v"
auto_generate = true auto_generate = true
gpg_sign = false
include_changelog = true
[changelog] [changelog]
path = "CHANGELOG.md" path = "CHANGELOG.md"
auto_generate = true auto_generate = true
format = "keep-a-changelog"
include_hashes = false
include_authors = false
group_by_type = true
[theme]
colors = true
icons = true
date_format = "%Y-%m-%d"
[repo_profiles] [repo_profiles]
"/path/to/work/project" = "work" "/path/to/work/project" = "work"
@@ -401,9 +368,6 @@ date_format = "%Y-%m-%d"
```bash ```bash
# 查看当前配置 # 查看当前配置
quicommit config list
# 显示配置详情
quicommit config show quicommit config show
# 编辑配置文件 # 编辑配置文件

View File

@@ -217,7 +217,7 @@ impl ChangelogCommand {
println!("{}", messages.ai_generating_changelog()); println!("{}", messages.ai_generating_changelog());
let generator = ContentGenerator::new_with_think(&manager, self.think).await?; let generator = ContentGenerator::new_with_think(&manager, self.think, None).await?;
generator generator
.generate_changelog_entry(version, commits, language) .generate_changelog_entry(version, commits, language)
.await .await

View File

@@ -280,7 +280,11 @@ impl CommitCommand {
) -> Result<String> { ) -> Result<String> {
let manager = ConfigManager::new()?; let manager = ConfigManager::new()?;
let generator = ContentGenerator::new_with_think(&manager, self.think) let template = manager
.default_profile()
.and_then(|p| p.commit_template().map(|t| t.to_string()));
let generator = ContentGenerator::new_with_think(&manager, self.think, template)
.await .await
.context("Failed to initialize LLM. Use --manual for manual commit.")?; .context("Failed to initialize LLM. Use --manual for manual commit.")?;

View File

@@ -37,9 +37,6 @@ enum ConfigSubcommand {
/// Show current configuration /// Show current configuration
Show, Show,
/// List all configuration information (with masked API keys)
List,
/// Edit configuration file /// Edit configuration file
Edit, Edit,
@@ -167,7 +164,6 @@ impl ConfigCommand {
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> { pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
match &self.command { match &self.command {
Some(ConfigSubcommand::Show) => self.show_config(&config_path).await, Some(ConfigSubcommand::Show) => self.show_config(&config_path).await,
Some(ConfigSubcommand::List) => self.list_config(&config_path).await,
Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await, Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await,
Some(ConfigSubcommand::Set { key, value }) => { Some(ConfigSubcommand::Set { key, value }) => {
self.set_value(key, value, &config_path).await self.set_value(key, value, &config_path).await
@@ -311,15 +307,6 @@ impl ConfigCommand {
"no".red() "no".red()
} }
); );
println!(
" GPG sign: {}",
if config.commit.gpg_sign {
"yes".green()
} else {
"no".red()
}
);
println!(" Max subject length: {}", config.commit.max_subject_length);
println!("\n{}", "Tag Configuration:".bold()); println!("\n{}", "Tag Configuration:".bold());
println!(" Version prefix: '{}'", config.tag.version_prefix); println!(" Version prefix: '{}'", config.tag.version_prefix);
@@ -331,22 +318,6 @@ impl ConfigCommand {
"no".red() "no".red()
} }
); );
println!(
" GPG sign: {}",
if config.tag.gpg_sign {
"yes".green()
} else {
"no".red()
}
);
println!(
" Include changelog: {}",
if config.tag.include_changelog {
"yes".green()
} else {
"no".red()
}
);
println!("\n{}", "Language Configuration:".bold()); println!("\n{}", "Language Configuration:".bold());
let language = manager.get_language().unwrap_or(Language::English); let language = manager.get_language().unwrap_or(Language::English);
@@ -378,243 +349,17 @@ impl ConfigCommand {
"no".red() "no".red()
} }
); );
println!(
" Include hashes: {}",
if config.changelog.include_hashes {
"yes".green()
} else {
"no".red()
}
);
println!(
" Include authors: {}",
if config.changelog.include_authors {
"yes".green()
} else {
"no".red()
}
);
println!(
" Group by type: {}",
if config.changelog.group_by_type {
"yes".green()
} else {
"no".red()
}
);
Ok(()) println!("\n{}", "Security:".bold());
} println!(" Repository mappings: {} mapping(s)", config.repo_profiles.len());
async fn list_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
println!("{}", "\nQuiCommit Configuration".bold());
println!("{}", "".repeat(80));
println!("\n{}", "📁 General Configuration:".bold().blue());
println!(" Config file: {}", manager.path().display());
println!( println!(
" Default profile: {}", " Keyring: {}",
config.default_profile.as_deref().unwrap_or("(none)").cyan() if manager.keyring().is_available() {
); "available".green()
println!(" Profiles: {} profile(s)", config.profiles.len());
println!(
" Repository mappings: {} mapping(s)",
config.repo_profiles.len()
);
println!("\n{}", "🤖 LLM Configuration:".bold().blue());
println!(" Provider: {}", config.llm.provider.cyan());
println!(" Model: {}", config.llm.model.cyan());
println!(" Base URL: {}", manager.llm_base_url());
println!(
" API Key: {}",
mask_api_key(manager.get_api_key().as_deref())
);
println!(" Max tokens: {}", config.llm.max_tokens);
println!(" Temperature: {}", config.llm.temperature);
println!(" Timeout: {}s", config.llm.timeout);
println!("\n{}", "📝 Commit Configuration:".bold().blue());
println!(" Format: {}", config.commit.format.to_string().cyan());
println!(
" Auto-generate: {}",
if config.commit.auto_generate {
"✓ yes".green()
} else { } else {
"✗ no".red() "unavailable".red()
} }
); );
println!(
" Allow empty: {}",
if config.commit.allow_empty {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" GPG sign: {}",
if config.commit.gpg_sign {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Default scope: {}",
config
.commit
.default_scope
.as_deref()
.unwrap_or("(none)")
.cyan()
);
println!(" Max subject length: {}", config.commit.max_subject_length);
println!(
" Require scope: {}",
if config.commit.require_scope {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Require body: {}",
if config.commit.require_body {
"✓ yes".green()
} else {
"✗ no".red()
}
);
if !config.commit.body_required_types.is_empty() {
println!(
" Body required types: {}",
config.commit.body_required_types.join(", ").cyan()
);
}
println!("\n{}", "🏷️ Tag Configuration:".bold().blue());
println!(" Version prefix: '{}'", config.tag.version_prefix.cyan());
println!(
" Auto-generate: {}",
if config.tag.auto_generate {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" GPG sign: {}",
if config.tag.gpg_sign {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Include changelog: {}",
if config.tag.include_changelog {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Annotation template: {}",
config
.tag
.annotation_template
.as_deref()
.unwrap_or("(none)")
.cyan()
);
println!("\n{}", "📋 Changelog Configuration:".bold().blue());
println!(" Path: {}", config.changelog.path);
println!(
" Auto-generate: {}",
if config.changelog.auto_generate {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Format: {}",
format!("{:?}", config.changelog.format).cyan()
);
println!(
" Include hashes: {}",
if config.changelog.include_hashes {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Include authors: {}",
if config.changelog.include_authors {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!(
" Group by type: {}",
if config.changelog.group_by_type {
"✓ yes".green()
} else {
"✗ no".red()
}
);
if !config.changelog.custom_categories.is_empty() {
println!(
" Custom categories: {} category(ies)",
config.changelog.custom_categories.len()
);
}
println!("\n{}", "🎨 Theme Configuration:".bold().blue());
println!(
" Colors: {}",
if config.theme.colors {
"✓ enabled".green()
} else {
"✗ disabled".red()
}
);
println!(
" Icons: {}",
if config.theme.icons {
"✓ enabled".green()
} else {
"✗ disabled".red()
}
);
println!(" Date format: {}", config.theme.date_format.cyan());
println!("\n{}", "🔒 Security:".bold().blue());
println!(
" Encrypt sensitive: {}",
if config.encrypt_sensitive {
"✓ yes".green()
} else {
"✗ no".red()
}
);
println!("\n{}", "🔑 Keyring:".bold().blue());
let keyring = manager.keyring();
if keyring.is_available() {
println!(" Status: {}", "✓ available".green());
println!(" Backend: {}", keyring.get_status_message());
} else {
println!(" Status: {}", "✗ unavailable".red());
println!(" Note: {}", keyring.get_status_message());
}
Ok(()) Ok(())
} }
@@ -671,7 +416,22 @@ impl ConfigCommand {
manager.set_auto_generate_commits(value == "true"); manager.set_auto_generate_commits(value == "true");
} }
"tag.version_prefix" => manager.set_version_prefix(value.to_string()), "tag.version_prefix" => manager.set_version_prefix(value.to_string()),
"tag.auto_generate" => {
manager.config_mut().tag.auto_generate = value == "true";
}
"changelog.path" => manager.set_changelog_path(value.to_string()), "changelog.path" => manager.set_changelog_path(value.to_string()),
"changelog.auto_generate" => {
manager.config_mut().changelog.auto_generate = value == "true";
}
"language.output_language" => {
manager.set_output_language(value.to_string());
}
"language.keep_types_english" => {
manager.set_keep_types_english(value == "true");
}
"language.keep_changelog_types_english" => {
manager.set_keep_changelog_types_english(value == "true");
}
_ => bail!("Unknown configuration key: {}", key), _ => bail!("Unknown configuration key: {}", key),
} }
@@ -702,7 +462,14 @@ impl ConfigCommand {
"commit.format" => config.commit.format.to_string(), "commit.format" => config.commit.format.to_string(),
"commit.auto_generate" => config.commit.auto_generate.to_string(), "commit.auto_generate" => config.commit.auto_generate.to_string(),
"tag.version_prefix" => config.tag.version_prefix.clone(), "tag.version_prefix" => config.tag.version_prefix.clone(),
"tag.auto_generate" => config.tag.auto_generate.to_string(),
"changelog.path" => config.changelog.path.clone(), "changelog.path" => config.changelog.path.clone(),
"changelog.auto_generate" => config.changelog.auto_generate.to_string(),
"language.output_language" => config.language.output_language.clone(),
"language.keep_types_english" => config.language.keep_types_english.to_string(),
"language.keep_changelog_types_english" => {
config.language.keep_changelog_types_english.to_string()
}
_ => bail!("Unknown configuration key: {}", key), _ => bail!("Unknown configuration key: {}", key),
}; };

View File

@@ -331,6 +331,17 @@ impl InitCommand {
.default(ssh_dir.join("id_rsa").display().to_string()) .default(ssh_dir.join("id_rsa").display().to_string())
.interact_text()?; .interact_text()?;
let pub_key_path: String = Input::new()
.with_prompt("SSH public key path (optional, leave empty to auto-detect)")
.default(ssh_dir.join("id_rsa.pub").display().to_string())
.allow_empty(true)
.interact_text()?;
let public_key_path = if pub_key_path.is_empty() {
None
} else {
Some(PathBuf::from(pub_key_path))
};
let has_passphrase = Confirm::new() let has_passphrase = Confirm::new()
.with_prompt(messages.has_passphrase()) .with_prompt(messages.has_passphrase())
.default(false) .default(false)
@@ -342,13 +353,38 @@ impl InitCommand {
None None
}; };
let agent_forwarding = Confirm::new()
.with_prompt("Enable SSH agent forwarding (-A)?")
.default(false)
.interact()?;
let known_hosts: String = Input::new()
.with_prompt("Custom known_hosts file path (optional)")
.allow_empty(true)
.interact_text()?;
let known_hosts_file = if known_hosts.is_empty() {
None
} else {
Some(PathBuf::from(known_hosts))
};
let custom_cmd: String = Input::new()
.with_prompt("Custom SSH command (optional, overrides all other SSH settings)")
.allow_empty(true)
.interact_text()?;
let ssh_command = if custom_cmd.is_empty() {
None
} else {
Some(custom_cmd)
};
Ok(SshConfig { Ok(SshConfig {
private_key_path: Some(PathBuf::from(key_path)), private_key_path: Some(PathBuf::from(key_path)),
public_key_path: None, public_key_path,
passphrase, passphrase,
agent_forwarding: false, agent_forwarding,
ssh_command: None, ssh_command,
known_hosts_file: None, known_hosts_file,
}) })
} }

View File

@@ -345,6 +345,13 @@ impl ProfileCommand {
if profile.has_gpg() { if profile.has_gpg() {
println!(" {} GPG configured", "🔒".to_string().dimmed()); println!(" {} GPG configured", "🔒".to_string().dimmed());
} }
if profile.signing_key().is_some() {
println!(
" {} Signing key: {}",
"🔏".to_string().dimmed(),
profile.signing_key().unwrap().dimmed()
);
}
if profile.has_tokens() { if profile.has_tokens() {
println!( println!(
" {} {} token(s)", " {} {} token(s)",
@@ -596,11 +603,64 @@ impl ProfileCommand {
println!("Organization: {}", org); println!("Organization: {}", org);
} }
if let Some(ref key) = profile.signing_key {
println!("Signing key: {}", key);
}
// Profile settings
println!("\n{}", "Settings:".bold());
println!(
" Auto-sign commits: {}",
if profile.settings.auto_sign_commits {
"yes"
} else {
"no"
}
);
println!(
" Auto-sign tags: {}",
if profile.settings.auto_sign_tags {
"yes"
} else {
"no"
}
);
if let Some(ref fmt) = profile.settings.default_commit_format {
println!(" Default commit format: {}", fmt.to_string());
}
if !profile.settings.repo_patterns.is_empty() {
println!(" Repo patterns: {:?}", profile.settings.repo_patterns);
}
if let Some(ref provider) = profile.settings.llm_provider {
println!(" Preferred LLM: {}", provider);
}
if let Some(ref template) = profile.settings.commit_template {
println!(" Commit template: {}", template);
}
if let Some(ref ssh) = profile.ssh { if let Some(ref ssh) = profile.ssh {
println!("\n{}", "SSH Configuration:".bold()); println!("\n{}", "SSH Configuration:".bold());
if let Some(ref path) = ssh.private_key_path { if let Some(ref path) = ssh.private_key_path {
println!(" Private key: {:?}", path); println!(" Private key: {:?}", path);
} }
if let Some(ref path) = ssh.public_key_path {
println!(" Public key: {:?}", path);
} else if let Some(ref path) = ssh.effective_public_key_path() {
println!(" Public key: {:?} (auto-detected)", path);
}
println!(
" Agent forwarding: {}",
if ssh.agent_forwarding { "yes" } else { "no" }
);
if let Some(ref cmd) = ssh.ssh_command {
println!(" Custom command: {}", cmd);
}
if let Some(ref kh) = ssh.known_hosts_file {
println!(" Known hosts file: {:?}", kh);
}
if ssh.passphrase.is_some() {
println!(" Passphrase: [set]");
}
} }
if let Some(ref gpg) = profile.gpg { if let Some(ref gpg) = profile.gpg {
@@ -749,9 +809,9 @@ impl ProfileCommand {
} }
async fn edit_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> { async fn edit_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?; let manager = self.get_manager(config_path)?;
let profile = manager let mut profile = manager
.get_profile(name) .get_profile(name)
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))? .ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?
.clone(); .clone();
@@ -772,16 +832,54 @@ impl ProfileCommand {
}) })
.interact_text()?; .interact_text()?;
let mut new_profile = GitProfile::new(name.to_string(), user_name, user_email); profile.user_name = user_name;
new_profile.description = profile.description; profile.user_email = user_email;
new_profile.is_work = profile.is_work;
new_profile.organization = profile.organization;
new_profile.ssh = profile.ssh;
new_profile.gpg = profile.gpg;
new_profile.tokens = profile.tokens;
new_profile.usage = profile.usage;
manager.update_profile(name, new_profile)?; // Sub-menu loop for optional configuration
loop {
println!();
let options = vec![
"Done / Save changes",
"Edit SSH configuration",
"Edit GPG configuration",
"Edit signing preferences",
"Manage tokens",
];
let selection = Select::new()
.with_prompt("What would you like to edit?")
.items(&options)
.default(0)
.interact()?;
match selection {
0 => break,
1 => {
profile.ssh = Some(self.setup_ssh_interactive().await?);
}
2 => {
profile.gpg = Some(self.setup_gpg_interactive().await?);
}
3 => {
profile.settings.auto_sign_commits = Confirm::new()
.with_prompt("Auto-sign commits?")
.default(profile.settings.auto_sign_commits)
.interact()?;
profile.settings.auto_sign_tags = Confirm::new()
.with_prompt("Auto-sign tags?")
.default(profile.settings.auto_sign_tags)
.interact()?;
}
4 => {
self.edit_tokens_interactive(&mut profile, &manager).await?;
}
_ => unreachable!(),
}
}
// Reload manager as mut to save
let config_path = manager.path().to_path_buf();
let mut manager = ConfigManager::with_path(&config_path)?;
manager.update_profile(name, profile)?;
manager.save()?; manager.save()?;
println!("{} Profile '{}' updated", "".green(), name); println!("{} Profile '{}' updated", "".green(), name);
@@ -789,6 +887,50 @@ impl ProfileCommand {
Ok(()) Ok(())
} }
async fn edit_tokens_interactive(
&self,
profile: &mut GitProfile,
manager: &ConfigManager,
) -> Result<()> {
loop {
println!();
let mut options: Vec<String> = profile
.tokens
.keys()
.map(|s| format!("Remove token: {}", s))
.collect();
options.push("Add new token".to_string());
options.push("Back".to_string());
let selection = Select::new()
.with_prompt(format!("Manage tokens for '{}'", profile.name))
.items(&options)
.default(options.len() - 1)
.interact()?;
if selection == options.len() - 1 {
break;
}
if selection < profile.tokens.len() {
let service: String = profile
.tokens
.keys()
.nth(selection)
.unwrap()
.clone();
profile.remove_token(&service);
println!(
"{} Token '{}' removed",
"".green(),
service.cyan()
);
} else {
self.setup_token_interactive(profile, manager).await?;
}
}
Ok(())
}
async fn set_default(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> { async fn set_default(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?; let mut manager = self.get_manager(config_path)?;
@@ -1231,18 +1373,56 @@ impl ProfileCommand {
.map(|h| h.join(".ssh")) .map(|h| h.join(".ssh"))
.unwrap_or_else(|| PathBuf::from("~/.ssh")); .unwrap_or_else(|| PathBuf::from("~/.ssh"));
println!("\n{}", "SSH Configuration".bold());
let key_path: String = Input::new() let key_path: String = Input::new()
.with_prompt("SSH private key path") .with_prompt("SSH private key path")
.default(ssh_dir.join("id_rsa").display().to_string()) .default(ssh_dir.join("id_rsa").display().to_string())
.interact_text()?; .interact_text()?;
let pub_key_path: String = Input::new()
.with_prompt("SSH public key path (optional, leave empty to auto-detect)")
.default(ssh_dir.join("id_rsa.pub").display().to_string())
.allow_empty(true)
.interact_text()?;
let public_key_path = if pub_key_path.is_empty() {
None
} else {
Some(PathBuf::from(pub_key_path))
};
let agent_forwarding = Confirm::new()
.with_prompt("Enable SSH agent forwarding (-A)?")
.default(false)
.interact()?;
let known_hosts: String = Input::new()
.with_prompt("Custom known_hosts file path (optional)")
.allow_empty(true)
.interact_text()?;
let known_hosts_file = if known_hosts.is_empty() {
None
} else {
Some(PathBuf::from(known_hosts))
};
let custom_cmd: String = Input::new()
.with_prompt("Custom SSH command (optional, overrides all other SSH settings)")
.allow_empty(true)
.interact_text()?;
let ssh_command = if custom_cmd.is_empty() {
None
} else {
Some(custom_cmd)
};
Ok(SshConfig { Ok(SshConfig {
private_key_path: Some(PathBuf::from(key_path)), private_key_path: Some(PathBuf::from(key_path)),
public_key_path: None, public_key_path,
passphrase: None, passphrase: None,
agent_forwarding: false, agent_forwarding,
ssh_command: None, ssh_command,
known_hosts_file: None, known_hosts_file,
}) })
} }

View File

@@ -319,7 +319,7 @@ impl TagCommand {
println!("{}", messages.ai_generating_tag(commits.len())); println!("{}", messages.ai_generating_tag(commits.len()));
let generator = ContentGenerator::new_with_think(&manager, self.think).await?; let generator = ContentGenerator::new_with_think(&manager, self.think, None).await?;
generator generator
.generate_tag_message(version, &commits, language) .generate_tag_message(version, &commits, language)
.await .await

View File

@@ -386,6 +386,22 @@ impl ConfigManager {
self.config.repo_profiles.get(repo_path) self.config.repo_profiles.get(repo_path)
} }
/// Find profiles whose repo_patterns match the given repo path
pub fn match_profiles_by_repo_pattern(&self, repo_path: &str) -> Vec<&GitProfile> {
self.config
.profiles
.values()
.filter(|p| {
p.settings.repo_patterns.iter().any(|pattern| {
let trimmed = pattern.trim_matches('*');
repo_path.ends_with(trimmed)
|| repo_path.starts_with(trimmed)
|| repo_path == trimmed
})
})
.collect()
}
// LLM configuration // LLM configuration
/// Get LLM provider /// Get LLM provider

View File

@@ -17,10 +17,11 @@ pub struct AppConfig {
pub version: String, pub version: String,
/// Default profile name /// Default profile name
#[serde(skip_serializing_if = "Option::is_none")]
pub default_profile: Option<String>, pub default_profile: Option<String>,
/// All configured profiles /// All configured profiles
#[serde(default)] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub profiles: HashMap<String, GitProfile>, pub profiles: HashMap<String, GitProfile>,
/// LLM configuration /// LLM configuration
@@ -40,17 +41,9 @@ pub struct AppConfig {
pub changelog: ChangelogConfig, pub changelog: ChangelogConfig,
/// Repository-specific profile mappings /// Repository-specific profile mappings
#[serde(default)] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub repo_profiles: HashMap<String, String>, pub repo_profiles: HashMap<String, String>,
/// Whether to encrypt sensitive data
#[serde(default = "default_true")]
pub encrypt_sensitive: bool,
/// Theme settings
#[serde(default)]
pub theme: ThemeConfig,
/// Language settings /// Language settings
#[serde(default)] #[serde(default)]
pub language: LanguageConfig, pub language: LanguageConfig,
@@ -67,8 +60,6 @@ impl Default for AppConfig {
tag: TagConfig::default(), tag: TagConfig::default(),
changelog: ChangelogConfig::default(), changelog: ChangelogConfig::default(),
repo_profiles: HashMap::new(), repo_profiles: HashMap::new(),
encrypt_sensitive: true,
theme: ThemeConfig::default(),
language: LanguageConfig::default(), language: LanguageConfig::default(),
} }
} }
@@ -81,11 +72,12 @@ pub struct LlmConfig {
#[serde(default = "default_llm_provider")] #[serde(default = "default_llm_provider")]
pub provider: String, pub provider: String,
/// Model to use (stored in config, not in keyring) /// Model to use
#[serde(default = "default_model")] #[serde(default = "default_model")]
pub model: String, pub model: String,
/// API base URL (optional, will use provider default if not set) /// API base URL (optional, will use provider default if not set)
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>, pub base_url: Option<String>,
/// Maximum tokens for generation /// Maximum tokens for generation
@@ -104,8 +96,8 @@ pub struct LlmConfig {
#[serde(default = "default_api_key_storage")] #[serde(default = "default_api_key_storage")]
pub api_key_storage: String, pub api_key_storage: String,
/// API key (stored in config for fallback, encrypted if encrypt_sensitive is true) /// API key (stored in config for fallback)
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>, pub api_key: Option<String>,
/// Enable thinking/reasoning mode (deepseek, kimi, anthropic) /// Enable thinking/reasoning mode (deepseek, kimi, anthropic)
@@ -113,7 +105,7 @@ pub struct LlmConfig {
pub thinking_enabled: bool, pub thinking_enabled: bool,
/// Budget tokens for thinking mode (Anthropic Claude 4) /// Budget tokens for thinking mode (Anthropic Claude 4)
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget_tokens: Option<u32>, pub thinking_budget_tokens: Option<u32>,
} }
@@ -148,33 +140,6 @@ pub struct CommitConfig {
/// Enable AI generation by default /// Enable AI generation by default
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub auto_generate: bool, pub auto_generate: bool,
/// Allow empty commits
#[serde(default)]
pub allow_empty: bool,
/// Sign commits with GPG
#[serde(default)]
pub gpg_sign: bool,
/// Default scope (optional)
pub default_scope: Option<String>,
/// Maximum subject length
#[serde(default = "default_max_subject_length")]
pub max_subject_length: usize,
/// Require scope
#[serde(default)]
pub require_scope: bool,
/// Require body for certain types
#[serde(default)]
pub require_body: bool,
/// Types that require body
#[serde(default = "default_body_required_types")]
pub body_required_types: Vec<String>,
} }
impl Default for CommitConfig { impl Default for CommitConfig {
@@ -182,13 +147,6 @@ impl Default for CommitConfig {
Self { Self {
format: default_commit_format(), format: default_commit_format(),
auto_generate: true, auto_generate: true,
allow_empty: false,
gpg_sign: false,
default_scope: None,
max_subject_length: default_max_subject_length(),
require_scope: false,
require_body: false,
body_required_types: default_body_required_types(),
} }
} }
} }
@@ -220,18 +178,6 @@ pub struct TagConfig {
/// Enable AI generation for tag messages /// Enable AI generation for tag messages
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub auto_generate: bool, pub auto_generate: bool,
/// Sign tags with GPG
#[serde(default)]
pub gpg_sign: bool,
/// Include changelog in annotated tags
#[serde(default = "default_true")]
pub include_changelog: bool,
/// Default annotation template
#[serde(default)]
pub annotation_template: Option<String>,
} }
impl Default for TagConfig { impl Default for TagConfig {
@@ -239,9 +185,6 @@ impl Default for TagConfig {
Self { Self {
version_prefix: default_version_prefix(), version_prefix: default_version_prefix(),
auto_generate: true, auto_generate: true,
gpg_sign: false,
include_changelog: true,
annotation_template: None,
} }
} }
} }
@@ -256,26 +199,6 @@ pub struct ChangelogConfig {
/// Enable AI generation for changelog entries /// Enable AI generation for changelog entries
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub auto_generate: bool, pub auto_generate: bool,
/// Changelog format
#[serde(default = "default_changelog_format")]
pub format: ChangelogFormat,
/// Include commit hashes
#[serde(default)]
pub include_hashes: bool,
/// Include authors
#[serde(default)]
pub include_authors: bool,
/// Group by type
#[serde(default = "default_true")]
pub group_by_type: bool,
/// Custom categories
#[serde(default)]
pub custom_categories: Vec<ChangelogCategory>,
} }
impl Default for ChangelogConfig { impl Default for ChangelogConfig {
@@ -283,56 +206,6 @@ impl Default for ChangelogConfig {
Self { Self {
path: default_changelog_path(), path: default_changelog_path(),
auto_generate: true, auto_generate: true,
format: default_changelog_format(),
include_hashes: false,
include_authors: false,
group_by_type: true,
custom_categories: vec![],
}
}
}
/// Changelog format
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChangelogFormat {
KeepAChangelog,
GitHubReleases,
Custom,
}
/// Changelog category mapping
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogCategory {
/// Category title
pub title: String,
/// Commit types included in this category
pub types: Vec<String>,
}
/// Theme configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
/// Enable colors
#[serde(default = "default_true")]
pub colors: bool,
/// Enable icons
#[serde(default = "default_true")]
pub icons: bool,
/// Preferred date format
#[serde(default = "default_date_format")]
pub date_format: String,
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
colors: true,
icons: true,
date_format: default_date_format(),
} }
} }
} }
@@ -415,6 +288,7 @@ impl Language {
} }
// Default value functions // Default value functions
fn default_version() -> String { fn default_version() -> String {
"1".to_string() "1".to_string()
} }
@@ -447,14 +321,6 @@ fn default_commit_format() -> CommitFormat {
CommitFormat::Conventional CommitFormat::Conventional
} }
fn default_max_subject_length() -> usize {
100
}
fn default_body_required_types() -> Vec<String> {
vec!["feat".to_string(), "fix".to_string()]
}
fn default_version_prefix() -> String { fn default_version_prefix() -> String {
"v".to_string() "v".to_string()
} }
@@ -463,14 +329,6 @@ fn default_changelog_path() -> String {
"CHANGELOG.md".to_string() "CHANGELOG.md".to_string()
} }
fn default_changelog_format() -> ChangelogFormat {
ChangelogFormat::KeepAChangelog
}
fn default_date_format() -> String {
"%Y-%m-%d".to_string()
}
fn default_output_language() -> String { fn default_output_language() -> String {
"en".to_string() "en".to_string()
} }
@@ -509,21 +367,6 @@ impl AppConfig {
let config_dir = dirs::config_dir().context("Could not find config directory")?; let config_dir = dirs::config_dir().context("Could not find config directory")?;
Ok(config_dir.join("quicommit").join("config.toml")) Ok(config_dir.join("quicommit").join("config.toml"))
} }
// /// Get profile for a repository
// pub fn get_profile_for_repo(&self, repo_path: &str) -> Option<&GitProfile> {
// let profile_name = self.repo_profiles.get(repo_path)?;
// self.profiles.get(profile_name)
// }
// /// Set profile for a repository
// pub fn set_profile_for_repo(&mut self, repo_path: String, profile_name: String) -> Result<()> {
// if !self.profiles.contains_key(&profile_name) {
// anyhow::bail!("Profile '{}' does not exist", profile_name);
// }
// self.repo_profiles.insert(repo_path, profile_name);
// Ok(())
// }
} }
/// Encrypted PAT data for export /// Encrypted PAT data for export

View File

@@ -124,6 +124,11 @@ impl GitProfile {
.or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str())) .or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str()))
} }
/// Get the commit template if set
pub fn commit_template(&self) -> Option<&str> {
self.settings.commit_template.as_deref()
}
/// Add a token to the profile /// Add a token to the profile
pub fn add_token(&mut self, service: String, token: TokenConfig) { pub fn add_token(&mut self, service: String, token: TokenConfig) {
self.tokens.insert(service, token); self.tokens.insert(service, token);
@@ -159,78 +164,120 @@ impl GitProfile {
pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> { pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> {
let mut config = repo.config()?; let mut config = repo.config()?;
config.set_str("user.name", &self.user_name)?; // Clean up old managed keys that the new profile won't set
config.set_str("user.email", &self.user_email)?; if self.ssh.as_ref().and_then(|s| s.git_ssh_command()).is_none() {
let _ = config.remove("core.sshCommand");
if let Some(key) = self.signing_key() { }
config.set_str("user.signingkey", key)?; if self.signing_key().is_none() {
let _ = config.remove("user.signingkey");
if self.settings.auto_sign_commits { let _ = config.remove("commit.gpgsign");
config.set_bool("commit.gpgsign", true)?; let _ = config.remove("tag.gpgsign");
} }
if self.gpg.is_none() {
if self.settings.auto_sign_tags { let _ = config.remove("gpg.program");
config.set_bool("tag.gpgsign", true)?;
}
} }
if let Some(ref ssh) = self.ssh // Apply new values; track whether we've written past name/email for rollback
&& let Some(ref key_path) = ssh.private_key_path let mut wrote_optional = false;
{ let result = (|| -> Result<()> {
let path_str = key_path.display().to_string(); config.set_str("user.name", &self.user_name)?;
#[cfg(target_os = "windows")] config.set_str("user.email", &self.user_email)?;
{
config.set_str( if let Some(ref gpg) = self.gpg {
"core.sshCommand", config.set_str("gpg.program", &gpg.program)?;
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")), wrote_optional = true;
)?;
} }
#[cfg(not(target_os = "windows"))]
{ if let Some(key) = self.signing_key() {
config.set_str("core.sshCommand", &format!("ssh -i '{}'", path_str))?; 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)?;
}
wrote_optional = true;
} }
if let Some(ref ssh) = self.ssh {
if let Some(ssh_cmd) = ssh.git_ssh_command() {
config.set_str("core.sshCommand", &ssh_cmd)?;
wrote_optional = true;
}
}
Ok(())
})();
if result.is_err() && wrote_optional {
let _ = config.remove("core.sshCommand");
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
let _ = config.remove("gpg.program");
} }
Ok(()) result
} }
/// Apply this profile globally /// Apply this profile globally
pub fn apply_global(&self) -> Result<()> { pub fn apply_global(&self) -> Result<()> {
let mut config = git2::Config::open_default()?; let mut config = git2::Config::open_default()?;
config.set_str("user.name", &self.user_name)?; // Clean up old managed keys that the new profile won't set
config.set_str("user.email", &self.user_email)?; if self.ssh.as_ref().and_then(|s| s.git_ssh_command()).is_none() {
let _ = config.remove("core.sshCommand");
if let Some(key) = self.signing_key() { }
config.set_str("user.signingkey", key)?; if self.signing_key().is_none() {
let _ = config.remove("user.signingkey");
if self.settings.auto_sign_commits { let _ = config.remove("commit.gpgsign");
config.set_bool("commit.gpgsign", true)?; let _ = config.remove("tag.gpgsign");
} }
if self.gpg.is_none() {
if self.settings.auto_sign_tags { let _ = config.remove("gpg.program");
config.set_bool("tag.gpgsign", true)?;
}
} }
if let Some(ref ssh) = self.ssh // Apply new values; track whether we've written past name/email for rollback
&& let Some(ref key_path) = ssh.private_key_path let mut wrote_optional = false;
{ let result = (|| -> Result<()> {
let path_str = key_path.display().to_string(); config.set_str("user.name", &self.user_name)?;
#[cfg(target_os = "windows")] config.set_str("user.email", &self.user_email)?;
{
config.set_str( if let Some(ref gpg) = self.gpg {
"core.sshCommand", config.set_str("gpg.program", &gpg.program)?;
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")), wrote_optional = true;
)?;
} }
#[cfg(not(target_os = "windows"))]
{ if let Some(key) = self.signing_key() {
config.set_str("core.sshCommand", &format!("ssh -i '{}'", path_str))?; 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)?;
}
wrote_optional = true;
} }
if let Some(ref ssh) = self.ssh {
if let Some(ssh_cmd) = ssh.git_ssh_command() {
config.set_str("core.sshCommand", &ssh_cmd)?;
wrote_optional = true;
}
}
Ok(())
})();
if result.is_err() && wrote_optional {
let _ = config.remove("core.sshCommand");
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
let _ = config.remove("gpg.program");
} }
Ok(()) result
} }
/// Compare with current git configuration /// Compare with current git configuration
@@ -349,25 +396,72 @@ impl SshConfig {
bail!("SSH public key does not exist: {:?}", path); bail!("SSH public key does not exist: {:?}", path);
} }
if let Some(ref path) = self.known_hosts_file
&& !path.exists()
{
bail!("SSH known_hosts file does not exist: {:?}", path);
}
Ok(()) Ok(())
} }
/// Get SSH command for git /// Get the effective public key path, deriving from private key if not explicitly set
pub fn effective_public_key_path(&self) -> Option<std::path::PathBuf> {
self.public_key_path.clone().or_else(|| {
self.private_key_path.as_ref().map(|pk| {
let mut pub_path = pk.clone();
let ext = pk
.extension()
.map(|e| format!("{}.pub", e.to_string_lossy()))
.unwrap_or_else(|| "pub".to_string());
pub_path.set_extension(&ext);
pub_path
})
})
}
/// Get the effective SSH command for git config
///
/// Priority: custom `ssh_command` > constructed from key/agent/known_hosts
pub fn git_ssh_command(&self) -> Option<String> { pub fn git_ssh_command(&self) -> Option<String> {
if let Some(ref cmd) = self.ssh_command { if let Some(ref cmd) = self.ssh_command {
Some(cmd.clone()) return Some(cmd.clone());
} else if let Some(ref key_path) = self.private_key_path { }
let mut parts: Vec<String> = vec!["ssh".to_string()];
if self.agent_forwarding {
parts.push("-A".to_string());
}
if let Some(ref key_path) = self.private_key_path {
let path_str = key_path.display().to_string(); let path_str = key_path.display().to_string();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
Some(format!("ssh -i \"{}\"", path_str.replace('\\', "/"))) parts.push(format!("-i \"{}\"", path_str.replace('\\', "/")));
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
{ {
Some(format!("ssh -i '{}'", path_str)) parts.push(format!("-i '{}'", path_str));
} }
} else { }
if let Some(ref known_hosts) = self.known_hosts_file {
let kh_str = known_hosts.display().to_string();
#[cfg(target_os = "windows")]
{
parts.push(format!("-o UserKnownHostsFile=\"{}\"", kh_str.replace('\\', "/")));
}
#[cfg(not(target_os = "windows"))]
{
parts.push(format!("-o UserKnownHostsFile='{}'", kh_str));
}
}
if parts.len() == 1 {
None None
} else {
Some(parts.join(" "))
} }
} }
} }

View File

@@ -7,16 +7,21 @@ use anyhow::{Context, Result};
/// Content generator using LLM /// Content generator using LLM
pub struct ContentGenerator { pub struct ContentGenerator {
llm_client: LlmClient, llm_client: LlmClient,
template: Option<String>,
} }
impl ContentGenerator { impl ContentGenerator {
/// Create new content generator /// Create new content generator
pub async fn new(manager: &ConfigManager) -> Result<Self> { pub async fn new(manager: &ConfigManager) -> Result<Self> {
Self::new_with_think(manager, false).await Self::new_with_think(manager, false, None).await
} }
/// Create new content generator with thinking override /// Create new content generator with thinking override and optional commit template
pub async fn new_with_think(manager: &ConfigManager, think_override: bool) -> Result<Self> { pub async fn new_with_think(
manager: &ConfigManager,
think_override: bool,
template: Option<String>,
) -> Result<Self> {
let mut thinking_enabled = if think_override { let mut thinking_enabled = if think_override {
true true
} else { } else {
@@ -42,7 +47,10 @@ impl ContentGenerator {
anyhow::bail!("LLM provider '{}' is not available", manager.llm_provider()); anyhow::bail!("LLM provider '{}' is not available", manager.llm_provider());
} }
Ok(Self { llm_client }) Ok(Self {
llm_client,
template,
})
} }
fn supports_thinking(provider: &str) -> bool { fn supports_thinking(provider: &str) -> bool {
@@ -66,7 +74,7 @@ impl ContentGenerator {
}; };
self.llm_client self.llm_client
.generate_commit_message(&truncated_diff, format, language) .generate_commit_message(&truncated_diff, format, language, self.template.as_deref())
.await .await
} }

View File

@@ -201,8 +201,16 @@ impl LlmClient {
diff: &str, diff: &str,
format: crate::config::CommitFormat, format: crate::config::CommitFormat,
language: Language, language: Language,
template: Option<&str>,
) -> Result<GeneratedCommit> { ) -> Result<GeneratedCommit> {
let system_prompt = get_commit_system_prompt(format, language); let mut system_prompt = get_commit_system_prompt(format, language).to_string();
if let Some(tmpl) = template {
system_prompt.push_str(&format!(
"\n\n## Commit Message Template\nFollow this template structure:\n{}",
tmpl
));
}
// Add language instruction to the prompt // Add language instruction to the prompt
let language_instruction = match language { let language_instruction = match language {
@@ -218,7 +226,7 @@ impl LlmClient {
let prompt = format!("{}{}", diff, language_instruction); let prompt = format!("{}{}", diff, language_instruction);
let response = self let response = self
.provider .provider
.generate_with_system(system_prompt, &prompt) .generate_with_system(&system_prompt, &prompt)
.await?; .await?;
self.parse_commit_response(&response, format) self.parse_commit_response(&response, format)

View File

@@ -126,31 +126,14 @@ api_key_storage = "keyring"
[commit] [commit]
format = "conventional" format = "conventional"
auto_generate = true auto_generate = true
allow_empty = false
gpg_sign = false
max_subject_length = 100
require_scope = false
require_body = false
body_required_types = ["feat", "fix"]
[tag] [tag]
version_prefix = "v" version_prefix = "v"
auto_generate = true auto_generate = true
gpg_sign = false
include_changelog = true
[changelog] [changelog]
path = "CHANGELOG.md" path = "CHANGELOG.md"
auto_generate = true auto_generate = true
format = "keep-a-changelog"
include_hashes = false
include_authors = false
group_by_type = true
[theme]
colors = true
icons = true
date_format = "%Y-%m-%d"
[language] [language]
output_language = "en" output_language = "en"