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

339
src/git/tag.rs Normal file
View File

@@ -0,0 +1,339 @@
use super::GitRepo;
use anyhow::{bail, Context, Result};
use semver::Version;
/// Tag builder for creating tags
pub struct TagBuilder {
name: Option<String>,
message: Option<String>,
annotate: bool,
sign: bool,
force: bool,
dry_run: bool,
version_prefix: String,
}
impl TagBuilder {
/// Create new tag builder
pub fn new() -> Self {
Self {
name: None,
message: None,
annotate: true,
sign: false,
force: false,
dry_run: false,
version_prefix: "v".to_string(),
}
}
/// Set tag name
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
/// Set tag message
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
/// Create annotated tag
pub fn annotate(mut self, annotate: bool) -> Self {
self.annotate = annotate;
self
}
/// Sign the tag
pub fn sign(mut self, sign: bool) -> Self {
self.sign = sign;
self
}
/// Force overwrite existing tag
pub fn force(mut self, force: bool) -> Self {
self.force = force;
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();
self
}
/// Set semantic version
pub fn version(mut self, version: &Version) -> Self {
self.name = Some(format!("{}{}", self.version_prefix, version));
self
}
/// Build tag message
pub fn build_message(&self) -> Result<String> {
let message = self.message.as_ref()
.cloned()
.unwrap_or_else(|| {
let name = self.name.as_deref().unwrap_or("unknown");
format!("Release {}", name)
});
Ok(message)
}
/// Execute tag creation
pub fn execute(&self, repo: &GitRepo) -> Result<()> {
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) {
bail!("Tag '{}' already exists. Use --force to overwrite.", name);
}
}
let message = if self.annotate {
Some(self.build_message()?.as_str().to_string())
} else {
None
};
repo.create_tag(name, message.as_deref(), self.sign)?;
Ok(())
}
/// Execute and push tag
pub fn execute_and_push(&self, repo: &GitRepo, remote: &str) -> Result<()> {
self.execute(repo)?;
let name = self.name.as_ref().unwrap();
repo.push(remote, &format!("refs/tags/{}", name))?;
Ok(())
}
}
impl Default for TagBuilder {
fn default() -> Self {
Self::new()
}
}
/// Semantic version bump types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionBump {
Major,
Minor,
Patch,
Prerelease,
}
impl VersionBump {
/// Parse from string
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"major" => Ok(Self::Major),
"minor" => Ok(Self::Minor),
"patch" => Ok(Self::Patch),
"prerelease" | "pre" => Ok(Self::Prerelease),
_ => bail!("Invalid version bump: {}. Use: major, minor, patch, prerelease", s),
}
}
/// Get all variants
pub fn variants() -> &'static [&'static str] {
&["major", "minor", "patch", "prerelease"]
}
}
/// Get latest version tag from repository
pub fn get_latest_version(repo: &GitRepo, prefix: &str) -> Result<Option<Version>> {
let tags = repo.get_tags()?;
let mut versions: Vec<Version> = tags
.iter()
.filter_map(|t| {
let name = &t.name;
let version_str = name.strip_prefix(prefix).unwrap_or(name);
Version::parse(version_str).ok()
})
.collect();
versions.sort_by(|a, b| b.cmp(a)); // Descending order
Ok(versions.into_iter().next())
}
/// Bump version
pub fn bump_version(version: &Version, bump: VersionBump, prerelease_id: Option<&str>) -> Version {
match bump {
VersionBump::Major => Version::new(version.major + 1, 0, 0),
VersionBump::Minor => Version::new(version.major, version.minor + 1, 0),
VersionBump::Patch => Version::new(version.major, version.minor, version.patch + 1),
VersionBump::Prerelease => {
let pre = prerelease_id.unwrap_or("alpha");
let pre_id = format!("{}.0", pre);
Version::parse(&format!("{}-{}", version, pre_id)).unwrap_or_else(|_| version.clone())
}
}
}
/// Suggest next version based on commits
pub fn suggest_version_bump(commits: &[super::CommitInfo]) -> VersionBump {
let mut has_breaking = false;
let mut has_feature = false;
let mut has_fix = false;
for commit in commits {
let msg = commit.message.to_lowercase();
if msg.contains("breaking change") || msg.contains("breaking-change") || msg.contains("breaking_change") {
has_breaking = true;
}
if let Some(commit_type) = commit.commit_type() {
match commit_type.as_str() {
"feat" => has_feature = true,
"fix" => has_fix = true,
_ => {}
}
}
}
if has_breaking {
VersionBump::Major
} else if has_feature {
VersionBump::Minor
} else if has_fix {
VersionBump::Patch
} else {
VersionBump::Patch
}
}
/// Generate tag message from commits
pub fn generate_tag_message(version: &str, commits: &[super::CommitInfo]) -> String {
let mut message = format!("Release {}\n\n", version);
// Group commits by type
let mut features = vec![];
let mut fixes = vec![];
let mut other = vec![];
let mut breaking = vec![];
for commit in commits {
let subject = commit.subject();
if commit.message.contains("BREAKING CHANGE") {
breaking.push(subject.to_string());
}
if let Some(commit_type) = commit.commit_type() {
match commit_type.as_str() {
"feat" => features.push(subject.to_string()),
"fix" => fixes.push(subject.to_string()),
_ => other.push(subject.to_string()),
}
} else {
other.push(subject.to_string());
}
}
// Build message
if !breaking.is_empty() {
message.push_str("## Breaking Changes\n\n");
for item in &breaking {
message.push_str(&format!("- {}\n", item));
}
message.push('\n');
}
if !features.is_empty() {
message.push_str("## Features\n\n");
for item in &features {
message.push_str(&format!("- {}\n", item));
}
message.push('\n');
}
if !fixes.is_empty() {
message.push_str("## Bug Fixes\n\n");
for item in &fixes {
message.push_str(&format!("- {}\n", item));
}
message.push('\n');
}
if !other.is_empty() {
message.push_str("## Other Changes\n\n");
for item in &other {
message.push_str(&format!("- {}\n", item));
}
}
message
}
/// Tag deletion helper
pub fn delete_tag(repo: &GitRepo, name: &str, remote: Option<&str>) -> Result<()> {
repo.delete_tag(name)?;
if let Some(remote) = remote {
use std::process::Command;
let output = Command::new("git")
.args(&["push", remote, ":refs/tags/{}"])
.current_dir(repo.path())
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to delete remote tag: {}", stderr);
}
}
Ok(())
}
/// List tags with filtering
pub fn list_tags(
repo: &GitRepo,
pattern: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<super::TagInfo>> {
let tags = repo.get_tags()?;
let filtered: Vec<_> = tags
.into_iter()
.filter(|t| {
if let Some(p) = pattern {
t.name.contains(p)
} else {
true
}
})
.collect();
if let Some(limit) = limit {
Ok(filtered.into_iter().take(limit).collect())
} else {
Ok(filtered)
}
}