xtask/cmd/semver_checks/
utils.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use anyhow::{Context, Result};
6use cargo_metadata::{Metadata, MetadataCommand};
7
8pub struct WorktreeCleanup {
9    path: PathBuf,
10}
11
12impl Drop for WorktreeCleanup {
13    fn drop(&mut self) {
14        // extra line to separate from semver output
15        println!("\n<details>");
16        // extra line to separate from below output for proper formatting
17        println!("<summary> 🛬 Cleanup details 🛬 </summary>\n");
18        println!("Cleaning up git worktree at {:?}\n", self.path);
19        let status = Command::new("git")
20            .args(["worktree", "remove", "--force", self.path.to_str().unwrap()])
21            .status();
22
23        match status {
24            Ok(status) if status.success() => {
25                println!("Successfully removed git worktree");
26            }
27            Ok(status) => {
28                eprintln!("Failed to remove git worktree. Exit code: {status}");
29            }
30            Err(e) => {
31                eprintln!("Error removing git worktree: {e:?}");
32            }
33        }
34
35        println!("</details>");
36    }
37}
38
39pub fn metadata_from_dir(dir: impl AsRef<Path>) -> Result<Metadata> {
40    MetadataCommand::new()
41        .manifest_path(dir.as_ref().join("Cargo.toml"))
42        .exec()
43        .context("fetching cargo metadata from directory")
44}
45
46pub fn checkout_baseline(baseline_rev_or_hash: &str, target_dir: &PathBuf) -> Result<WorktreeCleanup> {
47    if target_dir.exists() {
48        std::fs::remove_dir_all(target_dir)?;
49    }
50
51    // Attempt to resolve the revision locally first
52    let rev_parse_output = Command::new("git")
53        .args(["rev-parse", "--verify", baseline_rev_or_hash])
54        .output()
55        .context("git rev-parse failed")?;
56
57    let commit_hash = if rev_parse_output.status.success() {
58        String::from_utf8(rev_parse_output.stdout)?.trim().to_string()
59    } else {
60        println!("Revision {baseline_rev_or_hash} not found locally. Fetching from origin...\n");
61
62        Command::new("git")
63            .args(["fetch", "--depth", "1", "origin", baseline_rev_or_hash])
64            .status()
65            .context("git fetch failed")?
66            .success()
67            .then_some(())
68            .context("git fetch unsuccessful")?;
69
70        // Retry resolving after fetch
71        let retry_output = Command::new("git")
72            .args(["rev-parse", "--verify", "FETCH_HEAD"])
73            .output()
74            .context("git rev-parse after fetch failed")?;
75
76        retry_output
77            .status
78            .success()
79            .then(|| String::from_utf8(retry_output.stdout).unwrap().trim().to_string())
80            .context(format!("Failed to resolve revision {baseline_rev_or_hash}"))?
81    };
82
83    println!("Checking out commit {commit_hash} into {target_dir:?}\n");
84
85    Command::new("git")
86        .args(["worktree", "add", "--detach", target_dir.to_str().unwrap(), &commit_hash])
87        .status()
88        .context("git worktree add failed")?
89        .success()
90        .then_some(())
91        .context("git worktree add unsuccessful")?;
92
93    Ok(WorktreeCleanup {
94        path: target_dir.clone(),
95    })
96}
97
98pub fn workspace_crates_in_folder(meta: &Metadata, folder: &str) -> HashSet<String> {
99    let folder_path = std::fs::canonicalize(folder).expect("folder should exist");
100
101    meta.packages
102        .iter()
103        .filter(|p| {
104            // All crate examples have publish = false.
105            // The scuffle-bootstrap-derive crate doesn't work with the semver-checks tool at the moment.
106            let manifest_path = p.manifest_path.parent().unwrap();
107            manifest_path.starts_with(&folder_path)
108                && p.publish.as_ref().map(|v| !v.is_empty()).unwrap_or(true)
109                && p.name != "scuffle-bootstrap-derive"
110                && p.name != "scuffle-metrics-derive"
111        })
112        .map(|p| p.name.clone())
113        .collect()
114}