releaser/
workflow.rs

1use std::{thread, time};
2
3use console::style;
4use semver::Version;
5use vfs::VfsPath;
6
7use crate::CrateConfig;
8use crate::Increment;
9use crate::Publisher;
10use crate::Vcs;
11use crate::version_iter::VersionIter;
12use crate::{PublishOptions, new_cargo_config_path};
13use color_eyre::eyre::Result;
14
15/// Represents virtual path in a filesystem
16/// that keeps real fs path that is root of this
17/// virtual path
18pub struct VPath<'a> {
19    real_path: &'a str,
20    virtual_path: VfsPath,
21}
22
23impl<'a> VPath<'a> {
24    #[must_use]
25    pub fn new(real_path: &'a str, virtual_path: VfsPath) -> Self {
26        Self {
27            real_path,
28            virtual_path,
29        }
30    }
31}
32
33pub trait Release<'a> {
34    /// Releases crate or workspace
35    /// * `root` - path to folder where crate's or workspace's Cargo.toml located
36    /// * `incr` - Version increment (major, minor or patch)
37    /// * `all_features` - whether to publish all features i.e. pass --all-features flag to cargo publish
38    /// * `no_verify` - whether to verify package tarball before publish i.e. pass --no-verify flag to cargo publish
39    fn release(
40        &self,
41        root: VPath<'a>,
42        incr: Increment,
43        all_features: bool,
44        no_verify: bool,
45    ) -> Result<()>;
46}
47
48pub struct Workspace<P: Publisher, V: Vcs> {
49    delay_seconds: u64,
50    publisher: P,
51    vcs: V,
52}
53
54impl<P: Publisher, V: Vcs> Workspace<P, V> {
55    pub fn new(delay_seconds: u64, publisher: P, vcs: V) -> Self {
56        Self {
57            delay_seconds,
58            publisher,
59            vcs,
60        }
61    }
62}
63
64impl<'a, P: Publisher, V: Vcs> Release<'a> for Workspace<P, V> {
65    fn release(
66        &self,
67        root: VPath<'a>,
68        incr: Increment,
69        all_features: bool,
70        no_verify: bool,
71    ) -> Result<()> {
72        let crate_conf = new_cargo_config_path(&root.virtual_path)?;
73
74        let mut it = VersionIter::open(&crate_conf)?;
75        let version = crate::update_configs(&crate_conf, &mut it, incr)?;
76
77        let ver = commit_version(&self.vcs, root.real_path, &version)?;
78
79        let delay_str = format!("{}", self.delay_seconds);
80        let delay = time::Duration::from_secs(self.delay_seconds);
81        let crates_to_publish = it.topo_sort();
82        for (i, publish) in crates_to_publish.iter().enumerate() {
83            let options = PublishOptions {
84                crate_to_publish: Some(publish),
85                all_features,
86                no_verify,
87            };
88            self.publisher.publish(root.real_path, options)?;
89            // delay between crates needed to avoid publish failure
90            // because crates.io index aren't updated instantly
91            if i < crates_to_publish.len() - 1 {
92                println!(
93                    " Waiting {} seconds after publish {} ...",
94                    style(&delay_str).green().bold(),
95                    style(publish).green().bold()
96                );
97                thread::sleep(delay);
98            }
99        }
100
101        self.vcs.create_tag(root.real_path, &ver)?;
102        self.vcs.push_tag(root.real_path, &ver)?;
103
104        Ok(())
105    }
106}
107
108#[derive(Default)]
109pub struct Crate<P: Publisher, V: Vcs> {
110    publisher: P,
111    vcs: V,
112}
113
114impl<P: Publisher, V: Vcs> Crate<P, V> {
115    pub fn new(publisher: P, vcs: V) -> Self {
116        Self { publisher, vcs }
117    }
118}
119
120impl<'a, P: Publisher, V: Vcs> Release<'a> for Crate<P, V> {
121    fn release(
122        &self,
123        root: VPath<'a>,
124        incr: Increment,
125        all_features: bool,
126        no_verify: bool,
127    ) -> Result<()> {
128        let crate_conf = new_cargo_config_path(&root.virtual_path)?;
129
130        let conf = CrateConfig::open(&crate_conf)?;
131        let ver = conf.new_version(String::new());
132        let version = crate::update_config(&crate_conf, &ver, incr)?;
133
134        let ver = commit_version(&self.vcs, root.real_path, &version)?;
135
136        let options = PublishOptions {
137            crate_to_publish: None,
138            all_features,
139            no_verify,
140        };
141        self.publisher.publish(root.real_path, options)?;
142
143        self.vcs.create_tag(root.real_path, &ver)?;
144        self.vcs.push_tag(root.real_path, &ver)?;
145
146        Ok(())
147    }
148}
149
150fn commit_version(vcs: &impl Vcs, path: &str, version: &Version) -> Result<String> {
151    let ver = format!("v{version}");
152    let commit_msg = format!("changelog: {ver}");
153    vcs.commit(path, &commit_msg)?;
154    Ok(ver)
155}
156
157#[cfg(test)]
158mod tests {
159    #![allow(clippy::unwrap_in_result)]
160    #![allow(clippy::unwrap_used)]
161    use super::*;
162    use crate::MockVcs;
163    use crate::{CARGO_CONFIG, MockPublisher};
164    use mockall::predicate::{eq, str};
165    use rstest::{fixture, rstest};
166    use vfs::MemoryFS;
167
168    #[rstest]
169    #[case::all_features(true)]
170    #[case::default_features(false)]
171    #[trace]
172    fn release_workspace(root: VfsPath, #[case] all_features: bool) {
173        // Arrange
174        let mut mock_pub = MockPublisher::new();
175        let mut mock_vcs = MockVcs::new();
176
177        mock_vcs
178            .expect_commit()
179            .with(eq("/x"), eq("changelog: v0.2.0"))
180            .times(1)
181            .returning(|_, _| Ok(()));
182
183        let solp_options: PublishOptions = PublishOptions {
184            crate_to_publish: Some("solp"),
185            all_features,
186            no_verify: false,
187        };
188        mock_pub
189            .expect_publish()
190            .withf(move |p, o| p == "/x" && *o == solp_options)
191            .times(1)
192            .returning(|_, _| Ok(()));
193
194        let solv_options: PublishOptions = PublishOptions {
195            crate_to_publish: Some("solv"),
196            all_features,
197            no_verify: false,
198        };
199        mock_pub
200            .expect_publish()
201            .withf(move |p, o| p == "/x" && *o == solv_options)
202            .times(1)
203            .returning(|_, _| Ok(()));
204
205        mock_vcs
206            .expect_create_tag()
207            .with(eq("/x"), eq("v0.2.0"))
208            .times(1)
209            .returning(|_, _| Ok(()));
210
211        mock_vcs
212            .expect_push_tag()
213            .with(eq("/x"), eq("v0.2.0"))
214            .times(1)
215            .returning(|_, _| Ok(()));
216
217        let w = Workspace::new(0, mock_pub, mock_vcs);
218        let path = VPath::new("/x", root);
219
220        // Act
221        let r = w.release(path, Increment::Minor, all_features, false);
222
223        // Assert
224        assert!(r.is_ok());
225    }
226
227    #[rstest]
228    #[case::all_features(true)]
229    #[case::default_features(false)]
230    #[trace]
231    fn release_crate(root: VfsPath, #[case] all_features: bool) {
232        // Arrange
233        let mut mock_pub = MockPublisher::new();
234        let mut mock_vcs = MockVcs::new();
235
236        mock_vcs
237            .expect_commit()
238            .with(eq("/x"), eq("changelog: v0.2.0"))
239            .times(1)
240            .returning(|_, _| Ok(()));
241
242        let options: PublishOptions = PublishOptions {
243            crate_to_publish: None,
244            all_features,
245            no_verify: false,
246        };
247        mock_pub
248            .expect_publish()
249            .withf(move |p, o| p == "/x" && *o == options)
250            .times(1)
251            .returning(|_, _| Ok(()));
252
253        mock_vcs
254            .expect_create_tag()
255            .with(eq("/x"), eq("v0.2.0"))
256            .times(1)
257            .returning(|_, _| Ok(()));
258
259        mock_vcs
260            .expect_push_tag()
261            .with(eq("/x"), eq("v0.2.0"))
262            .times(1)
263            .returning(|_, _| Ok(()));
264
265        let c = Crate::new(mock_pub, mock_vcs);
266
267        let path = VPath::new("/x", root.join("solp").unwrap());
268
269        // Act
270        let r = c.release(path, Increment::Minor, all_features, false);
271
272        // Assert
273        assert!(r.is_ok());
274    }
275
276    #[fixture]
277    fn root() -> VfsPath {
278        let root = VfsPath::new(MemoryFS::new());
279
280        root.join("solv").unwrap().create_dir().unwrap();
281        root.join("solp").unwrap().create_dir().unwrap();
282        root.join(CARGO_CONFIG)
283            .unwrap()
284            .create_file()
285            .unwrap()
286            .write_all(WKS.as_bytes())
287            .unwrap();
288
289        let ch_fn = |c: &str, d: &str| {
290            let ch_conf = root.join(c).unwrap().join(CARGO_CONFIG).unwrap();
291            ch_conf
292                .create_file()
293                .unwrap()
294                .write_all(d.as_bytes())
295                .unwrap();
296        };
297
298        ch_fn("solv", SOLV);
299        ch_fn("solp", SOLP);
300
301        root
302    }
303
304    const WKS: &str = r#"
305[workspace]
306
307members = [
308    "solv",
309    "solp",
310]
311        "#;
312
313    const SOLP: &str = r#"
314[package]
315name = "solp"
316description = "Microsoft Visual Studio solution parsing library"
317repository = "https://github.com/aegoroff/solv"
318version = "0.1.13"
319authors = ["egoroff <egoroff@gmail.com>"]
320edition = "2018"
321license = "MIT"
322workspace = ".."
323
324# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
325
326[build-dependencies] # <-- We added this and everything after!
327lalrpop = "0.19"
328
329[dependencies]
330lalrpop-util = "0.19"
331regex = "1"
332jwalk = "0.6"
333phf = { version = "0.8", features = ["macros"] }
334itertools = "0.10"
335
336        "#;
337
338    const SOLV: &str = r#"
339[package]
340name = "solv"
341description = "Microsoft Visual Studio solution validator"
342repository = "https://github.com/aegoroff/solv"
343version = "0.1.13"
344authors = ["egoroff <egoroff@gmail.com>"]
345edition = "2018"
346license = "MIT"
347workspace = ".."
348
349# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
350
351[dependencies]
352prettytable-rs = "^0.8"
353ansi_term = "0.12"
354humantime = "2.1"
355clap = "2"
356fnv = "1"
357solp = { path = "../solp/", version = "0.1.13" }
358
359        "#;
360}