Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3cd01dbcd |
@@ -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"]
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -144,7 +144,6 @@ impl TagCommand {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create tag
|
||||
let builder = TagBuilder::new()
|
||||
.name(&tag_name)
|
||||
.message_opt(message)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user