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

307
src/commands/tag.rs Normal file
View File

@@ -0,0 +1,307 @@
use anyhow::{bail, Context, Result};
use clap::Parser;
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use semver::Version;
use crate::config::manager::ConfigManager;
use crate::git::{find_repo, GitRepo};
use crate::generator::ContentGenerator;
use crate::git::tag::{
bump_version, get_latest_version, suggest_version_bump, TagBuilder, VersionBump,
};
/// Generate and create Git tags
#[derive(Parser)]
pub struct TagCommand {
/// Tag name
#[arg(short, long)]
name: Option<String>,
/// Semantic version bump
#[arg(short, long, value_name = "TYPE")]
bump: Option<String>,
/// Tag message
#[arg(short, long)]
message: Option<String>,
/// Generate message with AI
#[arg(short, long)]
generate: bool,
/// Sign the tag
#[arg(short = 'S', long)]
sign: bool,
/// Create lightweight tag (no annotation)
#[arg(short, long)]
lightweight: bool,
/// Force overwrite existing tag
#[arg(short, long)]
force: bool,
/// Push tag to remote after creation
#[arg(short, long)]
push: bool,
/// Remote to push to
#[arg(short, long, default_value = "origin")]
remote: String,
/// Dry run
#[arg(long)]
dry_run: bool,
/// Skip interactive prompts
#[arg(short = 'y', long)]
yes: bool,
}
impl TagCommand {
pub async fn execute(&self) -> Result<()> {
let repo = find_repo(".")?;
let manager = ConfigManager::new()?;
let config = manager.config();
// Determine tag name
let tag_name = if let Some(name) = &self.name {
name.clone()
} else if let Some(bump_str) = &self.bump {
// Calculate bumped version
let prefix = &config.tag.version_prefix;
let latest = get_latest_version(&repo, prefix)?
.unwrap_or_else(|| Version::new(0, 0, 0));
let bump = VersionBump::from_str(bump_str)?;
let new_version = bump_version(&latest, bump, None);
format!("{}{}", prefix, new_version)
} else {
// Interactive mode
self.select_version_interactive(&repo, &config.tag.version_prefix).await?
};
// Validate tag name (if it looks like a version)
if tag_name.starts_with('v') || tag_name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
let version_str = tag_name.trim_start_matches('v');
if let Err(e) = crate::utils::validators::validate_semver(version_str) {
println!("{}: {}", "Warning".yellow(), e);
if !self.yes {
let proceed = Confirm::new()
.with_prompt("Proceed with this tag name anyway?")
.default(true)
.interact()?;
if !proceed {
bail!("Tag creation cancelled");
}
}
}
}
// Generate or get tag message
let message = if self.lightweight {
None
} else if let Some(msg) = &self.message {
Some(msg.clone())
} else if self.generate || (config.tag.auto_generate && !self.yes) {
Some(self.generate_tag_message(&repo, &tag_name).await?)
} else if !self.yes {
Some(self.input_message_interactive(&tag_name)?)
} else {
Some(format!("Release {}", tag_name))
};
// Show preview
println!("\n{}", "".repeat(60));
println!("{}", "Tag preview:".bold());
println!("{}", "".repeat(60));
println!("Name: {}", tag_name.cyan());
if let Some(ref msg) = message {
println!("Message:\n{}", msg);
} else {
println!("Type: {}", "lightweight".yellow());
}
println!("{}", "".repeat(60));
if !self.yes {
let confirm = Confirm::new()
.with_prompt("Create this tag?")
.default(true)
.interact()?;
if !confirm {
println!("{}", "Tag creation cancelled.".yellow());
return Ok(());
}
}
if self.dry_run {
println!("\n{}", "Dry run - tag not created.".yellow());
return Ok(());
}
// Create tag
let builder = TagBuilder::new()
.name(&tag_name)
.message_opt(message)
.annotate(!self.lightweight)
.sign(self.sign)
.force(self.force);
builder.execute(&repo)?;
println!("{} Created tag {}", "".green(), tag_name.cyan());
// Push if requested
if self.push {
println!("{} Pushing tag to {}...", "".blue(), &self.remote);
repo.push(&self.remote, &format!("refs/tags/{}", tag_name))?;
println!("{} Pushed tag to {}", "".green(), &self.remote);
}
Ok(())
}
async fn select_version_interactive(&self, repo: &GitRepo, prefix: &str) -> Result<String> {
loop {
let latest = get_latest_version(repo, prefix)?;
println!("\n{}", "Version selection:".bold());
if let Some(ref version) = latest {
println!("Latest version: {}{}", prefix, version);
} else {
println!("No existing version tags found");
}
let options = vec![
"Auto-detect bump from commits",
"Bump major version",
"Bump minor version",
"Bump patch version",
"Enter custom version",
"Enter custom tag name",
];
let selection = Select::new()
.with_prompt("Select option")
.items(&options)
.default(0)
.interact()?;
match selection {
0 => {
// Auto-detect
let commits = repo.get_commits(50)?;
let bump = suggest_version_bump(&commits);
let version = latest.as_ref()
.map(|v| bump_version(v, bump, None))
.unwrap_or_else(|| Version::new(0, 1, 0));
println!("Suggested bump: {:?}{}{}", bump, prefix, version);
let confirm = Confirm::new()
.with_prompt("Use this version?")
.default(true)
.interact()?;
if confirm {
return Ok(format!("{}{}", prefix, version));
}
// User rejected, continue the loop
}
1 => {
let version = latest.as_ref()
.map(|v| bump_version(v, VersionBump::Major, None))
.unwrap_or_else(|| Version::new(1, 0, 0));
return Ok(format!("{}{}", prefix, version));
}
2 => {
let version = latest.as_ref()
.map(|v| bump_version(v, VersionBump::Minor, None))
.unwrap_or_else(|| Version::new(0, 1, 0));
return Ok(format!("{}{}", prefix, version));
}
3 => {
let version = latest.as_ref()
.map(|v| bump_version(v, VersionBump::Patch, None))
.unwrap_or_else(|| Version::new(0, 0, 1));
return Ok(format!("{}{}", prefix, version));
}
4 => {
let input: String = Input::new()
.with_prompt("Enter version (e.g., 1.2.3)")
.interact_text()?;
let version = Version::parse(&input)?;
return Ok(format!("{}{}", prefix, version));
}
5 => {
let input: String = Input::new()
.with_prompt("Enter tag name")
.interact_text()?;
return Ok(input);
}
_ => unreachable!(),
}
}
}
async fn generate_tag_message(&self, repo: &GitRepo, version: &str) -> Result<String> {
let manager = ConfigManager::new()?;
let config = manager.config();
// Get commits since last tag
let tags = repo.get_tags()?;
let commits = if let Some(latest_tag) = tags.first() {
repo.get_commits_between(&latest_tag.name, "HEAD")?
} else {
repo.get_commits(50)?
};
if commits.is_empty() {
return Ok(format!("Release {}", version));
}
println!("{} AI is generating tag message from {} commits...", "🤖", commits.len());
let generator = ContentGenerator::new(&config.llm).await?;
generator.generate_tag_message(version, &commits).await
}
fn input_message_interactive(&self, version: &str) -> Result<String> {
let default_msg = format!("Release {}", version);
let use_editor = Confirm::new()
.with_prompt("Open editor for tag message?")
.default(false)
.interact()?;
if use_editor {
crate::utils::editor::edit_content(&default_msg)
} else {
Ok(Input::new()
.with_prompt("Tag message")
.default(default_msg)
.interact_text()?)
}
}
}
// Helper trait
trait TagBuilderExt {
fn message_opt(self, message: Option<String>) -> Self;
}
impl TagBuilderExt for TagBuilder {
fn message_opt(self, message: Option<String>) -> Self {
if let Some(m) = message {
self.message(m)
} else {
self
}
}
}