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