1use std::collections::HashMap;
2use std::fmt::Display;
3use std::process::{Command, Stdio};
4
5use cargo_metadata::MetadataCommand;
6
7#[derive(Debug, Clone, Copy, clap::ValueEnum)]
8enum SemverType {
9 Patch,
10 Minor,
11 Major,
12}
13
14impl Display for SemverType {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 SemverType::Patch => write!(f, "patch"),
18 SemverType::Minor => write!(f, "minor"),
19 SemverType::Major => write!(f, "major"),
20 }
21 }
22}
23
24#[derive(Debug, clap::Args)]
25pub struct SemverArgs {
26 #[clap(long = "type", default_value_t = SemverType::Minor)]
28 tpe: SemverType,
29 #[clap(long = "baseline-version")]
32 baseline_version: Option<String>,
33}
34
35impl SemverArgs {
36 pub fn run(&self) {
37 let metadata = MetadataCommand::default().exec().unwrap();
38 let false_positives_for_diesel = HashMap::from([
39 (
40 "inherent_method_missing",
41 &["SerializedDatabase::new"] as &[_],
43 ),
44 (
45 "trait_added_supertrait",
46 &["trait diesel::connection::Instrumentation gained Downcast"] as &[_],
49 ),
50 (
51 "trait_no_longer_dyn_compatible",
52 &[
57 "trait SqlOrd",
58 "trait Foldable",
59 "trait SqlType",
60 "trait SingleValue",
61 ] as &[_],
62 ),
63 ]);
64 self.run_semver_checks_for(
65 &metadata,
66 "diesel",
67 &["sqlite", "postgres", "mysql", "extras", "with-deprecated"],
68 false_positives_for_diesel,
69 );
70 self.run_semver_checks_for(&metadata, "diesel_migrations", &[], HashMap::new());
71 self.run_semver_checks_for(
72 &metadata,
73 "diesel-dynamic-schema",
74 &["postgres", "mysql", "sqlite"],
75 HashMap::new(),
76 );
77 }
78
79 fn run_semver_checks_for(
80 &self,
81 metadata: &cargo_metadata::Metadata,
82 crate_name: &str,
83 features: &[&str],
84 allow_list: HashMap<&str, &[&str]>,
85 ) {
86 let baseline_diesel_version = if let Some(ref baseline_version) = self.baseline_version {
87 baseline_version.clone()
88 } else {
89 let mut baseline_diesel_version = metadata
90 .packages
91 .iter()
92 .find_map(|c| (c.name == crate_name).then_some(&c.version))
93 .unwrap()
94 .clone();
95 if baseline_diesel_version.major != 0 {
96 baseline_diesel_version.patch = 0;
97 }
98 baseline_diesel_version.to_string()
99 };
100 let mut command = Command::new("cargo");
101 command
102 .args([
103 "semver-checks",
104 "-p",
105 crate_name,
106 "--only-explicit-features",
107 "--baseline-version",
108 &baseline_diesel_version,
109 "--release-type",
110 &self.tpe.to_string(),
111 ])
112 .current_dir(&metadata.workspace_root);
113 for f in features {
114 command.args(["--features", f]);
115 }
116 println!("Run cargo semver-checks via `{command:?}`");
117 let res = command
118 .stderr(Stdio::piped())
119 .stdout(Stdio::piped())
120 .output()
121 .unwrap();
122 let std_out = String::from_utf8(res.stdout).expect("Valid UTF-8");
123 let std_err = String::from_utf8(res.stderr).expect("Valid UTF-8");
124 let mut failed = false;
125 let mut std_out_out = String::new();
126 for lint in std_out.split("\n---") {
133 if lint.trim().is_empty() {
134 continue;
135 }
136 let (lint, content) = lint
137 .trim()
138 .strip_prefix("failure")
139 .unwrap_or(lint)
140 .trim()
141 .split_once(':')
142 .expect("Two parts exist");
143 let ignore_list = allow_list.get(lint).copied().unwrap_or_default();
144
145 let failures = content
146 .lines()
147 .skip_while(|l| !l.trim().starts_with("Failed in:"))
148 .skip(1)
149 .filter(|l| ignore_list.iter().all(|e| !l.trim().starts_with(e)))
150 .collect::<Vec<_>>();
151 let content = content
152 .lines()
153 .take_while(|l| !l.trim().starts_with("Failed in:"));
154 if !failures.is_empty() {
155 failed = true;
156 if !std_out_out.is_empty() {
157 std_out_out += "\n";
158 }
159 std_out_out += "--- failure ";
160 std_out_out += lint;
161 std_out_out += ":";
162 for l in content {
163 std_out_out += l;
164 std_out_out += "\n";
165 }
166 std_out_out += "Failed in:";
167 for failure in failures {
168 std_out_out += "\n";
169 std_out_out += failure;
170 }
171 }
172 }
173 let (front, back) = std_err.split_once("\n\n").unwrap_or((&std_err, ""));
174 eprintln!("{front}\n");
175 if failed {
176 eprintln!("Cargo semver check failed");
177 println!("{std_out_out}");
178 println!();
179 }
180 eprintln!("{back}");
181 if failed {
182 std::process::exit(1);
183 }
184 }
185}