xtask/cmd/release/
utils.rs

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            // pre-release always bump that
89            _ 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            // 0.0.x always bump patch
95            _ if version.major == 0 && version.minor == 0 => semver::Version {
96                patch: version.patch + 1,
97                ..version
98            },
99            // 0.x.y => 0.(x + 1).0
100            Self::Major if version.major == 0 => semver::Version {
101                minor: version.minor + 1,
102                patch: 0,
103                ..version
104            },
105            // x.y.z => (x + 1).0.0
106            Self::Major => semver::Version {
107                major: version.major + 1,
108                minor: 0,
109                patch: 0,
110                ..version
111            },
112            // 0.x.y => 0.x.(y + 1)
113            Self::Minor if version.major == 0 => semver::Version {
114                patch: version.patch + 1,
115                ..version
116            },
117            // x.y.z => x.(y + 1).0
118            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 to the first `## [Unreleased]`
711                .skip(1) // skips the `## [Unreleased]` line
712                .skip_while(|s| !s.starts_with("## ")) // skip to the first `## [{{ version }}]`
713                .skip(1) // skips the `## [{{ version }}]` line
714                .take_while(|s| !s.starts_with("## ")) // takes all lines until the next `## [{{ version }}]`
715                .skip_while(|s| s.is_empty())
716                .map(|s| s.trim()) // removes all whitespace
717                .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            // It only makes sense to check git diffs if we are currently on the latest published version.
768            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            // check if a tag exists.
803            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            // tag doesnt exist
814            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        // git rev-list -1 HEAD~100..HEAD -- README.md
824        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}