cargo_smart_release/
utils.rs1use std::process::Stdio;
2
3use anyhow::anyhow;
4use cargo_metadata::{
5 camino::{Utf8Component, Utf8Path},
6 Dependency, DependencyKind, Metadata, Package, PackageId,
7};
8use gix::bstr::{BStr, ByteSlice};
9use semver::{Version, VersionReq};
10
11pub struct Program {
12 pub found: bool,
13}
14
15impl Program {
16 pub fn named(name: &'static str) -> Self {
17 Program {
18 found: std::process::Command::new(name)
19 .stdin(Stdio::null())
20 .stdout(Stdio::null())
21 .stderr(Stdio::null())
22 .status()
23 .is_ok(),
24 }
25 }
26}
27
28pub fn will(not_really: bool) -> &'static str {
29 if not_really {
30 "WOULD"
31 } else {
32 "Will"
33 }
34}
35
36pub fn try_to_published_crate_and_new_version<'meta, 'a>(
37 c: &'a crate::traverse::Dependency<'meta>,
38) -> Option<(&'meta Package, &'a semver::Version)> {
39 match &c.mode {
40 crate::traverse::dependency::Mode::ToBePublished { adjustment } => {
41 Some((c.package, &adjustment.bump().next_release))
42 }
43 _ => None,
44 }
45}
46
47pub fn is_pre_release_version(semver: &Version) -> bool {
48 semver.major == 0
49}
50
51pub fn is_top_level_package(manifest_path: &Utf8Path, repo: &gix::Repository) -> bool {
52 manifest_path
53 .strip_prefix(
54 std::env::current_dir()
55 .expect("cwd")
56 .join(repo.workdir().as_ref().expect("repo with working tree")),
57 )
58 .is_ok_and(|p| p.components().count() == 1)
59}
60
61pub fn version_req_unset_or_default(req: &VersionReq) -> bool {
62 req.comparators.last().is_none_or(|comp| comp.op == semver::Op::Caret)
63}
64
65pub fn package_eq_dependency_ignore_dev_without_version(package: &Package, dependency: &Dependency) -> bool {
66 (dependency.kind != DependencyKind::Development || !version_req_unset_or_default(&dependency.req))
67 && package.name.as_str() == dependency.name
68}
69
70pub fn workspace_package_by_dependency<'a>(meta: &'a Metadata, dep: &Dependency) -> Option<&'a Package> {
71 meta.packages
72 .iter()
73 .find(|p| p.name.as_str() == dep.name && p.source.as_ref().is_none_or(|s| !s.is_crates_io()))
74 .filter(|p| meta.workspace_members.iter().any(|m| m == &p.id))
75}
76
77pub fn package_by_name<'a>(meta: &'a Metadata, name: &str) -> anyhow::Result<&'a Package> {
78 meta.packages
79 .iter()
80 .find(|p| p.name.as_str() == name && p.source.as_ref().is_none_or(|s| !s.is_crates_io()))
81 .ok_or_else(|| anyhow!("workspace member '{}' must be a listed package", name))
82}
83
84pub fn names_and_versions<'a>(publishees: impl IntoIterator<Item = &'a (&'a Package, &'a semver::Version)>) -> String {
85 publishees
86 .into_iter()
87 .map(|(p, nv)| format!("{} v{}", p.name, nv))
88 .collect::<Vec<_>>()
89 .join(", ")
90}
91
92pub fn package_by_id<'a>(meta: &'a Metadata, id: &PackageId) -> &'a Package {
93 meta.packages
94 .iter()
95 .find(|p| &p.id == id)
96 .expect("workspace members are in packages")
97}
98
99pub fn tag_prefix<'p>(package: &'p Package, repo: &gix::Repository) -> Option<&'p str> {
100 if is_top_level_package(&package.manifest_path, repo) {
101 None
102 } else {
103 Some(&package.name)
104 }
105}
106
107pub fn tag_name(package: &Package, version: &semver::Version, repo: &gix::Repository) -> String {
108 tag_name_inner(tag_prefix(package, repo), version)
109}
110
111fn tag_name_inner(package_name: Option<&str>, version: &semver::Version) -> String {
112 match package_name {
113 Some(name) => format!("{name}-v{version}"),
114 None => format!("v{version}"),
115 }
116}
117
118pub fn parse_possibly_prefixed_tag_version(package_name: Option<&str>, tag_name: &BStr) -> Option<Version> {
119 match package_name {
120 Some(name) => tag_name
121 .strip_prefix(name.as_bytes())
122 .and_then(|r| r.strip_prefix(b"-"))
123 .and_then(|possibly_version| parse_tag_version(possibly_version.as_bstr())),
124 None => parse_tag_version(tag_name),
125 }
126}
127
128pub fn parse_tag_version(name: &BStr) -> Option<Version> {
129 let version = name
130 .strip_prefix(b"vers")
131 .or_else(|| name.strip_prefix(b"v"))
132 .unwrap_or_else(|| name.as_bytes())
133 .to_str()
134 .ok()?;
135 Version::parse(version).ok()
136}
137
138pub fn is_tag_name(package_name: &str, tag_name: &gix::bstr::BStr) -> bool {
139 match tag_name
140 .strip_prefix(package_name.as_bytes())
141 .and_then(|r| r.strip_prefix(b"-"))
142 {
143 None => false,
144 Some(possibly_version) => parse_tag_version(possibly_version.as_bstr()).is_some(),
145 }
146}
147
148pub fn is_tag_version(name: &gix::bstr::BStr) -> bool {
149 parse_tag_version(name).is_some()
150}
151
152pub fn component_to_bytes(c: Utf8Component<'_>) -> &[u8] {
153 match c {
154 Utf8Component::Normal(c) => c.as_bytes(),
155 _ => unreachable!("only normal components are possible in paths here"),
156 }
157}
158
159pub fn time_to_zoned_time(time: gix::date::Time) -> anyhow::Result<jiff::Zoned> {
160 Ok(jiff::Timestamp::new(time.seconds, 0)?.to_zoned(jiff::tz::Offset::from_seconds(time.offset)?.to_time_zone()))
161}
162
163#[cfg(test)]
164mod tests {
165 mod parse_possibly_prefixed_tag_version {
166 mod matches {
167 use std::str::FromStr;
168
169 use gix::bstr::ByteSlice;
170 use semver::Version;
171
172 use crate::utils::{parse_possibly_prefixed_tag_version, tag_name_inner};
173
174 #[test]
175 fn whatever_tag_name_would_return() {
176 assert_eq!(
177 parse_possibly_prefixed_tag_version(
178 "git-test".into(),
179 tag_name_inner("git-test".into(), &Version::from_str("1.0.1").unwrap())
180 .as_bytes()
181 .as_bstr()
182 ),
183 Version::parse("1.0.1").expect("valid").into()
184 );
185
186 assert_eq!(
187 parse_possibly_prefixed_tag_version(
188 "single".into(),
189 tag_name_inner("single".into(), &Version::from_str("0.0.1-beta.1").unwrap())
190 .as_bytes()
191 .as_bstr()
192 ),
193 Version::parse("0.0.1-beta.1").expect("valid").into()
194 );
195
196 assert_eq!(
197 parse_possibly_prefixed_tag_version(
198 None,
199 tag_name_inner(None, &Version::from_str("0.0.1+123.x").unwrap())
200 .as_bytes()
201 .as_bstr()
202 ),
203 Version::parse("0.0.1+123.x").expect("valid").into()
204 );
205 }
206 }
207 }
208
209 mod is_tag_name {
210 mod no_match {
211 use std::str::FromStr;
212
213 use gix::bstr::ByteSlice;
214 use semver::Version;
215
216 use crate::utils::{is_tag_name, tag_name_inner};
217
218 #[test]
219 fn due_to_crate_name() {
220 assert!(!is_tag_name(
221 "foo",
222 tag_name_inner("bar".into(), &Version::from_str("0.0.1-beta.1").unwrap())
223 .as_bytes()
224 .as_bstr()
225 ));
226 }
227 }
228 mod matches {
229 use std::str::FromStr;
230
231 use gix::bstr::ByteSlice;
232 use semver::Version;
233
234 use crate::utils::{is_tag_name, tag_name_inner};
235
236 #[test]
237 fn whatever_tag_name_would_return() {
238 assert!(is_tag_name(
239 "git-test",
240 tag_name_inner("git-test".into(), &Version::from_str("1.0.1").unwrap())
241 .as_bytes()
242 .as_bstr()
243 ));
244
245 assert!(is_tag_name(
246 "single",
247 tag_name_inner("single".into(), &Version::from_str("0.0.1-beta.1").unwrap())
248 .as_bytes()
249 .as_bstr()
250 ));
251 }
252 }
253 }
254 mod is_tag_version {
255 mod no_match {
256 use gix::bstr::ByteSlice;
257
258 use crate::utils::is_tag_version;
259
260 #[test]
261 fn not_enough_numbers() {
262 assert!(!is_tag_version(b"v0.0".as_bstr()));
263 }
264
265 #[test]
266 fn funky() {
267 assert!(!is_tag_version(b"vHi.Ho.yada-anythingreally".as_bstr()));
268 }
269
270 #[test]
271 fn prefixed() {
272 assert!(!is_tag_version(b"cargo-v1.0.0".as_bstr()));
273 }
274 }
275 mod matches {
276 use gix::bstr::ByteSlice;
277
278 #[test]
279 fn no_prefix() {
280 assert!(is_tag_version(b"0.0.1".as_bstr()));
281 }
282
283 #[test]
284 fn custom_prefix() {
285 assert!(is_tag_version(b"vers0.0.1".as_bstr()));
286 }
287
288 use crate::utils::is_tag_version;
289
290 #[test]
291 fn pre_release() {
292 assert!(is_tag_version(b"v0.0.1".as_bstr()));
293 assert!(is_tag_version(b"v0.10.0-beta.1".as_bstr()));
294 }
295
296 #[test]
297 fn production() {
298 assert!(is_tag_version(b"v1.0.1-alpha.1".as_bstr()));
299 assert!(is_tag_version(b"v18.10.0+meta".as_bstr()));
300 }
301 }
302 }
303}