cargo_release/ops/
version.rs

1use std::str::FromStr;
2
3use crate::error::CargoResult;
4
5/// Additional version functionality
6pub trait VersionExt {
7    /// Increments the major version number for this Version.
8    fn increment_major(&mut self);
9    /// Increments the minor version number for this Version.
10    fn increment_minor(&mut self);
11    /// Increments the patch version number for this Version.
12    fn increment_patch(&mut self);
13    /// Increment the alpha pre-release number for this Version.
14    ///
15    /// If this isn't alpha, switch to it.
16    ///
17    /// Errors if this would decrement the pre-release phase.
18    fn increment_alpha(&mut self) -> CargoResult<()>;
19    /// Increment the beta pre-release number for this Version.
20    ///
21    /// If this isn't beta, switch to it.
22    ///
23    /// Errors if this would decrement the pre-release phase.
24    fn increment_beta(&mut self) -> CargoResult<()>;
25    /// Increment the rc pre-release number for this Version.
26    ///
27    /// If this isn't rc, switch to it.
28    ///
29    /// Errors if this would decrement the pre-release phase.
30    fn increment_rc(&mut self) -> CargoResult<()>;
31    /// Append informational-only metadata.
32    fn metadata(&mut self, metadata: &str) -> CargoResult<()>;
33    /// Checks to see if the current Version is in pre-release status
34    fn is_prerelease(&self) -> bool;
35}
36
37impl VersionExt for semver::Version {
38    fn increment_major(&mut self) {
39        self.major += 1;
40        self.minor = 0;
41        self.patch = 0;
42        self.pre = semver::Prerelease::EMPTY;
43        self.build = semver::BuildMetadata::EMPTY;
44    }
45
46    fn increment_minor(&mut self) {
47        self.minor += 1;
48        self.patch = 0;
49        self.pre = semver::Prerelease::EMPTY;
50        self.build = semver::BuildMetadata::EMPTY;
51    }
52
53    fn increment_patch(&mut self) {
54        self.patch += 1;
55        self.pre = semver::Prerelease::EMPTY;
56        self.build = semver::BuildMetadata::EMPTY;
57    }
58
59    fn increment_alpha(&mut self) -> CargoResult<()> {
60        if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? {
61            if pre_ext == VERSION_BETA || pre_ext == VERSION_RC {
62                Err(anyhow::format_err!(
63                    "unsupported release level {}, only major, minor, and patch are supported",
64                    VERSION_ALPHA
65                ))
66            } else {
67                let new_ext_ver = if pre_ext == VERSION_ALPHA {
68                    pre_ext_ver.unwrap_or(0) + 1
69                } else {
70                    1
71                };
72                self.pre = semver::Prerelease::new(&format!("{VERSION_ALPHA}.{new_ext_ver}"))?;
73                Ok(())
74            }
75        } else {
76            self.increment_patch();
77            self.pre = semver::Prerelease::new(&format!("{VERSION_ALPHA}.1"))?;
78            Ok(())
79        }
80    }
81
82    fn increment_beta(&mut self) -> CargoResult<()> {
83        if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? {
84            if pre_ext == VERSION_RC {
85                Err(anyhow::format_err!(
86                    "unsupported release level {}, only major, minor, and patch are supported",
87                    VERSION_BETA
88                ))
89            } else {
90                let new_ext_ver = if pre_ext == VERSION_BETA {
91                    pre_ext_ver.unwrap_or(0) + 1
92                } else {
93                    1
94                };
95                self.pre = semver::Prerelease::new(&format!("{VERSION_BETA}.{new_ext_ver}"))?;
96                Ok(())
97            }
98        } else {
99            self.increment_patch();
100            self.pre = semver::Prerelease::new(&format!("{VERSION_BETA}.1"))?;
101            Ok(())
102        }
103    }
104
105    fn increment_rc(&mut self) -> CargoResult<()> {
106        if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? {
107            let new_ext_ver = if pre_ext == VERSION_RC {
108                pre_ext_ver.unwrap_or(0) + 1
109            } else {
110                1
111            };
112            self.pre = semver::Prerelease::new(&format!("{VERSION_RC}.{new_ext_ver}"))?;
113            Ok(())
114        } else {
115            self.increment_patch();
116            self.pre = semver::Prerelease::new(&format!("{VERSION_RC}.1"))?;
117            Ok(())
118        }
119    }
120
121    fn metadata(&mut self, build: &str) -> CargoResult<()> {
122        self.build = semver::BuildMetadata::new(build)?;
123        Ok(())
124    }
125
126    fn is_prerelease(&self) -> bool {
127        !self.pre.is_empty()
128    }
129}
130
131static VERSION_ALPHA: &str = "alpha";
132static VERSION_BETA: &str = "beta";
133static VERSION_RC: &str = "rc";
134
135fn prerelease_id_version(version: &semver::Version) -> CargoResult<Option<(String, Option<u64>)>> {
136    if !version.pre.is_empty() {
137        if let Some((alpha, numeric)) = version.pre.as_str().split_once('.') {
138            let alpha = alpha.to_owned();
139            let numeric = u64::from_str(numeric)
140                .map_err(|_| anyhow::format_err!("pre-release `{}` version scheme is not supported by cargo-release.  Use format like `pre`, `dev`, or `alpha.1` for prerelease", version.pre))?;
141            Ok(Some((alpha, Some(numeric))))
142        } else {
143            Ok(Some((version.pre.as_str().to_owned(), None)))
144        }
145    } else {
146        Ok(None)
147    }
148}
149
150/// Upgrade an existing requirement to a new version
151pub fn upgrade_requirement(req: &str, version: &semver::Version) -> CargoResult<Option<String>> {
152    let req_text = req.to_owned();
153    let raw_req = semver::VersionReq::parse(&req_text)
154        .expect("semver to generate valid version requirements");
155    if raw_req.comparators.is_empty() {
156        // Empty matches everything, no-change.
157        Ok(None)
158    } else {
159        let comparators: Result<Vec<_>, _> = raw_req
160            .comparators
161            .into_iter()
162            .map(|p| set_comparator(p, version))
163            .collect();
164        let comparators = comparators?;
165        let new_req = semver::VersionReq { comparators };
166        let mut new_req_text = new_req.to_string();
167        if new_req_text.starts_with('^') && !req.starts_with('^') {
168            new_req_text.remove(0);
169        }
170        // Validate contract
171        #[cfg(debug_assertions)]
172        {
173            assert!(
174                new_req.matches(version),
175                "Invalid req created: {new_req_text}"
176            );
177        }
178        if new_req_text == req_text {
179            Ok(None)
180        } else {
181            Ok(Some(new_req_text))
182        }
183    }
184}
185
186fn set_comparator(
187    mut pred: semver::Comparator,
188    version: &semver::Version,
189) -> CargoResult<semver::Comparator> {
190    match pred.op {
191        semver::Op::Wildcard => {
192            pred.major = version.major;
193            if pred.minor.is_some() {
194                pred.minor = Some(version.minor);
195            }
196            if pred.patch.is_some() {
197                pred.patch = Some(version.patch);
198            }
199            Ok(pred)
200        }
201        semver::Op::Exact => Ok(assign_partial_req(version, pred)),
202        semver::Op::Greater | semver::Op::GreaterEq | semver::Op::Less | semver::Op::LessEq => Err(
203            anyhow::format_err!("support for modifying {} is currently unsupported", pred),
204        ),
205        semver::Op::Tilde => Ok(assign_partial_req(version, pred)),
206        semver::Op::Caret => Ok(assign_partial_req(version, pred)),
207        _ => {
208            log::debug!("new predicate added");
209            Err(anyhow::format_err!(
210                "support for modifying {} is currently unsupported",
211                pred
212            ))
213        }
214    }
215}
216
217fn assign_partial_req(
218    version: &semver::Version,
219    mut pred: semver::Comparator,
220) -> semver::Comparator {
221    pred.major = version.major;
222    if pred.minor.is_some() {
223        pred.minor = Some(version.minor);
224    }
225    if pred.patch.is_some() {
226        pred.patch = Some(version.patch);
227    }
228    pred.pre = version.pre.clone();
229    pred
230}
231
232#[cfg(test)]
233mod test {
234    use super::*;
235
236    mod increment {
237        use super::*;
238
239        #[test]
240        fn alpha() {
241            let mut v = semver::Version::parse("1.0.0").unwrap();
242            v.increment_alpha().unwrap();
243            assert_eq!(v, semver::Version::parse("1.0.1-alpha.1").unwrap());
244
245            let mut v2 = semver::Version::parse("1.0.1-dev").unwrap();
246            v2.increment_alpha().unwrap();
247            assert_eq!(v2, semver::Version::parse("1.0.1-alpha.1").unwrap());
248
249            let mut v3 = semver::Version::parse("1.0.1-alpha.1").unwrap();
250            v3.increment_alpha().unwrap();
251            assert_eq!(v3, semver::Version::parse("1.0.1-alpha.2").unwrap());
252
253            let mut v4 = semver::Version::parse("1.0.1-beta.1").unwrap();
254            assert!(v4.increment_alpha().is_err());
255        }
256
257        #[test]
258        fn beta() {
259            let mut v = semver::Version::parse("1.0.0").unwrap();
260            v.increment_beta().unwrap();
261            assert_eq!(v, semver::Version::parse("1.0.1-beta.1").unwrap());
262
263            let mut v2 = semver::Version::parse("1.0.1-dev").unwrap();
264            v2.increment_beta().unwrap();
265            assert_eq!(v2, semver::Version::parse("1.0.1-beta.1").unwrap());
266
267            let mut v2 = semver::Version::parse("1.0.1-alpha.1").unwrap();
268            v2.increment_beta().unwrap();
269            assert_eq!(v2, semver::Version::parse("1.0.1-beta.1").unwrap());
270
271            let mut v3 = semver::Version::parse("1.0.1-beta.1").unwrap();
272            v3.increment_beta().unwrap();
273            assert_eq!(v3, semver::Version::parse("1.0.1-beta.2").unwrap());
274
275            let mut v4 = semver::Version::parse("1.0.1-rc.1").unwrap();
276            assert!(v4.increment_beta().is_err());
277        }
278
279        #[test]
280        fn rc() {
281            let mut v = semver::Version::parse("1.0.0").unwrap();
282            v.increment_rc().unwrap();
283            assert_eq!(v, semver::Version::parse("1.0.1-rc.1").unwrap());
284
285            let mut v2 = semver::Version::parse("1.0.1-dev").unwrap();
286            v2.increment_rc().unwrap();
287            assert_eq!(v2, semver::Version::parse("1.0.1-rc.1").unwrap());
288
289            let mut v3 = semver::Version::parse("1.0.1-rc.1").unwrap();
290            v3.increment_rc().unwrap();
291            assert_eq!(v3, semver::Version::parse("1.0.1-rc.2").unwrap());
292        }
293
294        #[test]
295        fn metadata() {
296            let mut v = semver::Version::parse("1.0.0").unwrap();
297            v.metadata("git.123456").unwrap();
298            assert_eq!(v, semver::Version::parse("1.0.0+git.123456").unwrap());
299        }
300    }
301
302    mod upgrade_requirement {
303        use super::*;
304
305        #[track_caller]
306        fn assert_req_bump<'a, O: Into<Option<&'a str>>>(version: &str, req: &str, expected: O) {
307            let version = semver::Version::parse(version).unwrap();
308            let actual = upgrade_requirement(req, &version).unwrap();
309            let expected = expected.into();
310            assert_eq!(actual.as_deref(), expected);
311        }
312
313        #[test]
314        fn wildcard_major() {
315            assert_req_bump("1.0.0", "*", None);
316        }
317
318        #[test]
319        fn wildcard_minor() {
320            assert_req_bump("1.0.0", "1.*", None);
321            assert_req_bump("1.1.0", "1.*", None);
322            assert_req_bump("2.0.0", "1.*", "2.*");
323        }
324
325        #[test]
326        fn wildcard_patch() {
327            assert_req_bump("1.0.0", "1.0.*", None);
328            assert_req_bump("1.1.0", "1.0.*", "1.1.*");
329            assert_req_bump("1.1.1", "1.0.*", "1.1.*");
330            assert_req_bump("2.0.0", "1.0.*", "2.0.*");
331        }
332
333        #[test]
334        fn caret_major() {
335            assert_req_bump("1.0.0", "1", None);
336            assert_req_bump("1.0.0", "^1", None);
337
338            assert_req_bump("1.1.0", "1", None);
339            assert_req_bump("1.1.0", "^1", None);
340
341            assert_req_bump("2.0.0", "1", "2");
342            assert_req_bump("2.0.0", "^1", "^2");
343        }
344
345        #[test]
346        fn caret_minor() {
347            assert_req_bump("1.0.0", "1.0", None);
348            assert_req_bump("1.0.0", "^1.0", None);
349
350            assert_req_bump("1.1.0", "1.0", "1.1");
351            assert_req_bump("1.1.0", "^1.0", "^1.1");
352
353            assert_req_bump("1.1.1", "1.0", "1.1");
354            assert_req_bump("1.1.1", "^1.0", "^1.1");
355
356            assert_req_bump("2.0.0", "1.0", "2.0");
357            assert_req_bump("2.0.0", "^1.0", "^2.0");
358        }
359
360        #[test]
361        fn caret_patch() {
362            assert_req_bump("1.0.0", "1.0.0", None);
363            assert_req_bump("1.0.0", "^1.0.0", None);
364
365            assert_req_bump("1.1.0", "1.0.0", "1.1.0");
366            assert_req_bump("1.1.0", "^1.0.0", "^1.1.0");
367
368            assert_req_bump("1.1.1", "1.0.0", "1.1.1");
369            assert_req_bump("1.1.1", "^1.0.0", "^1.1.1");
370
371            assert_req_bump("2.0.0", "1.0.0", "2.0.0");
372            assert_req_bump("2.0.0", "^1.0.0", "^2.0.0");
373        }
374
375        #[test]
376        fn tilde_major() {
377            assert_req_bump("1.0.0", "~1", None);
378            assert_req_bump("1.1.0", "~1", None);
379            assert_req_bump("2.0.0", "~1", "~2");
380        }
381
382        #[test]
383        fn tilde_minor() {
384            assert_req_bump("1.0.0", "~1.0", None);
385            assert_req_bump("1.1.0", "~1.0", "~1.1");
386            assert_req_bump("1.1.1", "~1.0", "~1.1");
387            assert_req_bump("2.0.0", "~1.0", "~2.0");
388        }
389
390        #[test]
391        fn tilde_patch() {
392            assert_req_bump("1.0.0", "~1.0.0", None);
393            assert_req_bump("1.1.0", "~1.0.0", "~1.1.0");
394            assert_req_bump("1.1.1", "~1.0.0", "~1.1.1");
395            assert_req_bump("2.0.0", "~1.0.0", "~2.0.0");
396        }
397
398        #[test]
399        fn equal_major() {
400            assert_req_bump("1.0.0", "=1", None);
401            assert_req_bump("1.1.0", "=1", None);
402            assert_req_bump("2.0.0", "=1", "=2");
403        }
404
405        #[test]
406        fn equal_minor() {
407            assert_req_bump("1.0.0", "=1.0", None);
408            assert_req_bump("1.1.0", "=1.0", "=1.1");
409            assert_req_bump("1.1.1", "=1.0", "=1.1");
410            assert_req_bump("2.0.0", "=1.0", "=2.0");
411        }
412
413        #[test]
414        fn equal_patch() {
415            assert_req_bump("1.0.0", "=1.0.0", None);
416            assert_req_bump("1.1.0", "=1.0.0", "=1.1.0");
417            assert_req_bump("1.1.1", "=1.0.0", "=1.1.1");
418            assert_req_bump("2.0.0", "=1.0.0", "=2.0.0");
419        }
420    }
421}