use std::cmp::Ordering; use anyhow::{anyhow, Result}; use git2::{DiffOptions, Repository}; use indoc::indoc; use semver::{BuildMetadata, Prerelease, Version}; use toml::Value; use crate::{create_commit, BumpVersion}; pub fn get_latest_tag(repo: &Repository) -> Result { let mut tags = repo .tag_names(None)? .into_iter() .filter_map(|tag| tag.map(String::from)) .filter_map(|tag| Version::parse(tag.strip_prefix('v').unwrap_or(&tag)).ok()) .collect::>(); tags.sort_by( |a, b| match (a.pre != Prerelease::EMPTY, b.pre != Prerelease::EMPTY) { (true, true) | (false, false) => a.cmp(b), (true, false) => Ordering::Less, (false, true) => Ordering::Greater, }, ); tags.last() .map(std::string::ToString::to_string) .ok_or_else(|| anyhow!("No tags found")) } pub fn run(args: BumpVersion) -> Result<()> { let repo = Repository::open(".")?; let latest_tag = get_latest_tag(&repo)?; let current_version = Version::parse(&latest_tag)?; let latest_tag_sha = repo.revparse_single(&format!("v{latest_tag}"))?.id(); let workspace_toml_version = Version::parse(&fetch_workspace_version()?)?; if current_version.major != workspace_toml_version.major && current_version.minor != workspace_toml_version.minor { eprintln!( indoc! {" Seems like the workspace Cargo.toml ({}) version does not match up with the latest git tag ({}). Please ensure you don't change that yourself, this subcommand will handle this for you. "}, workspace_toml_version, latest_tag ); return Ok(()); } let mut revwalk = repo.revwalk()?; revwalk.push_range(format!("{latest_tag_sha}..HEAD").as_str())?; let mut diff_options = DiffOptions::new(); let mut should_increment_patch = false; let mut should_increment_minor = false; for oid in revwalk { let oid = oid?; let commit = repo.find_commit(oid)?; let message = commit.message().unwrap(); let message = message.trim(); let diff = { let parent = commit.parent(0).unwrap(); let parent_tree = parent.tree().unwrap(); let commit_tree = commit.tree().unwrap(); repo.diff_tree_to_tree( Some(&parent_tree), Some(&commit_tree), Some(&mut diff_options), )? }; let mut source_code_changed = false; diff.foreach( &mut |delta, _| { let path = delta.new_file().path().unwrap().to_str().unwrap(); if path.ends_with("rs") || path.ends_with("js") || path.ends_with('c') { source_code_changed = true; } true }, None, None, None, )?; if source_code_changed { should_increment_patch = true; let Some((prefix, _)) = message.split_once(':') else { continue; }; let convention = if prefix.contains('(') { prefix.split_once('(').unwrap().0 } else { prefix }; if ["feat", "feat!"].contains(&convention) || prefix.ends_with('!') { should_increment_minor = true; } } } let next_version = if let Some(version) = args.version { version } else { let mut next_version = current_version.clone(); if should_increment_minor { next_version.minor += 1; next_version.patch = 0; next_version.pre = Prerelease::EMPTY; next_version.build = BuildMetadata::EMPTY; } else if should_increment_patch { next_version.patch += 1; next_version.pre = Prerelease::EMPTY; next_version.build = BuildMetadata::EMPTY; } else { return Err(anyhow!(format!( "No source code changed since {current_version}" ))); } next_version }; println!("Bumping from {current_version} to {next_version}"); update_crates(¤t_version, &next_version)?; update_makefile(&next_version)?; update_cmake(&next_version)?; update_npm(&next_version)?; update_zig(&next_version)?; tag_next_version(&repo, &next_version)?; Ok(()) } fn tag_next_version(repo: &Repository, next_version: &Version) -> Result<()> { let signature = repo.signature()?; let commit_id = create_commit( repo, &format!("{next_version}"), &[ "Cargo.toml", "Cargo.lock", "cli/Cargo.toml", "cli/config/Cargo.toml", "cli/loader/Cargo.toml", "lib/Cargo.toml", "highlight/Cargo.toml", "tags/Cargo.toml", "cli/npm/package.json", "lib/binding_web/package.json", "Makefile", "lib/CMakeLists.txt", "build.zig.zon", ], )?; let tag = repo.tag( &format!("v{next_version}"), &repo.find_object(commit_id, None)?, &signature, &format!("v{next_version}"), false, )?; println!("Tagged commit {commit_id} with tag {tag}"); Ok(()) } fn update_makefile(next_version: &Version) -> Result<()> { let makefile = std::fs::read_to_string("Makefile")?; let makefile = makefile .lines() .map(|line| { if line.starts_with("VERSION") { format!("VERSION := {next_version}") } else { line.to_string() } }) .collect::>() .join("\n") + "\n"; std::fs::write("Makefile", makefile)?; Ok(()) } fn update_cmake(next_version: &Version) -> Result<()> { let cmake = std::fs::read_to_string("lib/CMakeLists.txt")?; let cmake = cmake .lines() .map(|line| { if line.contains(" VERSION") { let start_quote = line.find('"').unwrap(); let end_quote = line.rfind('"').unwrap(); format!( "{}{next_version}{}", &line[..=start_quote], &line[end_quote..] ) } else { line.to_string() } }) .collect::>() .join("\n") + "\n"; std::fs::write("lib/CMakeLists.txt", cmake)?; Ok(()) } fn update_crates(current_version: &Version, next_version: &Version) -> Result<()> { let mut cmd = std::process::Command::new("cargo"); cmd.arg("workspaces").arg("version"); if next_version.minor > current_version.minor { cmd.arg("minor"); } else { cmd.arg("patch"); } cmd.arg("--no-git-commit") .arg("--yes") .arg("--force") .arg("tree-sitter{,-cli,-config,-generate,-loader,-highlight,-tags}") .arg("--ignore-changes") .arg("lib/language/*"); let status = cmd.status()?; if !status.success() { return Err(anyhow!("Failed to update crates")); } Ok(()) } fn update_npm(next_version: &Version) -> Result<()> { for path in ["lib/binding_web/package.json", "cli/npm/package.json"] { let package_json = serde_json::from_str::(&std::fs::read_to_string(path)?)?; let mut package_json = package_json .as_object() .ok_or_else(|| anyhow!("Invalid package.json"))? .clone(); package_json.insert( "version".to_string(), serde_json::Value::String(next_version.to_string()), ); let package_json = serde_json::to_string_pretty(&package_json)? + "\n"; std::fs::write(path, package_json)?; } Ok(()) } fn update_zig(next_version: &Version) -> Result<()> { let zig = std::fs::read_to_string("build.zig.zon")?; let zig = zig .lines() .map(|line| { if line.starts_with(" .version") { format!(" .version = \"{next_version}\",") } else { line.to_string() } }) .collect::>() .join("\n") + "\n"; std::fs::write("build.zig.zon", zig)?; Ok(()) } /// read Cargo.toml and get the version fn fetch_workspace_version() -> Result { let cargo_toml = toml::from_str::(&std::fs::read_to_string("Cargo.toml")?)?; Ok(cargo_toml["workspace"]["package"]["version"] .as_str() .unwrap() .trim_matches('"') .to_string()) }