1use std::path::{Path, PathBuf};
4
5use crate_seq_ledger::{detect_tag_pattern, load, LedgerEntry, LedgerStatus, VersionSource};
6use crate_seq_registry::CratesIoClient;
7
8use crate::Error;
9
10#[derive(Debug, Clone)]
15pub struct OrphanedEntry {
16 pub version: semver::Version,
18 pub recorded_ref: String,
20}
21
22#[derive(Debug)]
24pub struct CheckReport {
25 pub crate_name: String,
27 pub registry_latest: Option<semver::Version>,
29 pub pending: Vec<LedgerEntry>,
31 pub skipped: Vec<LedgerEntry>,
33 pub yanked: Vec<LedgerEntry>,
35 pub orphaned: Vec<OrphanedEntry>,
37}
38
39fn resolve_tag_pattern(settings: &crate_seq_ledger::LedgerSettings, crate_name: &str) -> String {
41 settings
42 .tag_pattern
43 .clone()
44 .unwrap_or_else(|| detect_tag_pattern(crate_name, false))
45}
46
47fn registry_latest(metadata: Option<&crate_seq_registry::CrateMetadata>) -> Option<semver::Version> {
49 metadata?
50 .versions
51 .iter()
52 .filter(|v| !v.yanked)
53 .map(|v| &v.version)
54 .max()
55 .cloned()
56}
57
58fn entries_with_status(entries: &[LedgerEntry], status: LedgerStatus) -> Vec<LedgerEntry> {
60 let mut out: Vec<LedgerEntry> = entries
61 .iter()
62 .filter(|e| e.status == status)
63 .cloned()
64 .collect();
65 out.sort_by(|a, b| a.version.cmp(&b.version));
66 out
67}
68
69fn snapshot_hash_exists(sha256: &str, snapshot_store: &Path) -> bool {
71 let Ok(entries) = std::fs::read_dir(snapshot_store) else {
72 return false;
73 };
74 for entry in entries.flatten() {
75 let path = entry.path();
76 if path.extension().and_then(|e| e.to_str()) != Some("gz") {
77 continue;
78 }
79 if matches!(crate_seq_snapshot::hash_tarball(&path), Ok(h) if h == sha256) {
80 return true;
81 }
82 }
83 false
84}
85
86fn find_orphaned(
91 pending: &[LedgerEntry],
92 git_tag_names: &std::collections::HashSet<String>,
93 snapshot_store: &Path,
94) -> Vec<OrphanedEntry> {
95 pending
96 .iter()
97 .filter(|e| match e.source {
98 VersionSource::GitTag => !git_tag_names.contains(&e.ref_),
99 VersionSource::Snapshot => !snapshot_hash_exists(&e.ref_, snapshot_store),
100 })
101 .map(|e| OrphanedEntry {
102 version: e.version.clone(),
103 recorded_ref: e.ref_.clone(),
104 })
105 .collect()
106}
107
108fn default_snapshot_store(ledger_path: &Path) -> PathBuf {
110 ledger_path
111 .parent()
112 .unwrap_or_else(|| Path::new("."))
113 .join(".crate-seq-snapshots")
114}
115
116pub fn check_crate(
127 ledger_path: &Path,
128 repo_path: &Path,
129 crate_seq_version: &str,
130 snapshot_store: Option<PathBuf>,
131) -> Result<CheckReport, Error> {
132 let ledger = load(ledger_path)?;
133 let crate_name = ledger.crate_config.name.clone();
134 let tag_pattern = resolve_tag_pattern(&ledger.settings, &crate_name);
135
136 let client = CratesIoClient::new(crate_seq_version)?;
137 let metadata = client.fetch_crate_metadata(&crate_name)?;
138
139 let tags = crate_seq_git::discover_tags(repo_path, &tag_pattern)?;
140 let tag_names: std::collections::HashSet<String> =
141 tags.into_iter().map(|t| t.name).collect();
142
143 let store = snapshot_store.unwrap_or_else(|| default_snapshot_store(ledger_path));
144
145 let pending = entries_with_status(&ledger.entries, LedgerStatus::Pending);
146 let skipped = entries_with_status(&ledger.entries, LedgerStatus::Skipped);
147 let yanked = entries_with_status(&ledger.entries, LedgerStatus::Yanked);
148 let orphaned = find_orphaned(&pending, &tag_names, &store);
149
150 Ok(CheckReport {
151 crate_name,
152 registry_latest: registry_latest(metadata.as_ref()),
153 pending,
154 skipped,
155 yanked,
156 orphaned,
157 })
158}