1use std::str::FromStr;
2
3pub mod changes;
4pub mod commit;
5pub mod config;
6pub mod hook;
7pub mod owner;
8pub mod plan;
9pub mod publish;
10pub mod push;
11pub mod release;
12pub mod replace;
13pub mod tag;
14pub mod version;
15
16use crate::error::CargoResult;
17use crate::ops::version::VersionExt as _;
18
19pub fn verify_git_is_clean(
20 path: &std::path::Path,
21 dry_run: bool,
22 level: log::Level,
23) -> Result<bool, crate::error::CliError> {
24 let mut success = true;
25 if let Some(dirty) = crate::ops::git::is_dirty(path)? {
26 let _ = crate::ops::shell::log(
27 level,
28 format!(
29 "uncommitted changes detected, please resolve before release:\n {}",
30 dirty.join("\n ")
31 ),
32 );
33 if level == log::Level::Error {
34 success = false;
35 if !dry_run {
36 return Err(101.into());
37 }
38 }
39 }
40 Ok(success)
41}
42
43pub fn verify_tags_missing(
44 pkgs: &[plan::PackageRelease],
45 dry_run: bool,
46 level: log::Level,
47) -> Result<bool, crate::error::CliError> {
48 let mut success = true;
49
50 let mut tag_exists = false;
51 let mut seen_tags = std::collections::HashSet::new();
52 for pkg in pkgs {
53 if let Some(tag_name) = pkg.planned_tag.as_ref() {
54 if seen_tags.insert(tag_name) {
55 let cwd = &pkg.package_root;
56 if crate::ops::git::tag_exists(cwd, tag_name)? {
57 let crate_name = pkg.meta.name.as_str();
58 let _ = crate::ops::shell::log(
59 level,
60 format!("tag `{tag_name}` already exists (for `{crate_name}`)"),
61 );
62 tag_exists = true;
63 }
64 }
65 }
66 }
67 if tag_exists && level == log::Level::Error {
68 success = false;
69 if !dry_run {
70 return Err(101.into());
71 }
72 }
73
74 Ok(success)
75}
76
77pub fn verify_tags_exist(
78 pkgs: &[plan::PackageRelease],
79 dry_run: bool,
80 level: log::Level,
81) -> Result<bool, crate::error::CliError> {
82 let mut success = true;
83
84 let mut tag_missing = false;
85 let mut seen_tags = std::collections::HashSet::new();
86 for pkg in pkgs {
87 if let Some(tag_name) = pkg.planned_tag.as_ref() {
88 if seen_tags.insert(tag_name) {
89 let cwd = &pkg.package_root;
90 if !crate::ops::git::tag_exists(cwd, tag_name)? {
91 let crate_name = pkg.meta.name.as_str();
92 let _ = crate::ops::shell::log(
93 level,
94 format!("tag `{tag_name}` doesn't exist (for `{crate_name}`)"),
95 );
96 tag_missing = true;
97 }
98 }
99 }
100 }
101 if tag_missing && level == log::Level::Error {
102 success = false;
103 if !dry_run {
104 return Err(101.into());
105 }
106 }
107
108 Ok(success)
109}
110
111pub fn verify_git_branch(
112 path: &std::path::Path,
113 ws_config: &crate::config::Config,
114 dry_run: bool,
115 level: log::Level,
116) -> Result<bool, crate::error::CliError> {
117 use itertools::Itertools;
118
119 let mut success = true;
120
121 let branch = crate::ops::git::current_branch(path)?;
122 let mut good_branches = ignore::gitignore::GitignoreBuilder::new(".");
123 for pattern in ws_config.allow_branch() {
124 good_branches.add_line(None, pattern)?;
125 }
126 let good_branches = good_branches.build()?;
127 let good_branch_match = good_branches.matched_path_or_any_parents(&branch, false);
128 if !good_branch_match.is_ignore() {
129 let allowed = ws_config
130 .allow_branch()
131 .map(|b| format!("`{b}`"))
132 .join(", ");
133 let _ = crate::ops::shell::log(
134 level,
135 format!(
136 "cannot release from branch `{branch}` as it doesn't match {allowed}; either switch to an allowed branch or add this branch to `allow-branch`",
137 ),
138 );
139 log::trace!("due to {:?}", good_branch_match);
140 if level == log::Level::Error {
141 success = false;
142 if !dry_run {
143 return Err(101.into());
144 }
145 }
146 }
147
148 Ok(success)
149}
150
151pub fn verify_if_behind(
152 path: &std::path::Path,
153 ws_config: &crate::config::Config,
154 dry_run: bool,
155 level: log::Level,
156) -> Result<bool, crate::error::CliError> {
157 let mut success = true;
158
159 if !ws_config.push() {
161 return Ok(success);
162 }
163
164 let git_remote = ws_config.push_remote();
165 let branch = crate::ops::git::current_branch(path)?;
166 crate::ops::git::fetch(path, git_remote, &branch)?;
167 if crate::ops::git::is_behind_remote(path, git_remote, &branch)? {
168 let _ = crate::ops::shell::log(level, format!("{branch} is behind {git_remote}/{branch}"));
169 if level == log::Level::Error {
170 success = false;
171 if !dry_run {
172 return Err(101.into());
173 }
174 }
175 }
176
177 Ok(success)
178}
179
180pub fn verify_monotonically_increasing(
181 pkgs: &[plan::PackageRelease],
182 dry_run: bool,
183 level: log::Level,
184) -> Result<bool, crate::error::CliError> {
185 let mut success = true;
186
187 let mut downgrades_present = false;
188 for pkg in pkgs {
189 if let Some(version) = pkg.planned_version.as_ref() {
190 if version.full_version < pkg.initial_version.full_version {
191 let crate_name = pkg.meta.name.as_str();
192 let _ = crate::ops::shell::log(
193 level,
194 format!(
195 "cannot downgrade {} from {} to {}",
196 crate_name, version.full_version, pkg.initial_version.full_version
197 ),
198 );
199 downgrades_present = true;
200 }
201 }
202 }
203 if downgrades_present && level == log::Level::Error {
204 success = false;
205 if !dry_run {
206 return Err(101.into());
207 }
208 }
209
210 Ok(success)
211}
212
213pub fn verify_rate_limit(
214 pkgs: &[plan::PackageRelease],
215 index: &mut crate::ops::index::CratesIoIndex,
216 rate_limit: &crate::config::RateLimit,
217 dry_run: bool,
218 level: log::Level,
219) -> Result<bool, crate::error::CliError> {
220 let mut success = true;
221
222 let mut new = 0;
226 let mut existing = 0;
227 for pkg in pkgs {
228 if pkg.config.registry().is_none() && pkg.config.publish() {
230 let crate_name = pkg.meta.name.as_str();
231 if index.has_krate(None, crate_name, pkg.config.certs_source())? {
232 existing += 1;
233 } else {
234 new += 1;
235 }
236 }
237 }
238
239 if rate_limit.new_packages() < new {
240 success = false;
242 let _ = crate::ops::shell::log(
243 level,
244 format!(
245 "attempting to publish {} new crates which is above the rate limit: {}",
246 new,
247 rate_limit.new_packages()
248 ),
249 );
250 }
251
252 if rate_limit.existing_packages() < existing {
253 success = false;
255 let _ = crate::ops::shell::log(
256 level,
257 format!(
258 "attempting to publish {} existing crates which is above the rate limit: {}",
259 existing,
260 rate_limit.existing_packages()
261 ),
262 );
263 }
264
265 if !success && level == log::Level::Error && !dry_run {
266 return Err(101.into());
267 }
268
269 Ok(success)
270}
271
272pub fn verify_metadata(
273 pkgs: &[plan::PackageRelease],
274 dry_run: bool,
275 level: log::Level,
276) -> Result<bool, crate::error::CliError> {
277 let mut success = true;
278
279 for pkg in pkgs {
280 if !pkg.config.publish() {
281 continue;
282 }
283 let mut missing = Vec::new();
284
285 if pkg
287 .meta
288 .description
289 .as_deref()
290 .unwrap_or_default()
291 .is_empty()
292 {
293 missing.push("description");
294 }
295 if pkg.meta.license.as_deref().unwrap_or_default().is_empty()
296 && pkg.meta.license_file.is_none()
297 {
298 missing.push("license || license-file");
299 }
300 if pkg
301 .meta
302 .documentation
303 .as_deref()
304 .unwrap_or_default()
305 .is_empty()
306 && pkg.meta.homepage.as_deref().unwrap_or_default().is_empty()
307 && pkg
308 .meta
309 .repository
310 .as_deref()
311 .unwrap_or_default()
312 .is_empty()
313 {
314 missing.push("documentation || homepage || repository");
315 }
316
317 if !missing.is_empty() {
318 let _ = crate::ops::shell::log(
319 level,
320 format!(
321 "{} is missing the following fields:\n {}",
322 pkg.meta.name,
323 missing.join("\n ")
324 ),
325 );
326 success = false;
327 }
328 }
329
330 if !success && level == log::Level::Error && !dry_run {
331 return Err(101.into());
332 }
333
334 Ok(success)
335}
336
337pub fn warn_changed(
338 ws_meta: &cargo_metadata::Metadata,
339 pkgs: &[plan::PackageRelease],
340) -> Result<(), crate::error::CliError> {
341 let mut changed_pkgs = std::collections::HashSet::new();
342 for pkg in pkgs {
343 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
344 let crate_name = pkg.meta.name.as_str();
345 if let Some(prior_tag_name) = &pkg.prior_tag {
346 if let Some(changed) = version::changed_since(ws_meta, pkg, prior_tag_name) {
347 if !changed.is_empty() {
348 log::debug!(
349 "Files changed in {} since {}: {:#?}",
350 crate_name,
351 prior_tag_name,
352 changed
353 );
354 changed_pkgs.insert(&pkg.meta.id);
355 if changed.len() == 1 && changed[0].ends_with("Cargo.lock") {
356 } else {
358 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
359 }
360 } else if changed_pkgs.contains(&pkg.meta.id) {
361 log::debug!(
362 "Dependency changed for {} since {}",
363 crate_name,
364 prior_tag_name,
365 );
366 changed_pkgs.insert(&pkg.meta.id);
367 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
368 } else {
369 let _ = crate::ops::shell::warn(format!(
370 "updating {} to {} despite no changes made since tag {}",
371 crate_name, version.full_version_string, prior_tag_name
372 ));
373 }
374 } else {
375 log::debug!(
376 "cannot detect changes for {} because tag {} is missing. Try setting `--prev-tag-name <TAG>`.",
377 crate_name,
378 prior_tag_name
379 );
380 }
381 } else {
382 log::debug!(
383 "cannot detect changes for {} because no tag was found. Try setting `--prev-tag-name <TAG>`.",
384 crate_name,
385 );
386 }
387 }
388
389 Ok(())
390}
391
392pub fn find_shared_versions(
393 pkgs: &[plan::PackageRelease],
394) -> Result<Option<plan::Version>, crate::error::CliError> {
395 let mut is_shared = true;
396 let mut shared_versions: std::collections::HashMap<&str, &plan::Version> = Default::default();
397 for pkg in pkgs {
398 let group_name = if let Some(group_name) = pkg.config.shared_version() {
399 group_name
400 } else {
401 continue;
402 };
403 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
404 match shared_versions.entry(group_name) {
405 std::collections::hash_map::Entry::Occupied(existing) => {
406 if version.bare_version != existing.get().bare_version {
407 is_shared = false;
408 let _ = crate::ops::shell::error(format!(
409 "{} has version {}, should be {}",
410 pkg.meta.name,
411 version.bare_version_string,
412 existing.get().bare_version_string
413 ));
414 }
415 }
416 std::collections::hash_map::Entry::Vacant(vacant) => {
417 vacant.insert(version);
418 }
419 }
420 }
421 if !is_shared {
422 let _ = crate::ops::shell::error("crate versions deviated, aborting");
423 return Err(101.into());
424 }
425
426 if shared_versions.len() == 1 {
427 Ok(shared_versions.values().next().map(|s| (*s).clone()))
428 } else {
429 Ok(None)
430 }
431}
432
433pub fn consolidate_commits(
434 selected_pkgs: &[plan::PackageRelease],
435 excluded_pkgs: &[plan::PackageRelease],
436) -> Result<bool, crate::error::CliError> {
437 let mut consolidate_commits = None;
438 for pkg in selected_pkgs.iter().chain(excluded_pkgs.iter()) {
439 let current = Some(pkg.config.consolidate_commits());
440 if consolidate_commits.is_none() {
441 consolidate_commits = current;
442 } else if consolidate_commits != current {
443 let _ = crate::ops::shell::error("inconsistent `consolidate-commits` setting");
444 return Err(101.into());
445 }
446 }
447 Ok(consolidate_commits.expect("at least one package"))
448}
449
450pub fn confirm(
451 step: &str,
452 pkgs: &[plan::PackageRelease],
453 no_confirm: bool,
454 dry_run: bool,
455) -> Result<(), crate::error::CliError> {
456 if !dry_run && !no_confirm {
457 let prompt = if pkgs.len() == 1 {
458 let pkg = &pkgs[0];
459 let crate_name = pkg.meta.name.as_str();
460 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
461 format!("{} {} {}?", step, crate_name, version.full_version_string)
462 } else {
463 use std::io::Write;
464
465 let mut buffer: Vec<u8> = vec![];
466 writeln!(&mut buffer, "{step}").unwrap();
467 for pkg in pkgs {
468 let crate_name = pkg.meta.name.as_str();
469 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
470 writeln!(
471 &mut buffer,
472 " {} {}",
473 crate_name, version.full_version_string
474 )
475 .unwrap();
476 }
477 write!(&mut buffer, "?").unwrap();
478 String::from_utf8(buffer).expect("Only valid UTF-8 has been written")
479 };
480
481 let confirmed = crate::ops::shell::confirm(&prompt);
482 if !confirmed {
483 return Err(0.into());
484 }
485 }
486
487 Ok(())
488}
489
490pub fn finish(failed: bool, dry_run: bool) -> Result<(), crate::error::CliError> {
491 if dry_run {
492 if failed {
493 let _ =
494 crate::ops::shell::error("dry-run failed, resolve the above errors and try again.");
495 Err(101.into())
496 } else {
497 let _ =
498 crate::ops::shell::warn("aborting release due to dry run; re-run with `--execute`");
499 Ok(())
500 }
501 } else {
502 Ok(())
503 }
504}
505
506#[derive(Clone, Debug)]
507pub enum TargetVersion {
508 Relative(BumpLevel),
509 Absolute(semver::Version),
510}
511
512impl TargetVersion {
513 pub fn bump(
514 &self,
515 current: &semver::Version,
516 metadata: Option<&str>,
517 ) -> CargoResult<Option<plan::Version>> {
518 match self {
519 TargetVersion::Relative(bump_level) => {
520 let mut potential_version = current.to_owned();
521 bump_level.bump_version(&mut potential_version, metadata)?;
522 if potential_version != *current {
523 let full_version = potential_version;
524 let version = plan::Version::from(full_version);
525 Ok(Some(version))
526 } else {
527 Ok(None)
528 }
529 }
530 TargetVersion::Absolute(version) => {
531 let mut full_version = version.to_owned();
532 if full_version.build.is_empty() {
533 if let Some(metadata) = metadata {
534 full_version.build = semver::BuildMetadata::new(metadata)?;
535 } else {
536 full_version.build = current.build.clone();
537 }
538 }
539 let version = plan::Version::from(full_version);
540 if version.bare_version != plan::Version::from(current.clone()).bare_version {
541 Ok(Some(version))
542 } else {
543 Ok(None)
544 }
545 }
546 }
547 }
548}
549
550impl Default for TargetVersion {
551 fn default() -> Self {
552 TargetVersion::Relative(BumpLevel::Release)
553 }
554}
555
556impl std::fmt::Display for TargetVersion {
557 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
558 match self {
559 TargetVersion::Relative(bump_level) => {
560 write!(f, "{bump_level}")
561 }
562 TargetVersion::Absolute(version) => {
563 write!(f, "{version}")
564 }
565 }
566 }
567}
568
569impl FromStr for TargetVersion {
570 type Err = String;
571
572 fn from_str(s: &str) -> Result<Self, Self::Err> {
573 if let Ok(bump_level) = BumpLevel::from_str(s) {
574 Ok(TargetVersion::Relative(bump_level))
575 } else {
576 Ok(TargetVersion::Absolute(
577 semver::Version::parse(s).map_err(|e| e.to_string())?,
578 ))
579 }
580 }
581}
582
583impl clap::builder::ValueParserFactory for TargetVersion {
584 type Parser = TargetVersionParser;
585
586 fn value_parser() -> Self::Parser {
587 TargetVersionParser
588 }
589}
590
591#[derive(Copy, Clone)]
592pub struct TargetVersionParser;
593
594impl clap::builder::TypedValueParser for TargetVersionParser {
595 type Value = TargetVersion;
596
597 fn parse_ref(
598 &self,
599 cmd: &clap::Command,
600 arg: Option<&clap::Arg>,
601 value: &std::ffi::OsStr,
602 ) -> Result<Self::Value, clap::Error> {
603 let inner_parser = TargetVersion::from_str;
604 inner_parser.parse_ref(cmd, arg, value)
605 }
606
607 fn possible_values(
608 &self,
609 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
610 let inner_parser = clap::builder::EnumValueParser::<BumpLevel>::new();
611 #[allow(clippy::needless_collect)] inner_parser.possible_values().map(|ps| {
613 let ps = ps.collect::<Vec<_>>();
614 let ps: Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_> =
615 Box::new(ps.into_iter());
616 ps
617 })
618 }
619}
620
621#[derive(Debug, Clone, Copy, clap::ValueEnum)]
622#[value(rename_all = "kebab-case")]
623pub enum BumpLevel {
624 Major,
626 Minor,
628 Patch,
630 Release,
632 Rc,
634 Beta,
636 Alpha,
638}
639
640impl std::fmt::Display for BumpLevel {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 use clap::ValueEnum;
643
644 self.to_possible_value()
645 .expect("no values are skipped")
646 .get_name()
647 .fmt(f)
648 }
649}
650
651impl FromStr for BumpLevel {
652 type Err = String;
653
654 fn from_str(s: &str) -> Result<Self, Self::Err> {
655 use clap::ValueEnum;
656
657 for variant in Self::value_variants() {
658 if variant.to_possible_value().unwrap().matches(s, false) {
659 return Ok(*variant);
660 }
661 }
662 Err(format!("Invalid variant: {s}"))
663 }
664}
665
666impl BumpLevel {
667 pub fn bump_version(
668 self,
669 version: &mut semver::Version,
670 metadata: Option<&str>,
671 ) -> CargoResult<()> {
672 match self {
673 BumpLevel::Major => {
674 version.increment_major();
675 }
676 BumpLevel::Minor => {
677 version.increment_minor();
678 }
679 BumpLevel::Patch => {
680 if !version.is_prerelease() {
681 version.increment_patch();
682 } else {
683 version.pre = semver::Prerelease::EMPTY;
684 }
685 }
686 BumpLevel::Release => {
687 if version.is_prerelease() {
688 version.pre = semver::Prerelease::EMPTY;
689 }
690 }
691 BumpLevel::Rc => {
692 version.increment_rc()?;
693 }
694 BumpLevel::Beta => {
695 version.increment_beta()?;
696 }
697 BumpLevel::Alpha => {
698 version.increment_alpha()?;
699 }
700 };
701
702 if let Some(metadata) = metadata {
703 version.metadata(metadata)?;
704 }
705
706 Ok(())
707 }
708}