feat:(first commit)created repository and complete 0.1.0

This commit is contained in:
2026-01-30 14:18:32 +08:00
commit 5d4156e5e0
36 changed files with 8686 additions and 0 deletions

321
src/commands/commit.rs Normal file
View File

@@ -0,0 +1,321 @@
use anyhow::{bail, Context, Result};
use clap::Parser;
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use crate::config::manager::ConfigManager;
use crate::config::CommitFormat;
use crate::generator::ContentGenerator;
use crate::git::{find_repo, GitRepo};
use crate::git::commit::{CommitBuilder, create_date_commit_message, parse_commit_message};
use crate::utils::validators::get_commit_types;
/// Generate and execute conventional commits
#[derive(Parser)]
pub struct CommitCommand {
/// Commit type
#[arg(short, long)]
commit_type: Option<String>,
/// Commit scope
#[arg(short, long)]
scope: Option<String>,
/// Commit description/subject
#[arg(short, long)]
message: Option<String>,
/// Commit body
#[arg(long)]
body: Option<String>,
/// Mark as breaking change
#[arg(short, long)]
breaking: bool,
/// Use date-based commit message
#[arg(short, long)]
date: bool,
/// Manual input (skip AI generation)
#[arg(short, long)]
manual: bool,
/// Sign the commit
#[arg(short = 'S', long)]
sign: bool,
/// Amend previous commit
#[arg(long)]
amend: bool,
/// Stage all changes before committing
#[arg(short = 'a', long)]
all: bool,
/// Dry run (show message without committing)
#[arg(long)]
dry_run: bool,
/// Use conventional commit format
#[arg(long, group = "format")]
conventional: bool,
/// Use commitlint format
#[arg(long, group = "format")]
commitlint: bool,
/// Don't verify commit message
#[arg(long)]
no_verify: bool,
/// Skip interactive prompts
#[arg(short = 'y', long)]
yes: bool,
}
impl CommitCommand {
pub async fn execute(&self) -> Result<()> {
// Find git repository
let repo = find_repo(".")?;
// Check for changes
let status = repo.status_summary()?;
if status.clean && !self.amend {
bail!("No changes to commit. Working tree is clean.");
}
// Load configuration
let manager = ConfigManager::new()?;
let config = manager.config();
// Determine commit format
let format = if self.conventional {
CommitFormat::Conventional
} else if self.commitlint {
CommitFormat::Commitlint
} else {
config.commit.format
};
// Stage all if requested
if self.all {
repo.stage_all()?;
println!("{}", "✓ Staged all changes".green());
}
// Generate or build commit message
let commit_message = if self.date {
// Date-based commit
self.create_date_commit()
} else if self.manual || self.message.is_some() {
// Manual commit
self.create_manual_commit(format)?
} else if config.commit.auto_generate && !self.yes {
// AI-generated commit
self.generate_commit(&repo, format).await?
} else {
// Interactive commit creation
self.create_interactive_commit(format).await?
};
// Validate message
match format {
CommitFormat::Conventional => {
crate::utils::validators::validate_conventional_commit(&commit_message)?;
}
CommitFormat::Commitlint => {
crate::utils::validators::validate_commitlint_commit(&commit_message)?;
}
}
// Show commit preview
if !self.yes {
println!("\n{}", "".repeat(60));
println!("{}", "Commit preview:".bold());
println!("{}", "".repeat(60));
println!("{}", commit_message);
println!("{}", "".repeat(60));
let confirm = Confirm::new()
.with_prompt("Do you want to proceed with this commit?")
.default(true)
.interact()?;
if !confirm {
println!("{}", "Commit cancelled.".yellow());
return Ok(());
}
}
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)?)
};
if let Some(commit_oid) = result {
println!("{} {}", "✓ Created commit".green().bold(), commit_oid.to_string()[..8].to_string().cyan());
} else {
println!("{} {}", "✓ Amended commit".green().bold(), "successfully");
}
Ok(())
}
fn create_date_commit(&self) -> String {
let prefix = self.commit_type.as_deref();
create_date_commit_message(prefix)
}
fn create_manual_commit(&self, format: CommitFormat) -> Result<String> {
let commit_type = self.commit_type.clone()
.ok_or_else(|| anyhow::anyhow!("Commit type required for manual commit. Use -t <type>"))?;
let description = self.message.clone()
.ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m <message>"))?;
let builder = CommitBuilder::new()
.commit_type(commit_type)
.description(description)
.scope_opt(self.scope.clone())
.body_opt(self.body.clone())
.breaking(self.breaking)
.format(format);
builder.build_message()
}
async fn generate_commit(&self, repo: &GitRepo, format: CommitFormat) -> Result<String> {
let manager = ConfigManager::new()?;
let config = manager.config();
// Check if LLM is configured
let generator = ContentGenerator::new(&config.llm).await
.context("Failed to initialize LLM. Use --manual for manual commit.")?;
println!("{} AI is analyzing your changes...", "🤖".to_string());
let generated = if self.yes {
generator.generate_commit_from_repo(repo, format).await?
} else {
generator.generate_commit_interactive(repo, format).await?
};
Ok(generated.to_conventional())
}
async fn create_interactive_commit(&self, format: CommitFormat) -> Result<String> {
let types = get_commit_types(format == CommitFormat::Commitlint);
// Select type
let type_idx = Select::new()
.with_prompt("Select commit type")
.items(types)
.interact()?;
let commit_type = types[type_idx].to_string();
// Enter scope (optional)
let scope: String = Input::new()
.with_prompt("Scope (optional, press Enter to skip)")
.allow_empty(true)
.interact_text()?;
let scope = if scope.is_empty() { None } else { Some(scope) };
// Enter description
let description: String = Input::new()
.with_prompt("Description")
.interact_text()?;
// Breaking change
let breaking = Confirm::new()
.with_prompt("Is this a breaking change?")
.default(false)
.interact()?;
// Add body
let add_body = Confirm::new()
.with_prompt("Add body to commit?")
.default(false)
.interact()?;
let body = if add_body {
let body_text = crate::utils::editor::edit_content("Enter commit body...")?;
if body_text.trim().is_empty() {
None
} else {
Some(body_text)
}
} else {
None
};
// Build commit
let builder = CommitBuilder::new()
.commit_type(commit_type)
.description(description)
.scope_opt(scope)
.body_opt(body)
.breaking(breaking)
.format(format);
builder.build_message()
}
fn amend_commit(&self, repo: &GitRepo, message: &str) -> Result<()> {
use std::process::Command;
let mut args = vec!["commit", "--amend", "-m", message];
if self.no_verify {
args.push("--no-verify");
}
if self.sign {
args.push("-S");
}
let output = Command::new("git")
.args(&args)
.current_dir(repo.path())
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to amend commit: {}", stderr);
}
Ok(())
}
}
// Helper trait for optional builder methods
trait CommitBuilderExt {
fn scope_opt(self, scope: Option<String>) -> Self;
fn body_opt(self, body: Option<String>) -> Self;
}
impl CommitBuilderExt for CommitBuilder {
fn scope_opt(self, scope: Option<String>) -> Self {
if let Some(s) = scope {
self.scope(s)
} else {
self
}
}
fn body_opt(self, body: Option<String>) -> Self {
if let Some(b) = body {
self.body(b)
} else {
self
}
}
}