新增对 Kimi、DeepSeek、OpenRouter 的支持并升级版本至 0.1.2

This commit is contained in:
2026-02-01 15:09:39 +08:00
parent cb24b8ae85
commit c3cd01dbcd
10 changed files with 61 additions and 173 deletions

View File

@@ -1,11 +1,11 @@
[package]
name = "quicommit"
version = "0.1.0"
version = "0.1.2"
edition = "2024"
authors = ["Sidney Zhang <zly@lyzhang.me>"]
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation"
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
license = "MIT"
repository = "https://github.com/yourusername/quicommit"
repository = "https://git.lyz.one/SidneyZhang/QuiCommit"
keywords = ["git", "commit", "ai", "cli", "automation"]
categories = ["command-line-utilities", "development-tools"]

View File

@@ -148,17 +148,22 @@ impl CommitCommand {
}
}
let result = if self.amend {
if self.dry_run {
println!("\n{}", "Dry run - commit not amended.".yellow());
return Ok(());
}
self.amend_commit(&repo, &commit_message)?;
None
} else {
if self.dry_run {
println!("\n{}", "Dry run - commit not created.".yellow());
return Ok(());
}
// Execute commit
let result = if self.amend {
self.amend_commit(&repo, &commit_message)?;
None
} else {
Some(repo.commit(&commit_message, self.sign)?)
CommitBuilder::new()
.message(&commit_message)
.sign(self.sign)
.execute(&repo)?
};
if let Some(commit_oid) = result {

View File

@@ -185,7 +185,14 @@ impl InitCommand {
// LLM provider selection
println!("\n{}", "Select your preferred LLM provider:".bold());
let providers = vec!["Ollama (local)", "OpenAI", "Anthropic Claude"];
let providers = vec![
"Ollama (local)",
"OpenAI",
"Anthropic Claude",
"Kimi (Moonshot AI)",
"DeepSeek",
"OpenRouter"
];
let provider_idx = Select::new()
.items(&providers)
.default(0)
@@ -195,6 +202,9 @@ impl InitCommand {
0 => "ollama",
1 => "openai",
2 => "anthropic",
3 => "kimi",
4 => "deepseek",
5 => "openrouter",
_ => "ollama",
};
@@ -211,6 +221,21 @@ impl InitCommand {
.with_prompt("Anthropic API key")
.interact_text()?;
manager.set_anthropic_api_key(api_key);
} else if provider == "kimi" {
let api_key: String = Input::new()
.with_prompt("Kimi API key")
.interact_text()?;
manager.set_kimi_api_key(api_key);
} else if provider == "deepseek" {
let api_key: String = Input::new()
.with_prompt("DeepSeek API key")
.interact_text()?;
manager.set_deepseek_api_key(api_key);
} else if provider == "openrouter" {
let api_key: String = Input::new()
.with_prompt("OpenRouter API key")
.interact_text()?;
manager.set_openrouter_api_key(api_key);
}
Ok(())

View File

@@ -1,12 +1,12 @@
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use clap::{Parser, Subcommand};
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use crate::config::manager::ConfigManager;
use crate::config::{GitProfile, TokenConfig, TokenType, ProfileComparison};
use crate::config::{GitProfile, TokenConfig, TokenType};
use crate::config::profile::{GpgConfig, SshConfig};
use crate::git::{find_repo, GitConfigHelper, UserConfig};
use crate::git::find_repo;
use crate::utils::validators::validate_profile_name;
/// Manage Git profiles

View File

@@ -144,7 +144,6 @@ impl TagCommand {
return Ok(());
}
// Create tag
let builder = TagBuilder::new()
.name(&tag_name)
.message_opt(message)

View File

@@ -7,10 +7,9 @@ use std::path::{Path, PathBuf};
pub mod manager;
pub mod profile;
pub use manager::ConfigManager;
pub use profile::{
GitProfile, ProfileSettings, SshConfig, GpgConfig, TokenConfig, TokenType,
UsageStats, ProfileComparison, ConfigDifference
GitProfile, TokenConfig, TokenType,
UsageStats, ProfileComparison
};
/// Application configuration

View File

@@ -190,115 +190,8 @@ impl ContentGenerator {
}
}
/// Batch generator for multiple operations
pub struct BatchGenerator {
generator: ContentGenerator,
}
impl BatchGenerator {
/// Create new batch generator
pub async fn new(config: &LlmConfig) -> Result<Self> {
let generator = ContentGenerator::new(config).await?;
Ok(Self { generator })
}
/// Generate commits for multiple repositories
pub async fn generate_commits_batch<'a>(
&self,
repos: &[&'a GitRepo],
format: CommitFormat,
) -> Vec<(&'a str, Result<GeneratedCommit>)> {
let mut results = vec![];
for repo in repos {
let result = self.generator.generate_commit_from_repo(repo, format).await;
results.push((repo.path().to_str().unwrap_or("unknown"), result));
}
results
}
/// Generate changelog for multiple versions
pub async fn generate_changelog_batch(
&self,
repo: &GitRepo,
versions: &[String],
) -> Vec<(String, Result<String>)> {
let mut results = vec![];
// Get all tags
let tags = repo.get_tags().unwrap_or_default();
for (i, version) in versions.iter().enumerate() {
let from_tag = if i + 1 < tags.len() {
tags.get(i + 1).map(|t| t.name.as_str())
} else {
None
};
let result = self.generator.generate_changelog_from_repo(repo, version, from_tag).await;
results.push((version.clone(), result));
}
results
}
}
/// Generator options
#[derive(Debug, Clone)]
pub struct GeneratorOptions {
pub auto_commit: bool,
pub auto_push: bool,
pub interactive: bool,
pub dry_run: bool,
}
impl Default for GeneratorOptions {
fn default() -> Self {
Self {
auto_commit: false,
auto_push: false,
interactive: true,
dry_run: false,
}
}
}
/// Generate with options
pub async fn generate_with_options(
repo: &GitRepo,
config: &LlmConfig,
format: CommitFormat,
options: GeneratorOptions,
) -> Result<Option<GeneratedCommit>> {
let generator = ContentGenerator::new(config).await?;
let generated = if options.interactive {
generator.generate_commit_interactive(repo, format).await?
} else {
generator.generate_commit_from_repo(repo, format).await?
};
if options.dry_run {
println!("{}", generated.to_conventional());
return Ok(Some(generated));
}
if options.auto_commit {
let message = generated.to_conventional();
repo.commit(&message, false)?;
if options.auto_push {
repo.push("origin", "HEAD")?;
}
}
Ok(Some(generated))
}
/// Fallback generators when LLM is not available
pub mod fallback {
use super::*;
use crate::git::commit::create_date_commit_message;
/// Generate simple commit message without LLM

View File

@@ -9,11 +9,11 @@ pub struct CommitBuilder {
description: Option<String>,
body: Option<String>,
footer: Option<String>,
message: Option<String>,
breaking: bool,
sign: bool,
amend: bool,
no_verify: bool,
dry_run: bool,
format: crate::config::CommitFormat,
}
@@ -26,11 +26,11 @@ impl CommitBuilder {
description: None,
body: None,
footer: None,
message: None,
breaking: false,
sign: false,
amend: false,
no_verify: false,
dry_run: false,
format: crate::config::CommitFormat::Conventional,
}
}
@@ -65,6 +65,12 @@ impl CommitBuilder {
self
}
/// Set message
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
/// Mark as breaking change
pub fn breaking(mut self, breaking: bool) -> Self {
self.breaking = breaking;
@@ -89,12 +95,6 @@ impl CommitBuilder {
self
}
/// Dry run (don't actually commit)
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
/// Set commit format
pub fn format(mut self, format: crate::config::CommitFormat) -> Self {
self.format = format;
@@ -103,6 +103,10 @@ impl CommitBuilder {
/// Build commit message
pub fn build_message(&self) -> Result<String> {
if let Some(ref msg) = self.message {
return Ok(msg.clone());
}
let commit_type = self.commit_type.as_ref()
.ok_or_else(|| anyhow::anyhow!("Commit type is required"))?;
@@ -139,34 +143,14 @@ impl CommitBuilder {
pub fn execute(&self, repo: &GitRepo) -> Result<Option<String>> {
let message = self.build_message()?;
if self.dry_run {
return Ok(Some(message));
}
// Check if there are staged changes
let staged_files = repo.get_staged_files()?;
if staged_files.is_empty() && !self.amend {
bail!("No staged changes to commit. Use 'git add' to stage files first.");
}
// Validate message
match self.format {
crate::config::CommitFormat::Conventional => {
crate::utils::validators::validate_conventional_commit(&message)?;
}
crate::config::CommitFormat::Commitlint => {
crate::utils::validators::validate_commitlint_commit(&message)?;
}
}
if self.amend {
self.amend_commit(repo, &message)?;
Ok(None)
} else {
repo.commit(&message, self.sign)?;
}
Ok(None)
}
}
fn amend_commit(&self, repo: &GitRepo, message: &str) -> Result<()> {
use std::process::Command;

View File

@@ -128,7 +128,7 @@ impl GitRepo {
}
/// Create a signature using repository configuration
pub fn create_signature(&self) -> Result<Signature> {
pub fn create_signature(&self) -> Result<Signature<'_>> {
let name = self.get_user_name()?;
let email = self.get_user_email()?;
let time = git2::Time::new(std::time::SystemTime::now()

View File

@@ -9,7 +9,6 @@ pub struct TagBuilder {
annotate: bool,
sign: bool,
force: bool,
dry_run: bool,
version_prefix: String,
}
@@ -22,7 +21,6 @@ impl TagBuilder {
annotate: true,
sign: false,
force: false,
dry_run: false,
version_prefix: "v".to_string(),
}
}
@@ -57,12 +55,6 @@ impl TagBuilder {
self
}
/// Dry run (don't actually create tag)
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
/// Set version prefix
pub fn version_prefix(mut self, prefix: impl Into<String>) -> Self {
self.version_prefix = prefix.into();
@@ -92,15 +84,6 @@ impl TagBuilder {
let name = self.name.as_ref()
.ok_or_else(|| anyhow::anyhow!("Tag name is required"))?;
if self.dry_run {
println!("Would create tag: {}", name);
if self.annotate {
println!("Message: {}", self.build_message()?);
}
return Ok(());
}
// Check if tag already exists
if !self.force {
let existing_tags = repo.get_tags()?;
if existing_tags.iter().any(|t| t.name == *name) {