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