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 && seen_tags.insert(tag_name)
55 {
56 let cwd = &pkg.package_root;
57 if crate::ops::git::tag_exists(cwd, tag_name)? {
58 let crate_name = pkg.meta.name.as_str();
59 let _ = crate::ops::shell::log(
60 level,
61 format!("tag `{tag_name}` already exists (for `{crate_name}`)"),
62 );
63 tag_exists = true;
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 && seen_tags.insert(tag_name)
89 {
90 let cwd = &pkg.package_root;
91 if !crate::ops::git::tag_exists(cwd, tag_name)? {
92 let crate_name = pkg.meta.name.as_str();
93 let _ = crate::ops::shell::log(
94 level,
95 format!("tag `{tag_name}` doesn't exist (for `{crate_name}`)"),
96 );
97 tag_missing = true;
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 title = format!("{branch} is behind {git_remote}/{branch}");
169 if let Some(level) = crate::ops::shell::level(level) {
170 let report = &[
171 annotate_snippets::Group::with_title(level.primary_title(title)),
172 annotate_snippets::Group::with_title(
173 annotate_snippets::Level::HELP.primary_title(format!("to update your release branch, run `git pull --rebase {git_remote} {branch}`")),
174 ),
175 ];
176 let _ = crate::ops::shell::print_report(report);
177 } else {
178 let _ = crate::ops::shell::log(level, title);
179 }
180 if level == log::Level::Error {
181 success = false;
182 if !dry_run {
183 return Err(101.into());
184 }
185 }
186 }
187
188 Ok(success)
189}
190
191pub fn verify_monotonically_increasing(
192 pkgs: &[plan::PackageRelease],
193 dry_run: bool,
194 level: log::Level,
195) -> Result<bool, crate::error::CliError> {
196 let mut success = true;
197
198 let mut downgrades_present = false;
199 for pkg in pkgs {
200 if let Some(version) = pkg.planned_version.as_ref()
201 && version.full_version < pkg.initial_version.full_version
202 {
203 let crate_name = pkg.meta.name.as_str();
204 let _ = crate::ops::shell::log(
205 level,
206 format!(
207 "cannot downgrade {} from {} to {}",
208 crate_name, version.full_version, pkg.initial_version.full_version
209 ),
210 );
211 downgrades_present = true;
212 }
213 }
214 if downgrades_present && level == log::Level::Error {
215 success = false;
216 if !dry_run {
217 return Err(101.into());
218 }
219 }
220
221 Ok(success)
222}
223
224pub fn verify_rate_limit(
225 pkgs: &[plan::PackageRelease],
226 index: &mut crate::ops::index::CratesIoIndex,
227 rate_limit: &crate::config::RateLimit,
228 dry_run: bool,
229 level: log::Level,
230) -> Result<bool, crate::error::CliError> {
231 let mut success = true;
232
233 let mut new = 0;
237 let mut existing = 0;
238 for pkg in pkgs {
239 if pkg.config.registry().is_none() && pkg.config.publish() {
241 let crate_name = pkg.meta.name.as_str();
242 if index.has_krate(None, crate_name, pkg.config.certs_source())? {
243 existing += 1;
244 } else {
245 new += 1;
246 }
247 }
248 }
249
250 if rate_limit.new_packages() < new {
251 success = false;
253 let _ = crate::ops::shell::log(
254 level,
255 format!(
256 "attempting to publish {} new crates which is above the rate limit: {}",
257 new,
258 rate_limit.new_packages()
259 ),
260 );
261 }
262
263 if rate_limit.existing_packages() < existing {
264 success = false;
266 let _ = crate::ops::shell::log(
267 level,
268 format!(
269 "attempting to publish {} existing crates which is above the rate limit: {}",
270 existing,
271 rate_limit.existing_packages()
272 ),
273 );
274 }
275
276 if !success && level == log::Level::Error && !dry_run {
277 return Err(101.into());
278 }
279
280 Ok(success)
281}
282
283pub fn verify_metadata(
284 pkgs: &[plan::PackageRelease],
285 dry_run: bool,
286 level: log::Level,
287) -> Result<bool, crate::error::CliError> {
288 let mut success = true;
289
290 for pkg in pkgs {
291 if !pkg.config.publish() {
292 continue;
293 }
294 let mut missing = Vec::new();
295
296 if pkg
298 .meta
299 .description
300 .as_deref()
301 .unwrap_or_default()
302 .is_empty()
303 {
304 missing.push("description");
305 }
306 if pkg.meta.license.as_deref().unwrap_or_default().is_empty()
307 && pkg.meta.license_file.is_none()
308 {
309 missing.push("license || license-file");
310 }
311 if pkg
312 .meta
313 .documentation
314 .as_deref()
315 .unwrap_or_default()
316 .is_empty()
317 && pkg.meta.homepage.as_deref().unwrap_or_default().is_empty()
318 && pkg
319 .meta
320 .repository
321 .as_deref()
322 .unwrap_or_default()
323 .is_empty()
324 {
325 missing.push("documentation || homepage || repository");
326 }
327
328 if !missing.is_empty() {
329 let _ = crate::ops::shell::log(
330 level,
331 format!(
332 "{} is missing the following fields:\n {}",
333 pkg.meta.name,
334 missing.join("\n ")
335 ),
336 );
337 success = false;
338 }
339 }
340
341 if !success && level == log::Level::Error && !dry_run {
342 return Err(101.into());
343 }
344
345 Ok(success)
346}
347
348pub fn warn_changed(
349 ws_meta: &cargo_metadata::Metadata,
350 pkgs: &[plan::PackageRelease],
351) -> Result<(), crate::error::CliError> {
352 let mut changed_pkgs = std::collections::HashSet::new();
353 for pkg in pkgs {
354 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
355 let crate_name = pkg.meta.name.as_str();
356 if let Some(prior_tag_name) = &pkg.prior_tag {
357 if let Some(changed) = version::changed_since(ws_meta, pkg, prior_tag_name) {
358 if !changed.is_empty() {
359 log::debug!(
360 "Files changed in {crate_name} since {prior_tag_name}: {changed:#?}"
361 );
362 changed_pkgs.insert(&pkg.meta.id);
363 if changed.len() == 1 && changed[0].ends_with("Cargo.lock") {
364 } else {
366 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
367 }
368 } else if changed_pkgs.contains(&pkg.meta.id) {
369 log::debug!("Dependency changed for {crate_name} since {prior_tag_name}",);
370 changed_pkgs.insert(&pkg.meta.id);
371 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
372 } else {
373 let _ = crate::ops::shell::warn(format!(
374 "updating {} to {} despite no changes made since tag {}",
375 crate_name, version.full_version_string, prior_tag_name
376 ));
377 }
378 } else {
379 log::debug!(
380 "cannot detect changes for {crate_name} because tag {prior_tag_name} is missing. Try setting `--prev-tag-name <TAG>`."
381 );
382 }
383 } else {
384 log::debug!(
385 "cannot detect changes for {crate_name} because no tag was found. Try setting `--prev-tag-name <TAG>`.",
386 );
387 }
388 }
389
390 Ok(())
391}
392
393pub fn find_shared_versions(
394 pkgs: &[plan::PackageRelease],
395) -> Result<Option<plan::Version>, crate::error::CliError> {
396 let mut is_shared = true;
397 let mut shared_versions: std::collections::HashMap<&str, &plan::Version> = Default::default();
398 for pkg in pkgs {
399 let group_name = if let Some(group_name) = pkg.config.shared_version() {
400 group_name
401 } else {
402 continue;
403 };
404 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
405 match shared_versions.entry(group_name) {
406 std::collections::hash_map::Entry::Occupied(existing) => {
407 if version.bare_version != existing.get().bare_version {
408 is_shared = false;
409 let _ = crate::ops::shell::error(format!(
410 "{} has version {}, should be {}",
411 pkg.meta.name,
412 version.bare_version_string,
413 existing.get().bare_version_string
414 ));
415 }
416 }
417 std::collections::hash_map::Entry::Vacant(vacant) => {
418 vacant.insert(version);
419 }
420 }
421 }
422 if !is_shared {
423 let _ = crate::ops::shell::error("crate versions deviated, aborting");
424 return Err(101.into());
425 }
426
427 if shared_versions.len() == 1 {
428 Ok(shared_versions.values().next().map(|s| (*s).clone()))
429 } else {
430 Ok(None)
431 }
432}
433
434pub fn consolidate_commits(
435 selected_pkgs: &[plan::PackageRelease],
436 excluded_pkgs: &[plan::PackageRelease],
437) -> Result<bool, crate::error::CliError> {
438 let mut consolidate_commits = None;
439 for pkg in selected_pkgs.iter().chain(excluded_pkgs.iter()) {
440 let current = Some(pkg.config.consolidate_commits());
441 if consolidate_commits.is_none() {
442 consolidate_commits = current;
443 } else if consolidate_commits != current {
444 let _ = crate::ops::shell::error("inconsistent `consolidate-commits` setting");
445 return Err(101.into());
446 }
447 }
448 Ok(consolidate_commits.expect("at least one package"))
449}
450
451pub fn confirm(
452 step: &str,
453 pkgs: &[plan::PackageRelease],
454 no_confirm: bool,
455 dry_run: bool,
456) -> Result<(), crate::error::CliError> {
457 if !dry_run && !no_confirm {
458 let prompt = if pkgs.len() == 1 {
459 let pkg = &pkgs[0];
460 let crate_name = pkg.meta.name.as_str();
461 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
462 format!("{} {} {}?", step, crate_name, version.full_version_string)
463 } else {
464 use std::io::Write;
465
466 let mut buffer: Vec<u8> = vec![];
467 writeln!(&mut buffer, "{step}").unwrap();
468 for pkg in pkgs {
469 let crate_name = pkg.meta.name.as_str();
470 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
471 writeln!(
472 &mut buffer,
473 " {} {}",
474 crate_name, version.full_version_string
475 )
476 .unwrap();
477 }
478 write!(&mut buffer, "?").unwrap();
479 String::from_utf8(buffer).expect("Only valid UTF-8 has been written")
480 };
481
482 let confirmed = crate::ops::shell::confirm(&prompt);
483 if !confirmed {
484 return Err(0.into());
485 }
486 }
487
488 Ok(())
489}
490
491pub fn finish(failed: bool, dry_run: bool) -> Result<(), crate::error::CliError> {
492 if dry_run {
493 if failed {
494 let _ =
495 crate::ops::shell::error("dry-run failed, resolve the above errors and try again.");
496 Err(101.into())
497 } else {
498 let _ =
499 crate::ops::shell::warn("aborting release due to dry run; re-run with `--execute`");
500 Ok(())
501 }
502 } else {
503 Ok(())
504 }
505}
506
507#[derive(Clone, Debug)]
508pub enum TargetVersion {
509 Relative(BumpLevel),
510 Absolute(semver::Version),
511}
512
513impl TargetVersion {
514 pub fn bump(
515 &self,
516 current: &semver::Version,
517 metadata: Option<&str>,
518 ) -> CargoResult<Option<plan::Version>> {
519 match self {
520 TargetVersion::Relative(bump_level) => {
521 let mut potential_version = current.to_owned();
522 bump_level.bump_version(&mut potential_version, metadata)?;
523 if potential_version != *current {
524 let full_version = potential_version;
525 let version = plan::Version::from(full_version);
526 Ok(Some(version))
527 } else {
528 Ok(None)
529 }
530 }
531 TargetVersion::Absolute(version) => {
532 let mut full_version = version.to_owned();
533 if full_version.build.is_empty() {
534 if let Some(metadata) = metadata {
535 full_version.build = semver::BuildMetadata::new(metadata)?;
536 } else {
537 full_version.build = current.build.clone();
538 }
539 }
540 let version = plan::Version::from(full_version);
541 if version.bare_version != plan::Version::from(current.clone()).bare_version {
542 Ok(Some(version))
543 } else {
544 Ok(None)
545 }
546 }
547 }
548 }
549}
550
551impl Default for TargetVersion {
552 fn default() -> Self {
553 TargetVersion::Relative(BumpLevel::Release)
554 }
555}
556
557impl std::fmt::Display for TargetVersion {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
559 match self {
560 TargetVersion::Relative(bump_level) => {
561 write!(f, "{bump_level}")
562 }
563 TargetVersion::Absolute(version) => {
564 write!(f, "{version}")
565 }
566 }
567 }
568}
569
570impl FromStr for TargetVersion {
571 type Err = String;
572
573 fn from_str(s: &str) -> Result<Self, Self::Err> {
574 if let Ok(bump_level) = BumpLevel::from_str(s) {
575 Ok(TargetVersion::Relative(bump_level))
576 } else {
577 Ok(TargetVersion::Absolute(
578 semver::Version::parse(s).map_err(|e| e.to_string())?,
579 ))
580 }
581 }
582}
583
584impl clap::builder::ValueParserFactory for TargetVersion {
585 type Parser = TargetVersionParser;
586
587 fn value_parser() -> Self::Parser {
588 TargetVersionParser
589 }
590}
591
592#[derive(Copy, Clone)]
593pub struct TargetVersionParser;
594
595impl clap::builder::TypedValueParser for TargetVersionParser {
596 type Value = TargetVersion;
597
598 fn parse_ref(
599 &self,
600 cmd: &clap::Command,
601 arg: Option<&clap::Arg>,
602 value: &std::ffi::OsStr,
603 ) -> Result<Self::Value, clap::Error> {
604 let inner_parser = TargetVersion::from_str;
605 inner_parser.parse_ref(cmd, arg, value)
606 }
607
608 fn possible_values(
609 &self,
610 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
611 let inner_parser = clap::builder::EnumValueParser::<BumpLevel>::new();
612 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}