feat:(first commit)created repository and complete 0.1.0
This commit is contained in:
321
src/commands/commit.rs
Normal file
321
src/commands/commit.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user