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)] extern 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#[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
68pub 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
98pub 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
176fn increment_patch(v: &mut Version) {
196 v.patch += 1;
197 v.pre = Prerelease::EMPTY;
198 v.build = BuildMetadata::EMPTY;
199}
200
201fn 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
227fn 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#[derive(Debug)]
314pub enum Place {
315 Package(String),
317 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 let v = "0.1.1";
342
343 let actual = increment(v, incr).unwrap();
345
346 assert_eq!(actual, Version::parse(expected).unwrap());
348 }
349
350 #[test]
351 fn toml_parse_workspace() {
352 let cfg: WorkspaceConfig = toml::from_str(WKS).unwrap();
356
357 assert_eq!(2, cfg.workspace.members.len());
359 }
360
361 #[test]
362 fn toml_parse_crate() {
363 let cfg: CrateConfig = toml::from_str(SOLV).unwrap();
367
368 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 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 let cfg: CrateConfig = toml::from_str(conf).unwrap();
428
429 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}