use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use serde::Deserialize;
use xshell::Shell;
use crate::environment::{get_target_dir, quiet_println, Package, PackageManifest};
use crate::lock::LockFile;
use crate::quiet_cmd;
use crate::toolchain::{prepare_toolchain, Toolchain};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct PrereleaseConfig {
enabled: bool,
}
impl PrereleaseConfig {
fn load(package_dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
#[derive(serde::Deserialize, Default)]
struct RbmtTable {
#[serde(default)]
prerelease: PrereleaseConfig,
}
let path = package_dir.join("Cargo.toml");
if !path.exists() {
return Ok(Self::default());
}
let contents = std::fs::read_to_string(&path)?;
Ok(toml::from_str::<PackageManifest<RbmtTable>>(&contents)?
.package
.metadata
.rbmt
.prerelease)
}
}
pub fn run(
sh: &Shell,
packages: &[Package],
force: bool,
baseline: &str,
) -> Result<(), Box<dyn std::error::Error>> {
quiet_println(&format!("Running pre-release checks on {} packages", packages.len()));
for package in packages {
let config = PrereleaseConfig::load(Path::new(&package.dir))?;
if !config.enabled {
quiet_println(&format!("Skipping {} (pre-release not enabled)", package.name));
continue;
}
if !force && !has_version_bump(sh, &package.dir, baseline)? {
quiet_println(&format!(
"Skipping {} (no version bump detected since {})",
package.name, baseline
));
continue;
}
quiet_println(&format!("Checking package: {}", package.name));
let _dir = sh.push_dir(&package.dir);
if let Err(e) = check_todos(sh) {
eprintln!("Pre-release check failed for {}: {e}", package.name);
return Err(e);
}
if let Err(e) = check_publish(sh) {
eprintln!("Pre-release check failed for {}: {e}", package.name);
return Err(e);
}
}
quiet_println("All pre-release checks passed");
Ok(())
}
fn has_version_bump(
sh: &Shell,
package_dir: &Path,
baseline: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let cargo_toml = package_dir.join("Cargo.toml");
let range = format!("{baseline}..");
let output = quiet_cmd!(sh, "git log --patch --reverse {range} -- {cargo_toml}").read()?;
Ok(output.lines().any(|line| line.starts_with("+version")))
}
const TODOS: &[&str] = &["// TODO", "/* TODO", "// FIXME", "/* FIXME", "\"TBD\""];
const NONOS: &[&str] = &["doc_auto_cfg"];
fn check_todos(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
quiet_println("Greping source for todos and nonos...");
let mut issues = Vec::new();
let mut dirs_to_visit = vec![sh.current_dir().join("src")];
while let Some(dir) = dirs_to_visit.pop() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
dirs_to_visit.push(path);
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
let file = fs::File::open(&path)?;
let reader = BufReader::new(file);
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
if TODOS.iter().any(|pattern| line.contains(pattern))
|| NONOS.iter().any(|pattern| line.contains(pattern))
{
issues.push((path.clone(), line_num, line));
}
}
}
}
}
if !issues.is_empty() {
eprintln!("Found {} pre-release issue(s):", issues.len());
for (file, line_num, line) in &issues {
eprintln!("{}:{}:{}", file.display(), line_num, line.trim());
}
return Err(format!("Found {} pre-release issues", issues.len()).into());
}
quiet_println("No pre-release issues found");
Ok(())
}
fn check_publish(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
prepare_toolchain(sh, Toolchain::Nightly)?;
quiet_cmd!(sh, "cargo publish --dry-run").run()?;
let package_dir = get_publish_dir(sh)?;
let _dir = sh.push_dir(&package_dir);
quiet_println(&format!("Testing publish package: {}", package_dir));
LockFile::Minimal.derive(sh)?;
quiet_cmd!(sh, "cargo test --all-features --all-targets --locked").run()?;
quiet_println("Publish tests passed");
Ok(())
}
fn get_publish_dir(sh: &Shell) -> Result<String, Box<dyn std::error::Error>> {
let target_dir = get_target_dir(sh)?;
let metadata = quiet_cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;
let current_dir = sh.current_dir();
let current_manifest = current_dir.join("Cargo.toml");
let packages =
json["packages"].as_array().ok_or("Missing 'packages' field in cargo metadata")?;
for package in packages {
let manifest_path =
package["manifest_path"].as_str().ok_or("Missing manifest_path in package")?;
if manifest_path == current_manifest.to_str().ok_or("Invalid path")? {
let name = package["name"].as_str().ok_or("Missing name in package")?;
let version = package["version"].as_str().ok_or("Missing version in package")?;
return Ok(format!("{}/package/{}-{}", target_dir, name, version));
}
}
Err("Could not find current package in cargo metadata".into())
}