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