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 _ = 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 && version.full_version < pkg.initial_version.full_version
191 {
192 let crate_name = pkg.meta.name.as_str();
193 let _ = crate::ops::shell::log(
194 level,
195 format!(
196 "cannot downgrade {} from {} to {}",
197 crate_name, version.full_version, pkg.initial_version.full_version
198 ),
199 );
200 downgrades_present = true;
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 {crate_name} since {prior_tag_name}: {changed:#?}"
350 );
351 changed_pkgs.insert(&pkg.meta.id);
352 if changed.len() == 1 && changed[0].ends_with("Cargo.lock") {
353 } else {
355 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
356 }
357 } else if changed_pkgs.contains(&pkg.meta.id) {
358 log::debug!("Dependency changed for {crate_name} since {prior_tag_name}",);
359 changed_pkgs.insert(&pkg.meta.id);
360 changed_pkgs.extend(pkg.dependents.iter().map(|d| &d.pkg.id));
361 } else {
362 let _ = crate::ops::shell::warn(format!(
363 "updating {} to {} despite no changes made since tag {}",
364 crate_name, version.full_version_string, prior_tag_name
365 ));
366 }
367 } else {
368 log::debug!(
369 "cannot detect changes for {crate_name} because tag {prior_tag_name} is missing. Try setting `--prev-tag-name <TAG>`."
370 );
371 }
372 } else {
373 log::debug!(
374 "cannot detect changes for {crate_name} because no tag was found. Try setting `--prev-tag-name <TAG>`.",
375 );
376 }
377 }
378
379 Ok(())
380}
381
382pub fn find_shared_versions(
383 pkgs: &[plan::PackageRelease],
384) -> Result<Option<plan::Version>, crate::error::CliError> {
385 let mut is_shared = true;
386 let mut shared_versions: std::collections::HashMap<&str, &plan::Version> = Default::default();
387 for pkg in pkgs {
388 let group_name = if let Some(group_name) = pkg.config.shared_version() {
389 group_name
390 } else {
391 continue;
392 };
393 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
394 match shared_versions.entry(group_name) {
395 std::collections::hash_map::Entry::Occupied(existing) => {
396 if version.bare_version != existing.get().bare_version {
397 is_shared = false;
398 let _ = crate::ops::shell::error(format!(
399 "{} has version {}, should be {}",
400 pkg.meta.name,
401 version.bare_version_string,
402 existing.get().bare_version_string
403 ));
404 }
405 }
406 std::collections::hash_map::Entry::Vacant(vacant) => {
407 vacant.insert(version);
408 }
409 }
410 }
411 if !is_shared {
412 let _ = crate::ops::shell::error("crate versions deviated, aborting");
413 return Err(101.into());
414 }
415
416 if shared_versions.len() == 1 {
417 Ok(shared_versions.values().next().map(|s| (*s).clone()))
418 } else {
419 Ok(None)
420 }
421}
422
423pub fn consolidate_commits(
424 selected_pkgs: &[plan::PackageRelease],
425 excluded_pkgs: &[plan::PackageRelease],
426) -> Result<bool, crate::error::CliError> {
427 let mut consolidate_commits = None;
428 for pkg in selected_pkgs.iter().chain(excluded_pkgs.iter()) {
429 let current = Some(pkg.config.consolidate_commits());
430 if consolidate_commits.is_none() {
431 consolidate_commits = current;
432 } else if consolidate_commits != current {
433 let _ = crate::ops::shell::error("inconsistent `consolidate-commits` setting");
434 return Err(101.into());
435 }
436 }
437 Ok(consolidate_commits.expect("at least one package"))
438}
439
440pub fn confirm(
441 step: &str,
442 pkgs: &[plan::PackageRelease],
443 no_confirm: bool,
444 dry_run: bool,
445) -> Result<(), crate::error::CliError> {
446 if !dry_run && !no_confirm {
447 let prompt = if pkgs.len() == 1 {
448 let pkg = &pkgs[0];
449 let crate_name = pkg.meta.name.as_str();
450 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
451 format!("{} {} {}?", step, crate_name, version.full_version_string)
452 } else {
453 use std::io::Write;
454
455 let mut buffer: Vec<u8> = vec![];
456 writeln!(&mut buffer, "{step}").unwrap();
457 for pkg in pkgs {
458 let crate_name = pkg.meta.name.as_str();
459 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
460 writeln!(
461 &mut buffer,
462 " {} {}",
463 crate_name, version.full_version_string
464 )
465 .unwrap();
466 }
467 write!(&mut buffer, "?").unwrap();
468 String::from_utf8(buffer).expect("Only valid UTF-8 has been written")
469 };
470
471 let confirmed = crate::ops::shell::confirm(&prompt);
472 if !confirmed {
473 return Err(0.into());
474 }
475 }
476
477 Ok(())
478}
479
480pub fn finish(failed: bool, dry_run: bool) -> Result<(), crate::error::CliError> {
481 if dry_run {
482 if failed {
483 let _ =
484 crate::ops::shell::error("dry-run failed, resolve the above errors and try again.");
485 Err(101.into())
486 } else {
487 let _ =
488 crate::ops::shell::warn("aborting release due to dry run; re-run with `--execute`");
489 Ok(())
490 }
491 } else {
492 Ok(())
493 }
494}
495
496#[derive(Clone, Debug)]
497pub enum TargetVersion {
498 Relative(BumpLevel),
499 Absolute(semver::Version),
500}
501
502impl TargetVersion {
503 pub fn bump(
504 &self,
505 current: &semver::Version,
506 metadata: Option<&str>,
507 ) -> CargoResult<Option<plan::Version>> {
508 match self {
509 TargetVersion::Relative(bump_level) => {
510 let mut potential_version = current.to_owned();
511 bump_level.bump_version(&mut potential_version, metadata)?;
512 if potential_version != *current {
513 let full_version = potential_version;
514 let version = plan::Version::from(full_version);
515 Ok(Some(version))
516 } else {
517 Ok(None)
518 }
519 }
520 TargetVersion::Absolute(version) => {
521 let mut full_version = version.to_owned();
522 if full_version.build.is_empty() {
523 if let Some(metadata) = metadata {
524 full_version.build = semver::BuildMetadata::new(metadata)?;
525 } else {
526 full_version.build = current.build.clone();
527 }
528 }
529 let version = plan::Version::from(full_version);
530 if version.bare_version != plan::Version::from(current.clone()).bare_version {
531 Ok(Some(version))
532 } else {
533 Ok(None)
534 }
535 }
536 }
537 }
538}
539
540impl Default for TargetVersion {
541 fn default() -> Self {
542 TargetVersion::Relative(BumpLevel::Release)
543 }
544}
545
546impl std::fmt::Display for TargetVersion {
547 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
548 match self {
549 TargetVersion::Relative(bump_level) => {
550 write!(f, "{bump_level}")
551 }
552 TargetVersion::Absolute(version) => {
553 write!(f, "{version}")
554 }
555 }
556 }
557}
558
559impl FromStr for TargetVersion {
560 type Err = String;
561
562 fn from_str(s: &str) -> Result<Self, Self::Err> {
563 if let Ok(bump_level) = BumpLevel::from_str(s) {
564 Ok(TargetVersion::Relative(bump_level))
565 } else {
566 Ok(TargetVersion::Absolute(
567 semver::Version::parse(s).map_err(|e| e.to_string())?,
568 ))
569 }
570 }
571}
572
573impl clap::builder::ValueParserFactory for TargetVersion {
574 type Parser = TargetVersionParser;
575
576 fn value_parser() -> Self::Parser {
577 TargetVersionParser
578 }
579}
580
581#[derive(Copy, Clone)]
582pub struct TargetVersionParser;
583
584impl clap::builder::TypedValueParser for TargetVersionParser {
585 type Value = TargetVersion;
586
587 fn parse_ref(
588 &self,
589 cmd: &clap::Command,
590 arg: Option<&clap::Arg>,
591 value: &std::ffi::OsStr,
592 ) -> Result<Self::Value, clap::Error> {
593 let inner_parser = TargetVersion::from_str;
594 inner_parser.parse_ref(cmd, arg, value)
595 }
596
597 fn possible_values(
598 &self,
599 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
600 let inner_parser = clap::builder::EnumValueParser::<BumpLevel>::new();
601 inner_parser.possible_values().map(|ps| {
602 let ps = ps.collect::<Vec<_>>();
603 let ps: Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_> =
604 Box::new(ps.into_iter());
605 ps
606 })
607 }
608}
609
610#[derive(Debug, Clone, Copy, clap::ValueEnum)]
611#[value(rename_all = "kebab-case")]
612pub enum BumpLevel {
613 Major,
615 Minor,
617 Patch,
619 Release,
621 Rc,
623 Beta,
625 Alpha,
627}
628
629impl std::fmt::Display for BumpLevel {
630 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631 use clap::ValueEnum;
632
633 self.to_possible_value()
634 .expect("no values are skipped")
635 .get_name()
636 .fmt(f)
637 }
638}
639
640impl FromStr for BumpLevel {
641 type Err = String;
642
643 fn from_str(s: &str) -> Result<Self, Self::Err> {
644 use clap::ValueEnum;
645
646 for variant in Self::value_variants() {
647 if variant.to_possible_value().unwrap().matches(s, false) {
648 return Ok(*variant);
649 }
650 }
651 Err(format!("Invalid variant: {s}"))
652 }
653}
654
655impl BumpLevel {
656 pub fn bump_version(
657 self,
658 version: &mut semver::Version,
659 metadata: Option<&str>,
660 ) -> CargoResult<()> {
661 match self {
662 BumpLevel::Major => {
663 version.increment_major();
664 }
665 BumpLevel::Minor => {
666 version.increment_minor();
667 }
668 BumpLevel::Patch => {
669 if !version.is_prerelease() {
670 version.increment_patch();
671 } else {
672 version.pre = semver::Prerelease::EMPTY;
673 }
674 }
675 BumpLevel::Release => {
676 if version.is_prerelease() {
677 version.pre = semver::Prerelease::EMPTY;
678 }
679 }
680 BumpLevel::Rc => {
681 version.increment_rc()?;
682 }
683 BumpLevel::Beta => {
684 version.increment_beta()?;
685 }
686 BumpLevel::Alpha => {
687 version.increment_alpha()?;
688 }
689 };
690
691 if let Some(metadata) = metadata {
692 version.metadata(metadata)?;
693 }
694
695 Ok(())
696 }
697}