use crate::cli::output::print_success;
use crate::core::config::Config;
use crate::core::semver::validate_semver;
use crate::infra::git::{
git_commit_files, git_create_annotated_tag, git_is_clean, resolve_repo_name, resolve_toplevel,
};
use crate::workflow::error::WorkflowError;
use std::path::Path;
pub struct TagOptions<'a> {
pub version: &'a str,
pub repo_root: Option<&'a Path>,
}
pub fn update_cargo_toml_version(content: &str, new_version: &str) -> Option<String> {
let mut in_package = false;
let mut replaced = false;
let mut result = String::with_capacity(content.len() + 16);
for line in content.split_inclusive('\n') {
let trimmed = line.trim();
if trimmed.starts_with('[') {
let without_comment = if let Some(idx) = trimmed.find('#') {
trimmed[..idx].trim()
} else {
trimmed
};
in_package = without_comment == "[package]"
|| without_comment
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.map(|s| s.trim())
== Some("package");
} else if in_package && !replaced {
if let Some(rest) = trimmed.strip_prefix("version") {
let rest = rest.trim_start();
if let Some(after_eq) = rest.strip_prefix('=') {
let after_eq = after_eq.trim_start();
if let Some(quote) = after_eq.chars().next() {
if quote == '"' || quote == '\'' {
let after_quote = &after_eq[quote.len_utf8()..];
if let Some(end_idx) = after_quote.find(quote) {
let current_version = &after_quote[..end_idx];
if current_version == new_version {
return None;
}
let old_pattern = format!("{}{}{}", quote, current_version, quote);
let new_pattern = format!("{}{}{}", quote, new_version, quote);
let updated_line = line.replacen(&old_pattern, &new_pattern, 1);
result.push_str(&updated_line);
replaced = true;
continue;
}
}
}
}
}
}
result.push_str(line);
}
if replaced {
Some(result)
} else {
None
}
}
pub fn tag_release(options: TagOptions<'_>) -> Result<(), WorkflowError> {
if !validate_semver(options.version) {
return Err(WorkflowError::InvalidSemVer(options.version.to_string()));
}
let toplevel = resolve_toplevel(options.repo_root)?;
let repo_name = resolve_repo_name(&toplevel);
let config_path = toplevel.join(".git-claw.toml");
let config = Config::load_or_default(&config_path, &repo_name)?;
if !git_is_clean(&toplevel)? {
return Err(WorkflowError::DirtyWorkingTree);
}
let cargo_toml_path = toplevel.join("Cargo.toml");
if cargo_toml_path.exists() {
let clean_version = options.version.trim_start_matches('v');
let content = std::fs::read_to_string(&cargo_toml_path)?;
if let Some(new_content) = update_cargo_toml_version(&content, clean_version) {
std::fs::write(&cargo_toml_path, new_content)?;
let cargo_lock_path = toplevel.join("Cargo.lock");
let commit_files: &[&str] = if cargo_lock_path.exists() {
let _ = std::process::Command::new("cargo")
.current_dir(&toplevel)
.arg("check")
.output();
&["Cargo.toml", "Cargo.lock"]
} else {
&["Cargo.toml"]
};
let commit_msg = format!("chore: release {}", options.version);
git_commit_files(&toplevel, commit_files, &commit_msg)?;
print_success(format!("Updated Cargo.toml to version '{}'", clean_version));
}
}
let message = format!("Release {}", options.version);
git_create_annotated_tag(&toplevel, options.version, config.main_branch(), &message)?;
print_success(format!(
"Created annotated release tag '{}' on branch '{}'",
options.version,
config.main_branch()
));
Ok(())
}