releaser/
lib.rs

1#![warn(unused_extern_crates)]
2#![warn(clippy::unwrap_in_result)]
3#![warn(clippy::unwrap_used)]
4#![allow(clippy::missing_errors_doc)]
5#[macro_use]
6extern crate handlebars;
7
8use std::collections::HashMap;
9
10use clap::ValueEnum;
11#[cfg(test)]
12use mockall::{automock, predicate::str};
13use semver::{BuildMetadata, Prerelease, Version};
14use serde::Deserialize;
15
16use color_eyre::eyre::Result;
17use toml_edit::{DocumentMut, value};
18use vfs::VfsPath;
19
20pub mod brew;
21pub mod cargo;
22pub mod git;
23pub mod hash;
24mod packaging;
25mod resource;
26pub mod scoop;
27mod version_iter;
28pub mod workflow;
29
30#[cfg(test)] // <-- not needed in integration tests
31extern crate rstest;
32
33const CARGO_CONFIG: &str = "Cargo.toml";
34const VERSION: &str = "version";
35const PACK: &str = "package";
36const DEPS: &str = "dependencies";
37
38#[derive(Default, Eq, PartialEq, Debug)]
39pub struct PublishOptions<'a> {
40    pub crate_to_publish: Option<&'a str>,
41    pub all_features: bool,
42    pub no_verify: bool,
43}
44
45#[cfg_attr(test, automock)]
46pub trait Publisher {
47    fn publish<'a>(&'a self, path: &'a str, options: PublishOptions<'a>) -> Result<()>;
48}
49
50#[cfg_attr(test, automock)]
51pub trait Vcs {
52    fn commit(&self, path: &str, message: &str) -> Result<()>;
53    fn create_tag(&self, path: &str, tag: &str) -> Result<()>;
54    fn push_tag(&self, path: &str, tag: &str) -> Result<()>;
55    fn push(&self, path: &str) -> Result<()>;
56}
57
58/// Represents a publisher that does nothing.
59#[derive(Default)]
60pub struct NonPublisher;
61
62impl Publisher for NonPublisher {
63    fn publish<'a>(&'a self, _path: &'a str, _options: PublishOptions<'a>) -> Result<()> {
64        Ok(())
65    }
66}
67
68/// Updates the configurations by aggregating the maximum version from an iterator of crate versions.
69///
70/// This function takes a `VfsPath` and an iterator over crate versions as input, and returns the maximum version found.
71///
72/// The `update_config` function is used to update each configuration in the iterator. The resulting versions are aggregated using the `max` method of the `Ord` trait.
73///
74/// # Arguments
75///
76/// * `path`: A `VfsPath` representing the path to the configurations.
77/// * `iter`: An iterator over crate versions.
78/// * `incr`: An increment value for versioning (currently unused).
79///
80/// # Returns
81///
82/// The maximum version found among the aggregated configurations.
83pub fn update_configs<I>(path: &VfsPath, iter: &mut I, incr: Increment) -> Result<Version>
84where
85    I: Iterator<Item = CrateVersion>,
86{
87    let result = Version::parse("0.0.0")?;
88
89    let result = iter
90        .by_ref()
91        .map(|config| update_config(path, &config, incr))
92        .filter_map(std::result::Result::ok)
93        .fold(result, std::cmp::Ord::max);
94
95    Ok(result)
96}
97
98/// Updates the version configuration in the given `VfsPath` based on the provided `CrateVersion` and Increment.
99///
100/// # Arguments
101///
102/// * `path` - The root path where the configuration file is located.
103/// * `version` - The `CrateVersion` instance which contains the path and places where the version needs to be updated.
104/// * `incr` - The Increment enum value indicating the type of version increment (Major, Minor, Patch).
105///
106/// # Returns
107///
108/// * `Result<Version>` - The updated version after applying the increment.
109///
110/// # Errors
111///
112/// This function will return an error if:
113/// * The path provided is invalid or cannot be accessed.
114/// * The configuration file cannot be opened or read.
115/// * The content of the configuration file cannot be parsed.
116/// * The new version cannot be written back to the configuration file.
117///
118/// # Note
119///
120/// If the `version.path` is empty, the function will use the provided `path` as the working configuration path.
121/// Otherwise, it will construct the path using the `version.path` and `CARGO_CONFIG`.
122pub fn update_config(path: &VfsPath, version: &CrateVersion, incr: Increment) -> Result<Version> {
123    let working_config_path: &VfsPath;
124    let member_config_path: VfsPath;
125    if version.path.is_empty() {
126        working_config_path = path;
127    } else {
128        let parent = path.parent();
129        member_config_path = parent.join(&version.path)?.join(CARGO_CONFIG)?;
130        working_config_path = &member_config_path;
131    }
132
133    let mut file = working_config_path.open_file()?;
134    let mut content = String::new();
135    file.read_to_string(&mut content)?;
136
137    let mut doc = content.parse::<DocumentMut>()?;
138
139    let mut result = Version::parse("0.0.0")?;
140
141    for place in &version.places {
142        match place {
143            Place::Package(ver) => {
144                let v = increment(ver, incr)?;
145                result = result.max(v);
146                doc[PACK][VERSION] = value(result.to_string());
147            }
148            Place::Dependency(n, ver) => {
149                let v = increment(ver, incr)?;
150                result = result.max(v);
151                doc[DEPS][n][VERSION] = value(result.to_string());
152            }
153        }
154    }
155
156    let mut f = working_config_path.create_file()?;
157    let changed = doc.to_string();
158    f.write_all(changed.as_bytes())?;
159    Ok(result)
160}
161
162fn increment(v: &str, i: Increment) -> Result<Version> {
163    let mut v = Version::parse(v)?;
164    match i {
165        Increment::Major => increment_major(&mut v),
166        Increment::Minor => increment_minor(&mut v),
167        Increment::Patch => increment_patch(&mut v),
168    }
169    Ok(v)
170}
171
172fn new_cargo_config_path(root: &VfsPath) -> Result<VfsPath> {
173    Ok(root.join(CARGO_CONFIG)?)
174}
175
176/// Increments the patch version of the given `Version` instance.
177///
178/// This function increments the patch version component of the provided
179/// `Version` instance by 1. Additionally, it resets the pre-release and
180/// build metadata components to empty values.
181///
182/// # Arguments
183///
184/// * `v` - A mutable reference to a `Version` instance that will be modified.
185///
186/// # Note
187///
188/// This function assumes that the `Version` instance passed in is valid and
189/// properly initialized. It does not perform any validation on the input.
190///
191/// # See Also
192///
193/// * `increment_minor` - For incrementing the minor version component.
194/// * `increment_major` - For incrementing the major version component.
195fn increment_patch(v: &mut Version) {
196    v.patch += 1;
197    v.pre = Prerelease::EMPTY;
198    v.build = BuildMetadata::EMPTY;
199}
200
201/// Increments the minor version of the given `Version` instance.
202///
203/// This function increments the minor version component of the provided
204/// `Version` instance by 1. Additionally, it resets the patch, pre-release,
205/// and build metadata components to empty values.
206///
207/// # Arguments
208///
209/// * `v` - A mutable reference to a `Version` instance that will be modified.
210///
211/// # Note
212///
213/// This function assumes that the `Version` instance passed in is valid and
214/// properly initialized. It does not perform any validation on the input.
215///
216/// # See Also
217///
218/// * `increment_patch` - For incrementing the patch version component.
219/// * `increment_major` - For incrementing the major version component.
220fn increment_minor(v: &mut Version) {
221    v.minor += 1;
222    v.patch = 0;
223    v.pre = Prerelease::EMPTY;
224    v.build = BuildMetadata::EMPTY;
225}
226
227/// Increments the major version of the given `Version` instance.
228///
229/// This function increments the major version component of the provided
230/// `Version` instance by 1. Additionally, it resets the minor, patch,
231/// pre-release, and build metadata components to empty values.
232///
233/// # Arguments
234///
235/// * `v` - A mutable reference to a `Version` instance that will be modified.
236///
237/// # Note
238///
239/// This function assumes that the `Version` instance passed in is valid and
240/// properly initialized. It does not perform any validation on the input.
241///
242/// # See Also
243///
244/// * `increment_minor` - For incrementing the minor version component.
245/// * `increment_patch` - For incrementing the patch version component.
246fn increment_major(v: &mut Version) {
247    v.major += 1;
248    v.minor = 0;
249    v.patch = 0;
250    v.pre = Prerelease::EMPTY;
251    v.build = BuildMetadata::EMPTY;
252}
253
254#[derive(Deserialize)]
255struct WorkspaceConfig {
256    workspace: Workspace,
257}
258
259#[derive(Deserialize)]
260struct Workspace {
261    members: Vec<String>,
262}
263
264#[derive(Deserialize, Default)]
265struct CrateConfig {
266    package: Package,
267    dependencies: Option<HashMap<String, Dependency>>,
268}
269
270impl CrateConfig {
271    pub fn open(path: &VfsPath) -> Result<Self> {
272        let mut file = path.open_file()?;
273        let mut content = String::new();
274        file.read_to_string(&mut content)?;
275        let conf: CrateConfig = toml::from_str(&content)?;
276        Ok(conf)
277    }
278
279    pub fn new_version(&self, path: String) -> CrateVersion {
280        let places = vec![Place::Package(self.package.version.clone())];
281
282        CrateVersion { path, places }
283    }
284}
285
286#[derive(Deserialize, Default)]
287struct Package {
288    name: String,
289    version: String,
290    description: Option<String>,
291    license: Option<String>,
292    homepage: Option<String>,
293}
294
295#[derive(Deserialize, Debug)]
296#[serde(untagged)]
297enum Dependency {
298    Plain(String),
299    #[allow(dead_code)]
300    Optional(bool),
301    Object(HashMap<String, Dependency>),
302    #[allow(dead_code)]
303    List(Vec<Dependency>),
304}
305
306#[derive(Debug, Default)]
307pub struct CrateVersion {
308    path: String,
309    places: Vec<Place>,
310}
311
312/// Place defines where to find version
313#[derive(Debug)]
314pub enum Place {
315    /// Find version in package metadata (i.e. `package` section)
316    Package(String),
317    /// Find version in dependencies (i.e. `dependencies` section)
318    Dependency(String, String),
319}
320
321#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
322pub enum Increment {
323    Major,
324    Minor,
325    Patch,
326}
327
328#[cfg(test)]
329mod tests {
330    #![allow(clippy::unwrap_used)]
331    use super::*;
332    use rstest::rstest;
333
334    #[rstest]
335    #[case::patch(Increment::Patch, "0.1.2")]
336    #[case::minor(Increment::Minor, "0.2.0")]
337    #[case::major(Increment::Major, "1.0.0")]
338    #[trace]
339    fn increment_tests(#[case] incr: Increment, #[case] expected: &str) {
340        // Arrange
341        let v = "0.1.1";
342
343        // Act
344        let actual = increment(v, incr).unwrap();
345
346        // Assert
347        assert_eq!(actual, Version::parse(expected).unwrap());
348    }
349
350    #[test]
351    fn toml_parse_workspace() {
352        // Arrange
353
354        // Act
355        let cfg: WorkspaceConfig = toml::from_str(WKS).unwrap();
356
357        // Assert
358        assert_eq!(2, cfg.workspace.members.len());
359    }
360
361    #[test]
362    fn toml_parse_crate() {
363        // Arrange
364
365        // Act
366        let cfg: CrateConfig = toml::from_str(SOLV).unwrap();
367
368        // Assert
369        let deps = cfg.dependencies.unwrap();
370        assert_eq!("solv", cfg.package.name);
371        assert_eq!("0.1.13", cfg.package.version);
372        assert_eq!(6, deps.len());
373        let solp = &deps["solp"];
374        if let Dependency::Object(o) = solp {
375            assert_eq!(2, o.len());
376            assert!(o.contains_key(VERSION));
377            assert!(o.contains_key("path"));
378        }
379    }
380
381    #[test]
382    fn toml_parse_crate_with_optional_deps() {
383        // Arrange
384        let conf = r#"[package]
385name = "editorconfiger"
386version = "0.1.9"
387description = "Plain tool to validate and compare .editorconfig files"
388authors = ["egoroff <egoroff@gmail.com>"]
389keywords = ["editorconfig"]
390homepage = "https://github.com/aegoroff/editorconfiger"
391edition = "2021"
392license = "MIT"
393
394# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
395
396[build-dependencies] # <-- We added this and everything after!
397lalrpop = "0.19"
398
399[dependencies]
400lalrpop-util  = { version = "0.19", features = ["lexer"] }
401regex = "1"
402jwalk = "0.6"
403aho-corasick = "0.7"
404nom = "7"
405num_cpus = "1.13.0"
406
407ansi_term = { version = "0.12", optional = true }
408prettytable-rs = { version = "^0.8", optional = true }
409clap = { version = "2", optional = true }
410
411[dev-dependencies]
412table-test = "0.2.1"
413spectral = "0.6.0"
414rstest = "0.12.0"
415
416[features]
417build-binary = ["clap", "ansi_term", "prettytable-rs"]
418
419[[bin]]
420name = "editorconfiger"
421required-features = ["build-binary"]
422
423[profile.release]
424lto = true"#;
425
426        // Act
427        let cfg: CrateConfig = toml::from_str(conf).unwrap();
428
429        // Assert
430        let deps = cfg.dependencies.unwrap();
431        assert_eq!("editorconfiger", cfg.package.name);
432        assert_eq!("0.1.9", cfg.package.version);
433        assert_eq!(9, deps.len());
434        let ansi_term = &deps["ansi_term"];
435        if let Dependency::Object(o) = ansi_term {
436            assert_eq!(2, o.len());
437            assert!(o.contains_key(VERSION));
438            assert!(o.contains_key("optional"));
439        }
440    }
441
442    const WKS: &str = r#"
443[workspace]
444
445members = [
446    "solv",
447    "solp",
448]
449        "#;
450
451    const SOLV: &str = r#"
452[package]
453name = "solv"
454description = "Microsoft Visual Studio solution validator"
455repository = "https://github.com/aegoroff/solv"
456version = "0.1.13"
457authors = ["egoroff <egoroff@gmail.com>"]
458edition = "2018"
459license = "MIT"
460workspace = ".."
461
462# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
463
464[dependencies]
465prettytable-rs = "^0.8"
466ansi_term = "0.12"
467humantime = "2.1"
468clap = "2"
469fnv = "1"
470solp = { path = "../solp/", version = "0.1.13" }
471
472        "#;
473}