302 lines
8.6 KiB
Rust
302 lines
8.6 KiB
Rust
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<String> {
|
|
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::<Vec<Version>>();
|
|
|
|
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::<Vec<_>>()
|
|
.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::<Vec<_>>()
|
|
.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::<serde_json::Value>(&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::<Vec<_>>()
|
|
.join("\n")
|
|
+ "\n";
|
|
|
|
std::fs::write("build.zig.zon", zig)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// read Cargo.toml and get the version
|
|
fn fetch_workspace_version() -> Result<String> {
|
|
let cargo_toml = toml::from_str::<Value>(&std::fs::read_to_string("Cargo.toml")?)?;
|
|
|
|
Ok(cargo_toml["workspace"]["package"]["version"]
|
|
.as_str()
|
|
.unwrap()
|
|
.trim_matches('"')
|
|
.to_string())
|
|
}
|