use std::process::Stdio; use anyhow::anyhow; use cargo_metadata::{ camino::{Utf8Component, Utf8Path}, Dependency, DependencyKind, Metadata, Package, PackageId, }; use gix::bstr::{BStr, ByteSlice}; use semver::{Version, VersionReq}; use time::OffsetDateTime; pub struct Program { pub found: bool, } impl Program { pub fn named(name: &'static str) -> Self { Program { found: std::process::Command::new(name) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .is_ok(), } } } pub fn will(not_really: bool) -> &'static str { if not_really { "WOULD" } else { "Will" } } pub fn try_to_published_crate_and_new_version<'meta, 'a>( c: &'a crate::traverse::Dependency<'meta>, ) -> Option<(&'meta Package, &'a semver::Version)> { match &c.mode { crate::traverse::dependency::Mode::ToBePublished { adjustment } => { Some((c.package, &adjustment.bump().next_release)) } _ => None, } } pub fn is_pre_release_version(semver: &Version) -> bool { semver.major == 0 } pub fn is_top_level_package(manifest_path: &Utf8Path, repo: &gix::Repository) -> bool { manifest_path .strip_prefix( std::env::current_dir() .expect("cwd") .join(repo.work_dir().as_ref().expect("repo with working tree")), ) .map_or(false, |p| p.components().count() == 1) } pub fn version_req_unset_or_default(req: &VersionReq) -> bool { req.comparators.last().map_or(true, |comp| comp.op == semver::Op::Caret) } pub fn package_eq_dependency_ignore_dev_without_version(package: &Package, dependency: &Dependency) -> bool { (dependency.kind != DependencyKind::Development || !version_req_unset_or_default(&dependency.req)) && package.name == dependency.name } pub fn workspace_package_by_dependency<'a>(meta: &'a Metadata, dep: &Dependency) -> Option<&'a Package> { meta.packages .iter() .find(|p| p.name == dep.name && p.source.as_ref().map_or(true, |s| !s.is_crates_io())) .filter(|p| meta.workspace_members.iter().any(|m| m == &p.id)) } pub fn package_by_name<'a>(meta: &'a Metadata, name: &str) -> anyhow::Result<&'a Package> { meta.packages .iter() .find(|p| p.name == name && p.source.as_ref().map_or(true, |s| !s.is_crates_io())) .ok_or_else(|| anyhow!("workspace member '{}' must be a listed package", name)) } pub fn names_and_versions<'a>(publishees: impl IntoIterator) -> String { publishees .into_iter() .map(|(p, nv)| format!("{} v{}", p.name, nv)) .collect::>() .join(", ") } pub fn package_by_id<'a>(meta: &'a Metadata, id: &PackageId) -> &'a Package { meta.packages .iter() .find(|p| &p.id == id) .expect("workspace members are in packages") } pub fn tag_prefix<'p>(package: &'p Package, repo: &gix::Repository) -> Option<&'p str> { if is_top_level_package(&package.manifest_path, repo) { None } else { Some(&package.name) } } pub fn tag_name(package: &Package, version: &semver::Version, repo: &gix::Repository) -> String { tag_name_inner(tag_prefix(package, repo), version) } fn tag_name_inner(package_name: Option<&str>, version: &semver::Version) -> String { match package_name { Some(name) => format!("{name}-v{version}"), None => format!("v{version}"), } } pub fn parse_possibly_prefixed_tag_version(package_name: Option<&str>, tag_name: &BStr) -> Option { match package_name { Some(name) => tag_name .strip_prefix(name.as_bytes()) .and_then(|r| r.strip_prefix(b"-")) .and_then(|possibly_version| parse_tag_version(possibly_version.as_bstr())), None => parse_tag_version(tag_name), } } pub fn parse_tag_version(name: &BStr) -> Option { let version = name .strip_prefix(b"vers") .or_else(|| name.strip_prefix(b"v")) .unwrap_or_else(|| name.as_bytes()) .to_str() .ok()?; Version::parse(version).ok() } pub fn is_tag_name(package_name: &str, tag_name: &gix::bstr::BStr) -> bool { match tag_name .strip_prefix(package_name.as_bytes()) .and_then(|r| r.strip_prefix(b"-")) { None => false, Some(possibly_version) => parse_tag_version(possibly_version.as_bstr()).is_some(), } } pub fn is_tag_version(name: &gix::bstr::BStr) -> bool { parse_tag_version(name).is_some() } pub fn component_to_bytes(c: Utf8Component<'_>) -> &[u8] { match c { Utf8Component::Normal(c) => c.as_bytes(), _ => unreachable!("only normal components are possible in paths here"), } } #[cfg(test)] mod tests { mod parse_possibly_prefixed_tag_version { mod matches { use std::str::FromStr; use gix::bstr::ByteSlice; use semver::Version; use crate::utils::{parse_possibly_prefixed_tag_version, tag_name_inner}; #[test] fn whatever_tag_name_would_return() { assert_eq!( parse_possibly_prefixed_tag_version( "git-test".into(), tag_name_inner("git-test".into(), &Version::from_str("1.0.1").unwrap()) .as_bytes() .as_bstr() ), Version::parse("1.0.1").expect("valid").into() ); assert_eq!( parse_possibly_prefixed_tag_version( "single".into(), tag_name_inner("single".into(), &Version::from_str("0.0.1-beta.1").unwrap()) .as_bytes() .as_bstr() ), Version::parse("0.0.1-beta.1").expect("valid").into() ); assert_eq!( parse_possibly_prefixed_tag_version( None, tag_name_inner(None, &Version::from_str("0.0.1+123.x").unwrap()) .as_bytes() .as_bstr() ), Version::parse("0.0.1+123.x").expect("valid").into() ); } } } mod is_tag_name { mod no_match { use std::str::FromStr; use gix::bstr::ByteSlice; use semver::Version; use crate::utils::{is_tag_name, tag_name_inner}; #[test] fn due_to_crate_name() { assert!(!is_tag_name( "foo", tag_name_inner("bar".into(), &Version::from_str("0.0.1-beta.1").unwrap()) .as_bytes() .as_bstr() )); } } mod matches { use std::str::FromStr; use gix::bstr::ByteSlice; use semver::Version; use crate::utils::{is_tag_name, tag_name_inner}; #[test] fn whatever_tag_name_would_return() { assert!(is_tag_name( "git-test", tag_name_inner("git-test".into(), &Version::from_str("1.0.1").unwrap()) .as_bytes() .as_bstr() )); assert!(is_tag_name( "single", tag_name_inner("single".into(), &Version::from_str("0.0.1-beta.1").unwrap()) .as_bytes() .as_bstr() )); } } } mod is_tag_version { mod no_match { use gix::bstr::ByteSlice; use crate::utils::is_tag_version; #[test] fn not_enough_numbers() { assert!(!is_tag_version(b"v0.0".as_bstr())); } #[test] fn funky() { assert!(!is_tag_version(b"vHi.Ho.yada-anythingreally".as_bstr())); } #[test] fn prefixed() { assert!(!is_tag_version(b"cargo-v1.0.0".as_bstr())); } } mod matches { use gix::bstr::ByteSlice; #[test] fn no_prefix() { assert!(is_tag_version(b"0.0.1".as_bstr())); } #[test] fn custom_prefix() { assert!(is_tag_version(b"vers0.0.1".as_bstr())); } use crate::utils::is_tag_version; #[test] fn pre_release() { assert!(is_tag_version(b"v0.0.1".as_bstr())); assert!(is_tag_version(b"v0.10.0-beta.1".as_bstr())); } #[test] fn production() { assert!(is_tag_version(b"v1.0.1-alpha.1".as_bstr())); assert!(is_tag_version(b"v18.10.0+meta".as_bstr())); } } } } pub fn time_to_offset_date_time(time: gix::actor::Time) -> OffsetDateTime { time::OffsetDateTime::from_unix_timestamp(time.seconds_since_unix_epoch as i64) .expect("always valid unix time") .replace_offset(time::UtcOffset::from_whole_seconds(time.offset_in_seconds).expect("valid offset")) }