1use std::collections::{BTreeMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::process::Stdio;
4use std::sync::{Arc, Mutex};
5
6use anyhow::Context;
7use cargo_metadata::camino::Utf8PathBuf;
8use cargo_metadata::{Dependency, DependencyKind, semver};
9use cargo_platform::Platform;
10use sha2::Digest;
11
12use crate::utils::Command;
13
14#[derive(Clone)]
15pub struct Package {
16 pkg: cargo_metadata::Package,
17 version_slated: Option<semver::Version>,
18 published_versions: Arc<Mutex<Vec<CratesIoVersion>>>,
19 data: Arc<Mutex<PackageData>>,
20 metadata: XTaskPackageMetadata,
21}
22
23impl std::ops::Deref for Package {
24 type Target = cargo_metadata::Package;
25
26 fn deref(&self) -> &Self::Target {
27 &self.pkg
28 }
29}
30
31#[derive(serde_derive::Deserialize, Default, Debug, Clone)]
32#[serde(default, rename_all = "kebab-case")]
33struct GitReleaseMeta {
34 name: Option<String>,
35 tag_name: Option<String>,
36 enabled: Option<bool>,
37 body: Option<String>,
38 artifacts: Vec<GitReleaseArtifact>,
39}
40
41#[derive(serde_derive::Deserialize, Debug, Clone)]
42#[serde(rename_all = "kebab-case", tag = "kind")]
43pub enum GitReleaseArtifact {
44 File {
45 path: String,
46 name: Option<String>,
47 },
48}
49
50#[derive(serde_derive::Deserialize, Default, Debug, Clone)]
51#[serde(default, rename_all = "kebab-case")]
52struct XTaskPackageMetadata {
53 group: Option<String>,
54 git_release: GitReleaseMeta,
55 semver_checks: Option<bool>,
56 min_versions_checks: Option<bool>,
57 private_dependencies: HashSet<String>,
58}
59
60impl XTaskPackageMetadata {
61 fn from_package(package: &cargo_metadata::Package) -> anyhow::Result<Self> {
62 let Some(metadata) = package.metadata.get("xtask").and_then(|v| v.get("release")) else {
63 return Ok(Self::default());
64 };
65
66 serde_json::from_value(metadata.clone()).context("xtask")
67 }
68}
69
70#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
71pub enum VersionBump {
72 Minor = 1,
73 Major = 2,
74}
75
76impl VersionBump {
77 fn bump(&mut self, new: Self) -> &mut Self {
78 *self = new.max(*self);
79 self
80 }
81
82 fn bump_major(&mut self) -> &mut Self {
83 self.bump(Self::Major)
84 }
85
86 fn bump_minor(&mut self) -> &mut Self {
87 self.bump(Self::Minor)
88 }
89
90 pub fn next_semver(&self, version: semver::Version) -> semver::Version {
91 match self {
92 _ if !version.pre.is_empty() => semver::Version {
94 pre: semver::Prerelease::new(&increment_last_identifier(&version.pre))
95 .expect("pre release increment failed, this is a bug"),
96 ..version
97 },
98 _ if version.major == 0 && version.minor == 0 => semver::Version {
100 patch: version.patch + 1,
101 ..version
102 },
103 Self::Major if version.major == 0 => semver::Version {
105 minor: version.minor + 1,
106 patch: 0,
107 ..version
108 },
109 Self::Major => semver::Version {
111 major: version.major + 1,
112 minor: 0,
113 patch: 0,
114 ..version
115 },
116 Self::Minor if version.major == 0 => semver::Version {
118 patch: version.patch + 1,
119 ..version
120 },
121 Self::Minor => semver::Version {
123 minor: version.minor + 1,
124 patch: 0,
125 ..version
126 },
127 }
128 }
129}
130
131fn increment_last_identifier(release: &str) -> String {
132 match release.rsplit_once('.') {
133 Some((left, right)) => {
134 if let Ok(right_num) = right.parse::<u32>() {
135 format!("{left}.{}", right_num + 1)
136 } else {
137 format!("{release}.1")
138 }
139 }
140 None => format!("{release}.1"),
141 }
142}
143
144#[derive(Clone, Copy)]
145pub enum PackageErrorMissing {
146 Description,
147 License,
148 Readme,
149 Repopository,
150 Author,
151 Documentation,
152 ChangelogEntry,
153}
154
155impl std::fmt::Display for PackageErrorMissing {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Description => f.write_str("description in Cargo.toml"),
159 Self::License => f.write_str("license in Cargo.toml"),
160 Self::Readme => f.write_str("readme file path in Cargo.toml"),
161 Self::Repopository => f.write_str("repository link in Cargo.toml"),
162 Self::Author => f.write_str("authors in Cargo.toml"),
163 Self::Documentation => f.write_str("documentation link in Cargo.toml"),
164 Self::ChangelogEntry => f.write_str("changelog entry"),
165 }
166 }
167}
168
169impl From<PackageErrorMissing> for PackageError {
170 fn from(value: PackageErrorMissing) -> Self {
171 PackageError::Missing(value)
172 }
173}
174
175#[derive(Clone, Copy)]
176pub enum LicenseKind {
177 Mit,
178 Apache2,
179 AGpl3,
180}
181
182impl LicenseKind {
183 pub const AGPL_3: &str = "AGPL-3.0";
184 const APACHE2: &str = "Apache-2.0";
185 const MIT: &str = "MIT";
186 pub const MIT_OR_APACHE2: &str = "MIT OR Apache-2.0";
187
188 pub fn from_text(text: &str) -> Option<Vec<LicenseKind>> {
189 match text {
190 Self::MIT_OR_APACHE2 => Some(vec![LicenseKind::Mit, LicenseKind::Apache2]),
191 Self::AGPL_3 => Some(vec![LicenseKind::AGpl3]),
192 _ => None,
193 }
194 }
195}
196
197impl std::fmt::Display for LicenseKind {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::Mit => f.write_str(Self::MIT),
201 Self::Apache2 => f.write_str(Self::APACHE2),
202 Self::AGpl3 => f.write_str(Self::AGPL_3),
203 }
204 }
205}
206
207#[derive(Clone, Copy)]
208pub enum PackageFile {
209 License(LicenseKind),
210 Readme,
211 Changelog,
212}
213
214impl std::fmt::Display for PackageFile {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self {
217 Self::Changelog => f.write_str("CHANGELOG.md"),
218 Self::Readme => f.write_str("README.md"),
219 Self::License(name) => write!(f, "LICENSE.{name}"),
220 }
221 }
222}
223
224impl From<PackageFile> for PackageError {
225 fn from(value: PackageFile) -> Self {
226 PackageError::MissingFile(value)
227 }
228}
229
230#[derive(Clone)]
231pub enum PackageError {
232 Missing(PackageErrorMissing),
233 MissingFile(PackageFile),
234 DependencyMissingVersion {
235 name: String,
236 target: Option<Platform>,
237 kind: DependencyKind,
238 },
239 DevDependencyHasVersion {
240 name: String,
241 target: Option<Platform>,
242 },
243 DependencyGroupedVersion {
244 name: String,
245 target: Option<Platform>,
246 kind: DependencyKind,
247 },
248 DependencyNotPublishable {
249 name: String,
250 target: Option<Platform>,
251 kind: DependencyKind,
252 },
253 GitRelease {
254 error: String,
255 },
256 GitReleaseArtifactFileMissing {
257 path: String,
258 },
259 VersionChanged {
260 from: semver::Version,
261 to: semver::Version,
262 },
263}
264
265impl PackageError {
266 pub fn missing_version(dep: &Dependency) -> Self {
267 Self::DependencyMissingVersion {
268 kind: dep.kind,
269 name: dep.name.clone(),
270 target: dep.target.clone(),
271 }
272 }
273
274 pub fn has_version(dep: &Dependency) -> Self {
275 Self::DevDependencyHasVersion {
276 name: dep.name.clone(),
277 target: dep.target.clone(),
278 }
279 }
280
281 pub fn not_publish(dep: &Dependency) -> Self {
282 Self::DependencyNotPublishable {
283 kind: dep.kind,
284 name: dep.name.clone(),
285 target: dep.target.clone(),
286 }
287 }
288
289 pub fn grouped_version(dep: &Dependency) -> Self {
290 Self::DependencyGroupedVersion {
291 kind: dep.kind,
292 name: dep.name.clone(),
293 target: dep.target.clone(),
294 }
295 }
296
297 pub fn version_changed(from: semver::Version, to: semver::Version) -> Self {
298 Self::VersionChanged { from, to }
299 }
300}
301
302pub fn dep_kind_to_name(kind: &DependencyKind) -> &str {
303 match kind {
304 DependencyKind::Build => "build-dependencies",
305 DependencyKind::Development => "dev-dependencies",
306 DependencyKind::Normal => "dependencies",
307 kind => panic!("unknown dep kind: {kind:?}"),
308 }
309}
310
311impl std::fmt::Display for PackageError {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 match self {
314 Self::Missing(item) => write!(f, "{item} must be provided"),
315 Self::DependencyMissingVersion {
316 name,
317 target: Some(Platform::Cfg(cfg)),
318 kind,
319 } => {
320 write!(
321 f,
322 "`{name}` must have a version in `[target.'{cfg}'.{kind}]`",
323 kind = dep_kind_to_name(kind)
324 )
325 }
326 Self::DependencyMissingVersion {
327 name,
328 target: Some(Platform::Name(platform)),
329 kind,
330 } => {
331 write!(
332 f,
333 "`{name}` must have a version in `[target.{platform}.{kind}]`",
334 kind = dep_kind_to_name(kind)
335 )
336 }
337 Self::DependencyMissingVersion {
338 name,
339 target: None,
340 kind,
341 } => {
342 write!(f, "`{name}` must have a version in `[{kind}]`", kind = dep_kind_to_name(kind))
343 }
344 Self::DevDependencyHasVersion {
345 name,
346 target: Some(Platform::Cfg(cfg)),
347 } => {
348 write!(f, "`{name}` must not have a version in `[target.'{cfg}'.dev-dependencies]`",)
349 }
350 Self::DevDependencyHasVersion {
351 name,
352 target: Some(Platform::Name(platform)),
353 } => {
354 write!(
355 f,
356 "`{name}` must not have a version in `[target.{platform}.dev-dependencies]`",
357 )
358 }
359 Self::DevDependencyHasVersion { name, target: None } => {
360 write!(f, "`{name}` must not have a version in `[dev-dependencies]`")
361 }
362 Self::DependencyNotPublishable {
363 name,
364 target: Some(Platform::Cfg(cfg)),
365 kind,
366 } => {
367 write!(
368 f,
369 "`{name}` is not publishable in `[target.'{cfg}'.{kind}]`",
370 kind = dep_kind_to_name(kind)
371 )
372 }
373 Self::DependencyNotPublishable {
374 name,
375 target: Some(Platform::Name(platform)),
376 kind,
377 } => {
378 write!(
379 f,
380 "{name} is not publishable in [target.{platform}.{kind}]",
381 kind = dep_kind_to_name(kind)
382 )
383 }
384 Self::DependencyNotPublishable {
385 name,
386 target: None,
387 kind,
388 } => {
389 write!(f, "`{name}` is not publishable in `[{kind}]`", kind = dep_kind_to_name(kind))
390 }
391 Self::DependencyGroupedVersion {
392 name,
393 target: Some(Platform::Name(platform)),
394 kind,
395 } => {
396 write!(
397 f,
398 "`{name}` must be pinned to the same version as the current crate in `[target.{platform}.{kind}]`",
399 kind = dep_kind_to_name(kind)
400 )
401 }
402 Self::DependencyGroupedVersion {
403 name,
404 target: Some(Platform::Cfg(cfg)),
405 kind,
406 } => {
407 write!(
408 f,
409 "`{name}` must be pinned to the same version as the current crate in `[target.'{cfg}'.{kind}]`",
410 kind = dep_kind_to_name(kind)
411 )
412 }
413 Self::DependencyGroupedVersion {
414 name,
415 target: None,
416 kind,
417 } => {
418 write!(
419 f,
420 "`{name}` must be pinned to the same version as the current crate in `[{kind}]`",
421 kind = dep_kind_to_name(kind)
422 )
423 }
424 Self::MissingFile(file) => {
425 write!(f, "missing file {file} in crate")
426 }
427 Self::GitRelease { error } => {
428 write!(f, "error generating git release: {error}")
429 }
430 Self::GitReleaseArtifactFileMissing { path } => {
431 write!(f, "missing file artifact used by git release: {path}")
432 }
433 Self::VersionChanged { from, to } => write!(f, "package version has changed `{from}` -> `{to}`"),
434 }
435 }
436}
437
438#[derive(Default)]
439pub struct PackageData {
440 version_bump: Option<VersionBump>,
441 semver_output: Option<(bool, String)>,
442 min_versions_output: Option<String>,
443 next_version: Option<semver::Version>,
444 issues: Vec<PackageError>,
445}
446
447#[derive(serde_derive::Deserialize, Clone)]
448pub struct CratesIoVersion {
449 pub name: String,
450 pub vers: semver::Version,
451 pub cksum: String,
452}
453
454#[tracing::instrument(skip_all, fields(package = %crate_name))]
455pub fn crates_io_versions(crate_name: &str) -> anyhow::Result<Vec<CratesIoVersion>> {
456 let url = crate_index_url(crate_name);
457
458 tracing::info!(url = %url, "checking on crates.io");
459 let command = Command::new("curl")
460 .arg("-s")
461 .arg("-L")
462 .arg("-w")
463 .arg("\n%{http_code}\n")
464 .arg(url)
465 .stdout(Stdio::piped())
466 .stderr(Stdio::piped())
467 .output()
468 .context("curl")?;
469
470 let stdout = String::from_utf8_lossy(&command.stdout);
471 let stderr = String::from_utf8_lossy(&command.stderr);
472 let lines = stdout.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect::<Vec<_>>();
473 let status = lines.last().copied().unwrap_or_default();
474 match status {
475 "200" => {}
476 "404" => return Ok(Vec::new()),
477 status => {
478 anyhow::bail!("curl failed ({status}): {stderr} {stdout}")
479 }
480 }
481
482 let mut versions = Vec::new();
483 for line in lines.iter().take(lines.len() - 1).copied() {
484 versions.push(serde_json::from_str::<CratesIoVersion>(line).context("json")?)
485 }
486
487 versions.sort_by(|a, b| a.vers.cmp(&b.vers));
488
489 Ok(versions)
490}
491
492fn crate_index_url(crate_name: &str) -> String {
493 let name = crate_name.to_lowercase();
494 let len = name.len();
495
496 match len {
497 0 => panic!("Invalid crate name"),
498 1 => format!("https://index.crates.io/1/{name}"),
499 2 => format!("https://index.crates.io/2/{name}"),
500 3 => format!("https://index.crates.io/3/{}/{}", &name[0..1], name),
501 _ => {
502 let prefix = &name[0..2];
503 let suffix = &name[2..4];
504 format!("https://index.crates.io/{prefix}/{suffix}/{name}")
505 }
506 }
507}
508
509#[tracing::instrument(skip_all, fields(name = %version.name, version = %version.vers))]
510pub fn download_crate(version: &CratesIoVersion) -> anyhow::Result<PathBuf> {
511 let crate_file = format!("{}-{}.crate", version.name, version.vers);
512 let home = home::cargo_home().context("home dir")?;
513 let registry_cache = home.join("registry").join("cache");
514 let mut desired_path = home.join("scuffle-xtask-release").join(&crate_file);
515 let is_match = |path: &Path| {
516 tracing::debug!("checking {}", path.display());
517 if let Ok(read) = std::fs::read(path) {
518 let hash = sha2::Sha256::digest(&read);
519 let hash = hex::encode(hash);
520 hash == version.cksum
521 } else {
522 false
523 }
524 };
525
526 if is_match(&desired_path) {
527 tracing::debug!("found {}", desired_path.display());
528 return Ok(desired_path);
529 }
530
531 if registry_cache.exists() {
532 let dirs = std::fs::read_dir(registry_cache).context("read_dir")?;
533 for dir in dirs {
534 let dir = dir?;
535 let file_name = dir.file_name();
536 let Some(file_name) = file_name.to_str() else {
537 continue;
538 };
539
540 if file_name.starts_with("index.crates.io-") {
541 desired_path = dir.path().join(&crate_file);
542 if is_match(&desired_path) {
543 tracing::debug!("found at {}", desired_path.display());
544 return Ok(desired_path);
545 }
546 }
547 }
548 }
549
550 let url = format!("https://static.crates.io/crates/{}/{crate_file}", version.name);
551
552 tracing::info!(url = %url, "fetching from crates.io");
553
554 let output = Command::new("curl")
555 .arg("-s")
556 .arg("-L")
557 .arg(url)
558 .arg("-o")
559 .arg(&desired_path)
560 .output()
561 .context("download")?;
562
563 if !output.status.success() {
564 anyhow::bail!("curl failed")
565 }
566
567 Ok(desired_path)
568}
569
570#[derive(Debug, Clone)]
571pub struct GitRelease {
572 pub name: String,
573 pub tag_name: String,
574 pub body: String,
575 pub artifacts: Vec<GitReleaseArtifact>,
576}
577
578impl Package {
579 const DEFAULT_GIT_RELEASE_BODY: &str = include_str!("./git_release_body_tmpl.md");
580 const DEFAULT_GIT_TAG_NAME: &str = "{{ package }}-v{{ version }}";
581
582 pub fn new(workspace_meta: &WorkspaceReleaseMetadata, pkg: cargo_metadata::Package) -> anyhow::Result<Self> {
583 Ok(Self {
584 data: Default::default(),
585 version_slated: workspace_meta.packages.get(pkg.name.as_str()).cloned(),
586 metadata: XTaskPackageMetadata::from_package(&pkg)?,
587 published_versions: Default::default(),
588 pkg,
589 })
590 }
591
592 pub fn should_publish(&self) -> bool {
593 self.pkg.publish.is_none()
594 }
595
596 pub fn group(&self) -> &str {
597 self.metadata.group.as_deref().unwrap_or(&self.pkg.name)
598 }
599
600 pub fn is_dep_public(&self, name: &str) -> bool {
601 !self.metadata.private_dependencies.contains(name)
602 }
603
604 pub fn unreleased_req(&self) -> semver::VersionReq {
605 semver::VersionReq {
606 comparators: vec![semver::Comparator {
607 op: semver::Op::GreaterEq,
608 major: self.version.major,
609 minor: Some(self.version.minor),
610 patch: Some(self.version.patch),
611 pre: self.version.pre.clone(),
612 }],
613 }
614 }
615
616 pub fn changelog_path(&self) -> Option<Utf8PathBuf> {
617 if self.group() == self.pkg.name.as_ref() && self.should_release() {
618 Some(self.pkg.manifest_path.with_file_name("CHANGELOG.md"))
619 } else {
620 None
621 }
622 }
623
624 pub fn should_git_release(&self) -> bool {
625 self.metadata.git_release.enabled.unwrap_or_else(|| self.should_publish()) && self.group() == self.pkg.name.as_ref()
626 }
627
628 pub fn git_release(&self) -> anyhow::Result<Option<GitRelease>> {
629 if !self.should_git_release() {
630 return Ok(None);
631 }
632
633 Ok(Some(GitRelease {
634 body: self.git_release_body().context("body")?,
635 name: self.git_release_name().context("name")?,
636 tag_name: self.git_tag_name().context("tag")?,
637 artifacts: self.metadata.git_release.artifacts.clone(),
638 }))
639 }
640
641 pub fn should_semver_checks(&self) -> bool {
642 self.metadata.semver_checks.unwrap_or(true) && self.should_publish() && self.pkg.targets.iter().any(|t| t.is_lib())
643 }
644
645 pub fn should_min_version_check(&self) -> bool {
646 self.metadata.min_versions_checks.unwrap_or(true)
647 && self.should_publish()
648 && self.pkg.targets.iter().any(|t| t.is_lib())
649 }
650
651 pub fn should_release(&self) -> bool {
652 self.should_git_release() || self.should_publish()
653 }
654
655 pub fn last_published_version(&self) -> Option<CratesIoVersion> {
656 let published_versions = self.published_versions.lock().unwrap();
657 let version = published_versions.binary_search_by(|r| r.vers.cmp(&self.pkg.version));
658 match version {
659 Ok(idx) => Some(published_versions[idx].clone()),
660 Err(idx) => idx.checked_sub(1).and_then(|idx| published_versions.get(idx).cloned()),
661 }
662 }
663
664 pub fn slated_for_release(&self) -> bool {
665 self.should_release()
666 && self.version_slated.as_ref().is_some_and(|v| v == &self.version)
667 && self.last_published_version().is_none_or(|p| p.vers != self.version)
668 }
669
670 fn git_tag_name(&self) -> anyhow::Result<String> {
671 self.git_tag_name_version(&self.pkg.version)
672 }
673
674 fn git_tag_name_version(&self, version: &semver::Version) -> anyhow::Result<String> {
675 let tag_name = self
676 .metadata
677 .git_release
678 .tag_name
679 .as_deref()
680 .unwrap_or(Self::DEFAULT_GIT_TAG_NAME);
681
682 let env = minijinja::Environment::new();
683 let ctx = minijinja::context! {
684 package => &self.pkg.name,
685 version => version,
686 };
687
688 env.render_str(tag_name, ctx).context("render")
689 }
690
691 fn git_release_name(&self) -> anyhow::Result<String> {
692 let tag_name = self
693 .metadata
694 .git_release
695 .name
696 .as_deref()
697 .or(self.metadata.git_release.tag_name.as_deref())
698 .unwrap_or(Self::DEFAULT_GIT_TAG_NAME);
699
700 let env = minijinja::Environment::new();
701 let ctx = minijinja::context! {
702 package => &self.pkg.name,
703 version => &self.pkg.version,
704 };
705
706 env.render_str(tag_name, ctx).context("render")
707 }
708
709 fn git_release_body(&self) -> anyhow::Result<String> {
710 let tag_name = self
711 .metadata
712 .git_release
713 .body
714 .as_deref()
715 .unwrap_or(Self::DEFAULT_GIT_RELEASE_BODY);
716
717 let changelog = if let Some(path) = self.changelog_path() {
718 let changelogs = std::fs::read_to_string(path).context("read changelog")?;
719 changelogs
720 .lines()
721 .skip_while(|s| !s.starts_with("## ")) .skip(1) .skip_while(|s| !s.starts_with("## ")) .skip(1) .take_while(|s| !s.starts_with("## ")) .skip_while(|s| s.is_empty())
727 .map(|s| s.trim()) .collect::<Vec<_>>()
729 .join("\n")
730 } else {
731 String::new()
732 };
733
734 let env = minijinja::Environment::new();
735 let ctx = minijinja::context! {
736 package => &self.pkg.name,
737 version => &self.pkg.version,
738 publish => self.should_publish(),
739 changelog => changelog,
740 };
741
742 env.render_str(tag_name, ctx).context("render")
743 }
744
745 pub fn has_branch_changes(&self, base: &str) -> bool {
746 let output = match Command::new("git")
747 .arg("diff")
748 .arg(format!("{base}..HEAD"))
749 .arg("--quiet")
750 .arg("--")
751 .arg(self.pkg.manifest_path.parent().unwrap())
752 .stderr(Stdio::piped())
753 .stdout(Stdio::piped())
754 .output()
755 {
756 Ok(output) => output,
757 Err(err) => {
758 tracing::warn!("git diff failed: {err}");
759 return true;
760 }
761 };
762
763 if !output.status.success() && !output.stderr.is_empty() {
764 tracing::warn!("git diff failed: {}", String::from_utf8_lossy(&output.stderr));
765 }
766
767 !output.status.success()
768 }
769
770 pub fn has_changed_since_publish(&self) -> anyhow::Result<bool> {
771 let last_commit = if self.should_publish() {
772 let Some(last_published) = self.last_published_version() else {
773 return Ok(false);
774 };
775
776 if last_published.vers != self.pkg.version {
778 return Ok(false);
779 }
780
781 let crate_path = download_crate(&last_published)?;
782 let tar_output = Command::new("tar")
783 .arg("-xOzf")
784 .arg(crate_path)
785 .arg(format!(
786 "{}-{}/.cargo_vcs_info.json",
787 last_published.name, last_published.vers
788 ))
789 .stderr(Stdio::piped())
790 .stdout(Stdio::piped())
791 .output()
792 .context("tar get cargo vcs info")?;
793
794 if !tar_output.status.success() {
795 anyhow::bail!("tar extact of crate failed: {}", String::from_utf8_lossy(&tar_output.stderr))
796 }
797
798 #[derive(serde::Deserialize)]
799 struct VscInfo {
800 git: VscInfoGit,
801 }
802
803 #[derive(serde::Deserialize)]
804 struct VscInfoGit {
805 sha1: String,
806 }
807
808 let vsc_info: VscInfo = serde_json::from_slice(&tar_output.stdout).context("invalid vcs info")?;
809 vsc_info.git.sha1
810 } else if self.should_release() {
811 let tag_name = self.git_tag_name().context("tag name")?;
813
814 let output = Command::new("git")
815 .arg("rev-parse")
816 .arg(format!("refs/tags/{tag_name}"))
817 .stderr(Stdio::piped())
818 .stdout(Stdio::piped())
819 .output()
820 .context("git rev-parse for tag")?;
821
822 if !output.status.success() {
824 return Ok(false);
825 }
826
827 String::from_utf8_lossy(&output.stdout).trim().to_owned()
828 } else {
829 return Ok(false);
830 };
831
832 let output = Command::new("git")
834 .arg("diff")
835 .arg(format!("{last_commit}..HEAD"))
836 .arg("--quiet")
837 .arg("--")
838 .arg(self.pkg.manifest_path.parent().unwrap())
839 .stderr(Stdio::piped())
840 .stdout(Stdio::piped())
841 .output()
842 .context("git diff")?;
843
844 if !output.status.success() && !output.stderr.is_empty() {
845 anyhow::bail!("failed to get git diff {}", String::from_utf8_lossy(&output.stderr))
846 }
847
848 Ok(!output.status.success())
849 }
850
851 pub fn next_version(&self) -> Option<semver::Version> {
852 self.data.lock().unwrap().next_version.clone()
853 }
854
855 pub fn set_next_version(&self, version: semver::Version) {
856 if self.version != version {
857 self.data.lock().unwrap().next_version = Some(version);
858 }
859 }
860
861 pub fn report_change(&self) {
862 self.data
863 .lock()
864 .unwrap()
865 .version_bump
866 .get_or_insert(VersionBump::Minor)
867 .bump_minor();
868 }
869
870 pub fn report_breaking_change(&self) {
871 self.data
872 .lock()
873 .unwrap()
874 .version_bump
875 .get_or_insert(VersionBump::Major)
876 .bump_major();
877 }
878
879 pub fn version_bump(&self) -> Option<VersionBump> {
880 self.data.lock().unwrap().version_bump
881 }
882
883 pub fn published_versions(&self) -> Vec<CratesIoVersion> {
884 self.published_versions.lock().unwrap().clone()
885 }
886
887 pub fn fetch_published(&self) -> anyhow::Result<()> {
888 if self.should_publish() {
889 *self.published_versions.lock().unwrap() = crates_io_versions(&self.pkg.name)?;
890 }
891 Ok(())
892 }
893
894 pub fn report_issue(&self, issue: impl Into<PackageError>) {
895 let issue = issue.into();
896 tracing::warn!("{}", issue.to_string().replace("`", ""));
897 self.data.lock().unwrap().issues.push(issue);
898 }
899
900 pub fn set_semver_output(&self, breaking: bool, output: String) {
901 if breaking {
902 self.report_breaking_change();
903 }
904 tracing::debug!(breaking = breaking, "cargo-semver-checks-output: {output}");
905 self.data.lock().unwrap().semver_output = Some((breaking, output));
906 }
907
908 pub fn set_min_versions_output(&self, output: String) {
909 self.data.lock().unwrap().min_versions_output = Some(output);
910 }
911
912 pub fn semver_output(&self) -> Option<(bool, String)> {
913 self.data.lock().unwrap().semver_output.clone()
914 }
915
916 pub fn min_versions_output(&self) -> Option<String> {
917 self.data.lock().unwrap().min_versions_output.clone()
918 }
919
920 pub fn errors(&self) -> Vec<PackageError> {
921 self.data.lock().unwrap().issues.clone()
922 }
923}
924
925#[derive(serde_derive::Deserialize, serde_derive::Serialize, Default)]
926pub struct WorkspaceReleaseMetadata {
927 pub packages: BTreeMap<String, semver::Version>,
928}
929
930impl WorkspaceReleaseMetadata {
931 pub fn from_metadadata(metadata: &cargo_metadata::Metadata) -> anyhow::Result<Self> {
932 let Some(value) = metadata.workspace_metadata.get("xtask").and_then(|x| x.get("release")) else {
933 return Ok(Default::default());
934 };
935
936 serde_json::from_value(value.clone()).context("deserialize")
937 }
938}
939
940pub fn vers_to_comp(vers: semver::Version) -> semver::Comparator {
941 semver::Comparator {
942 op: semver::Op::Caret,
943 major: vers.major,
944 minor: Some(vers.minor),
945 patch: Some(vers.patch),
946 pre: vers.pre,
947 }
948}