feat:(first commit)created repository and complete 0.1.0
This commit is contained in:
340
src/generator/mod.rs
Normal file
340
src/generator/mod.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
use crate::config::{CommitFormat, LlmConfig};
|
||||
use crate::git::{CommitInfo, GitRepo};
|
||||
use crate::llm::{GeneratedCommit, LlmClient};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
|
||||
/// Content generator using LLM
|
||||
pub struct ContentGenerator {
|
||||
llm_client: LlmClient,
|
||||
}
|
||||
|
||||
impl ContentGenerator {
|
||||
/// Create new content generator
|
||||
pub async fn new(config: &LlmConfig) -> Result<Self> {
|
||||
let llm_client = LlmClient::from_config(config).await?;
|
||||
|
||||
// Check if provider is available
|
||||
if !llm_client.is_available().await {
|
||||
anyhow::bail!("LLM provider '{}' is not available", config.provider);
|
||||
}
|
||||
|
||||
Ok(Self { llm_client })
|
||||
}
|
||||
|
||||
/// Generate commit message from diff
|
||||
pub async fn generate_commit_message(
|
||||
&self,
|
||||
diff: &str,
|
||||
format: CommitFormat,
|
||||
) -> Result<GeneratedCommit> {
|
||||
// Truncate diff if too long
|
||||
let max_diff_len = 4000;
|
||||
let truncated_diff = if diff.len() > max_diff_len {
|
||||
format!("{}\n... (truncated)", &diff[..max_diff_len])
|
||||
} else {
|
||||
diff.to_string()
|
||||
};
|
||||
|
||||
self.llm_client.generate_commit_message(&truncated_diff, format).await
|
||||
}
|
||||
|
||||
/// Generate commit message from repository changes
|
||||
pub async fn generate_commit_from_repo(
|
||||
&self,
|
||||
repo: &GitRepo,
|
||||
format: CommitFormat,
|
||||
) -> Result<GeneratedCommit> {
|
||||
let diff = repo.get_staged_diff()
|
||||
.context("Failed to get staged diff")?;
|
||||
|
||||
if diff.is_empty() {
|
||||
anyhow::bail!("No staged changes to generate commit from");
|
||||
}
|
||||
|
||||
self.generate_commit_message(&diff, format).await
|
||||
}
|
||||
|
||||
/// Generate tag message
|
||||
pub async fn generate_tag_message(
|
||||
&self,
|
||||
version: &str,
|
||||
commits: &[CommitInfo],
|
||||
) -> Result<String> {
|
||||
let commit_messages: Vec<String> = commits
|
||||
.iter()
|
||||
.map(|c| c.subject().to_string())
|
||||
.collect();
|
||||
|
||||
self.llm_client.generate_tag_message(version, &commit_messages).await
|
||||
}
|
||||
|
||||
/// Generate changelog entry
|
||||
pub async fn generate_changelog_entry(
|
||||
&self,
|
||||
version: &str,
|
||||
commits: &[CommitInfo],
|
||||
) -> Result<String> {
|
||||
let typed_commits: Vec<(String, String)> = commits
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let commit_type = c.commit_type().unwrap_or_else(|| "other".to_string());
|
||||
(commit_type, c.subject().to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.llm_client.generate_changelog_entry(version, &typed_commits).await
|
||||
}
|
||||
|
||||
/// Generate changelog from repository
|
||||
pub async fn generate_changelog_from_repo(
|
||||
&self,
|
||||
repo: &GitRepo,
|
||||
version: &str,
|
||||
from_tag: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let commits = if let Some(tag) = from_tag {
|
||||
repo.get_commits_between(tag, "HEAD")?
|
||||
} else {
|
||||
repo.get_commits(50)?
|
||||
};
|
||||
|
||||
self.generate_changelog_entry(version, &commits).await
|
||||
}
|
||||
|
||||
/// Interactive commit generation with user feedback
|
||||
pub async fn generate_commit_interactive(
|
||||
&self,
|
||||
repo: &GitRepo,
|
||||
format: CommitFormat,
|
||||
) -> Result<GeneratedCommit> {
|
||||
use dialoguer::{Confirm, Select};
|
||||
use console::Term;
|
||||
|
||||
let diff = repo.get_staged_diff()?;
|
||||
|
||||
if diff.is_empty() {
|
||||
anyhow::bail!("No staged changes");
|
||||
}
|
||||
|
||||
// Show diff summary
|
||||
let files = repo.get_staged_files()?;
|
||||
println!("\nStaged files ({}):", files.len());
|
||||
for file in &files {
|
||||
println!(" • {}", file);
|
||||
}
|
||||
|
||||
// Generate initial commit
|
||||
println!("\nGenerating commit message...");
|
||||
let mut generated = self.generate_commit_message(&diff, format).await?;
|
||||
|
||||
loop {
|
||||
println!("\n{}", "─".repeat(60));
|
||||
println!("Generated commit message:");
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("{}", generated.to_conventional());
|
||||
println!("{}", "─".repeat(60));
|
||||
|
||||
let options = vec![
|
||||
"✓ Accept and commit",
|
||||
"🔄 Regenerate",
|
||||
"✏️ Edit",
|
||||
"📋 Copy to clipboard",
|
||||
"❌ Cancel",
|
||||
];
|
||||
|
||||
let selection = Select::new()
|
||||
.with_prompt("What would you like to do?")
|
||||
.items(&options)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
match selection {
|
||||
0 => return Ok(generated),
|
||||
1 => {
|
||||
println!("Regenerating...");
|
||||
generated = self.generate_commit_message(&diff, format).await?;
|
||||
}
|
||||
2 => {
|
||||
let edited = crate::utils::editor::edit_content(&generated.to_conventional())?;
|
||||
generated = self.parse_edited_commit(&edited, format)?;
|
||||
}
|
||||
3 => {
|
||||
#[cfg(feature = "clipboard")]
|
||||
{
|
||||
arboard::Clipboard::new()?.set_text(generated.to_conventional())?;
|
||||
println!("Copied to clipboard!");
|
||||
}
|
||||
#[cfg(not(feature = "clipboard"))]
|
||||
{
|
||||
println!("Clipboard feature not enabled");
|
||||
}
|
||||
}
|
||||
4 => anyhow::bail!("Cancelled by user"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_edited_commit(&self, edited: &str, format: CommitFormat) -> Result<GeneratedCommit> {
|
||||
let parsed = crate::git::commit::parse_commit_message(edited);
|
||||
|
||||
Ok(GeneratedCommit {
|
||||
commit_type: parsed.commit_type.unwrap_or_else(|| "chore".to_string()),
|
||||
scope: parsed.scope,
|
||||
description: parsed.description.unwrap_or_else(|| "update".to_string()),
|
||||
body: parsed.body,
|
||||
footer: parsed.footer,
|
||||
breaking: parsed.breaking,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn generate_simple_commit(files: &[String]) -> String {
|
||||
if files.len() == 1 {
|
||||
format!("chore: update {}", files[0])
|
||||
} else if files.len() <= 3 {
|
||||
format!("chore: update {}", files.join(", "))
|
||||
} else {
|
||||
format!("chore: update {} files", files.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate date-based commit
|
||||
pub fn generate_date_commit() -> String {
|
||||
create_date_commit_message(None)
|
||||
}
|
||||
|
||||
/// Generate commit based on file types
|
||||
pub fn generate_by_file_types(files: &[String]) -> String {
|
||||
let has_code = files.iter().any(|f| {
|
||||
f.ends_with(".rs") || f.ends_with(".py") || f.ends_with(".js") || f.ends_with(".ts")
|
||||
});
|
||||
|
||||
let has_docs = files.iter().any(|f| f.ends_with(".md") || f.contains("README"));
|
||||
|
||||
let has_tests = files.iter().any(|f| f.contains("test") || f.contains("spec"));
|
||||
|
||||
if has_tests {
|
||||
"test: update tests".to_string()
|
||||
} else if has_docs {
|
||||
"docs: update documentation".to_string()
|
||||
} else if has_code {
|
||||
"refactor: update code".to_string()
|
||||
} else {
|
||||
"chore: update files".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user