use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use git2::Repository;
use std::io::{self, Write};
use std::path::Path;
mod utils;
use utils::{
file::create_changelog,
git::{
add_project_files, commit_changes, create_tag, get_project_info, get_project_info_silent,
initialize_git_repo, reset_tags, ProjectInfo,
},
version::{get_latest_version, update_version_to_project, BumpType, Version},
};
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init,
Patch {
#[arg(short = 'V', long)]
version: Option<String>,
},
Minor {
#[arg(short = 'V', long)]
version: Option<String>,
},
Major {
#[arg(short = 'V', long)]
version: Option<String>,
},
Reset,
Show {
#[arg(long, conflicts_with = "quiet")]
json: bool,
#[arg(short, long, conflicts_with = "json")]
quiet: bool,
},
Release {
#[arg(short = 'l', long)]
list: bool,
#[arg(short = 't', long)]
tag: Option<String>,
},
Sync,
Config {
#[arg(short, long)]
set: Option<String>,
#[arg(short, long)]
global: bool,
#[arg(short, long)]
local: bool,
},
}
fn show_project_info(repo: &Repository, json: bool, quiet: bool) -> Result<()> {
let info = if json || quiet {
get_project_info_silent(repo)?
} else {
get_project_info(repo)?
};
if quiet {
println!("{}", info.version);
return Ok(());
}
if json {
println!("{}", project_info_json(&info)?);
return Ok(());
}
println!("\n📦 Project Information");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("🏷️ Current Version: {}", info.version);
if let Some(branch) = info.branch_name {
println!("🌿 Current Branch: {}", branch);
}
println!("📝 Commit Count: {}", info.commit_count);
if let Some(url) = info.repo_url {
println!("🔗 Repository URL: {}", url);
}
println!("\n🛠️ RustyTag Information");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("📋 Version: {}", env!("CARGO_PKG_VERSION"));
println!("📘 Description: A Git tag and version management tool");
println!("🏠 Homepage: https://github.com/sichang824/rustytag");
println!("👤 Author: sichang <sichang824@gmail.com>");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Ok(())
}
fn project_info_json(info: &ProjectInfo) -> Result<String> {
Ok(serde_json::to_string(&serde_json::json!({
"version": info.version.to_string(),
"branch": info.branch_name,
"commit_count": info.commit_count,
"repository_url": info.repo_url,
"rustytag_version": env!("CARGO_PKG_VERSION"),
}))?)
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Init => {
handle_git_initialization()?;
}
_ => {
if !Path::new(".git").exists() {
println!("This directory is not a Git repository");
println!("Please run `rustytag init` to initialize a repository");
return Ok(());
}
let repo = Repository::open(".").context("Failed to open Git repository")?;
match &cli.command {
Commands::Patch { version }
| Commands::Minor { version }
| Commands::Major { version } => {
let bump_type = match cli.command {
Commands::Patch { .. } => BumpType::Patch,
Commands::Minor { .. } => BumpType::Minor,
Commands::Major { .. } => BumpType::Major,
_ => unreachable!(),
};
let new_version = if let Some(version_str) = version {
Version::parse(version_str).context("Invalid version format")?
} else {
let latest_version = get_latest_version()?;
latest_version.bump(bump_type)
};
if version.is_some() && !new_version.prefix.is_empty() {
let mut cli_override = utils::config::Config::default();
cli_override.version_prefix = Some(new_version.prefix.clone());
utils::config::Config::update_repository(&cli_override)?;
}
update_version_to_project(&new_version)?;
add_project_files(&repo)?;
create_changelog(&new_version)?;
commit_changes(&repo, &new_version)?;
create_tag(&repo, &new_version)?;
println!("\nℹ Run the following command to publish the release");
println!("git push --follow-tags origin main\n");
}
Commands::Reset => {
reset_tags(&repo)?;
}
Commands::Show { json, quiet } => {
show_project_info(&repo, *json, *quiet)?;
}
Commands::Release { tag, list } => {
if *list {
tokio::runtime::Runtime::new()?
.block_on(async { utils::github::list_github_releases().await })?;
} else {
let version = if let Some(tag_str) = tag {
Version::parse(tag_str).context("Invalid version format")?
} else {
get_latest_version()?
};
print!(
"\n🚀 Are you sure you want to create release {}? [y/N] ",
version
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() == "y" {
tokio::runtime::Runtime::new()?.block_on(async {
utils::github::create_github_release(&version).await
})?;
} else {
println!("❌ Release cancelled");
}
}
}
Commands::Sync => {
utils::git::show_and_sync_tags(&repo)?;
}
Commands::Config { set, global, local } => {
utils::config::handle_config_command(set.clone(), *global, *local)?;
}
_ => unreachable!(),
}
}
}
Ok(())
}
fn handle_git_initialization() -> Result<()> {
if Path::new(".git").exists() {
println!("This directory is already a Git repository");
return Ok(());
}
initialize_git_repo()?;
println!("✔ Successfully initialized new Git repository");
println!("✔ Created .gitignore file");
println!("✔ Created initial commit");
Ok(())
}