1#[cfg(test)]
4#[path = "check_tests.rs"]
5mod tests;
6
7use std::path::{Path, PathBuf};
8
9use crate_seq_ledger::{detect_tag_pattern, load, LedgerEntry, LedgerStatus, VersionSource};
10use crate_seq_registry::CratesIoClient;
11
12use crate::Error;
13
14#[derive(Debug, Clone)]
19pub struct OrphanedEntry {
20 pub version: semver::Version,
22 pub recorded_ref: String,
24}
25
26#[derive(Debug)]
28pub struct CheckReport {
29 pub crate_name: String,
31 pub registry_latest: Option<semver::Version>,
33 pub pending: Vec<LedgerEntry>,
35 pub skipped: Vec<LedgerEntry>,
37 pub yanked: Vec<LedgerEntry>,
39 pub orphaned: Vec<OrphanedEntry>,
41}
42
43fn resolve_tag_pattern(settings: &crate_seq_ledger::LedgerSettings, crate_name: &str) -> String {
45 settings
46 .tag_pattern
47 .clone()
48 .unwrap_or_else(|| detect_tag_pattern(crate_name, false))
49}
50
51fn registry_latest(
53 metadata: Option<&crate_seq_registry::CrateMetadata>,
54) -> Option<semver::Version> {
55 metadata?
56 .versions
57 .iter()
58 .filter(|v| !v.yanked)
59 .map(|v| &v.version)
60 .max()
61 .cloned()
62}
63
64fn entries_with_status(entries: &[LedgerEntry], status: LedgerStatus) -> Vec<LedgerEntry> {
66 let mut out: Vec<LedgerEntry> = entries
67 .iter()
68 .filter(|e| e.status == status)
69 .cloned()
70 .collect();
71 out.sort_by(|a, b| a.version.cmp(&b.version));
72 out
73}
74
75fn snapshot_hash_exists(sha256: &str, snapshot_store: &Path) -> bool {
77 let Ok(entries) = std::fs::read_dir(snapshot_store) else {
78 return false;
79 };
80 for entry in entries.flatten() {
81 let path = entry.path();
82 if path.extension().and_then(|e| e.to_str()) != Some("gz") {
83 continue;
84 }
85 if matches!(crate_seq_snapshot::hash_tarball(&path), Ok(h) if h == sha256) {
86 return true;
87 }
88 }
89 false
90}
91
92fn find_orphaned(
97 pending: &[LedgerEntry],
98 git_tag_names: &std::collections::HashSet<String>,
99 snapshot_store: &Path,
100) -> Vec<OrphanedEntry> {
101 pending
102 .iter()
103 .filter(|e| match e.source {
104 VersionSource::GitTag => !git_tag_names.contains(&e.ref_),
105 VersionSource::Snapshot => !snapshot_hash_exists(&e.ref_, snapshot_store),
106 })
107 .map(|e| OrphanedEntry {
108 version: e.version.clone(),
109 recorded_ref: e.ref_.clone(),
110 })
111 .collect()
112}
113
114fn default_snapshot_store(ledger_path: &Path) -> PathBuf {
116 ledger_path
117 .parent()
118 .unwrap_or_else(|| Path::new("."))
119 .join(".crate-seq-snapshots")
120}
121
122pub fn check_crate(
133 ledger_path: &Path,
134 repo_path: &Path,
135 crate_seq_version: &str,
136 snapshot_store: Option<PathBuf>,
137) -> Result<CheckReport, Error> {
138 let client = CratesIoClient::new(crate_seq_version)?;
139 check_crate_with_client(ledger_path, repo_path, &client, snapshot_store)
140}
141
142pub(crate) fn check_crate_with_client(
147 ledger_path: &Path,
148 repo_path: &Path,
149 client: &CratesIoClient,
150 snapshot_store: Option<PathBuf>,
151) -> Result<CheckReport, Error> {
152 let ledger = load(ledger_path)?;
153 let crate_name = ledger.crate_config.name.clone();
154 let tag_pattern = resolve_tag_pattern(&ledger.settings, &crate_name);
155
156 let metadata = client.fetch_crate_metadata(&crate_name)?;
157
158 let tags = crate_seq_git::discover_tags(repo_path, &tag_pattern)?;
159 let tag_names: std::collections::HashSet<String> = tags.into_iter().map(|t| t.name).collect();
160
161 let store = snapshot_store.unwrap_or_else(|| default_snapshot_store(ledger_path));
162
163 let pending = entries_with_status(&ledger.entries, LedgerStatus::Pending);
164 let skipped = entries_with_status(&ledger.entries, LedgerStatus::Skipped);
165 let yanked = entries_with_status(&ledger.entries, LedgerStatus::Yanked);
166 let orphaned = find_orphaned(&pending, &tag_names, &store);
167
168 Ok(CheckReport {
169 crate_name,
170 registry_latest: registry_latest(metadata.as_ref()),
171 pending,
172 skipped,
173 yanked,
174 orphaned,
175 })
176}