Skip to main content

cargo_smart_release/
utils.rs

1use 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}