batuta/stack/publish_status/
scanner.rs1use anyhow::Result;
6use std::path::{Path, PathBuf};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use super::cache::{compute_cache_key, PublishStatusCache};
10use super::git::{determine_action, get_git_status, get_local_version};
11use super::types::{CacheEntry, CrateStatus, GitStatus, PublishAction, PublishStatusReport};
12use crate::stack::PAIML_CRATES;
13
14pub struct PublishStatusScanner {
20 pub(crate) workspace_root: PathBuf,
22 pub(crate) cache: PublishStatusCache,
24 #[cfg(feature = "native")]
26 pub(crate) crates_io: Option<crate::stack::crates_io::CratesIoClient>,
27}
28
29impl PublishStatusScanner {
30 #[must_use]
32 pub fn new(workspace_root: PathBuf) -> Self {
33 Self {
34 workspace_root,
35 cache: PublishStatusCache::load(),
36 #[cfg(feature = "native")]
37 crates_io: None,
38 }
39 }
40
41 #[cfg(feature = "native")]
43 pub fn with_crates_io(mut self) -> Self {
44 self.crates_io =
45 Some(crate::stack::crates_io::CratesIoClient::new().with_persistent_cache());
46 self
47 }
48
49 #[must_use]
51 pub fn find_crate_dirs(&self) -> Vec<(String, PathBuf)> {
52 PAIML_CRATES
53 .iter()
54 .filter_map(|name| {
55 let path = self.workspace_root.join(name);
56 if path.join("Cargo.toml").exists() {
57 Some(((*name).to_string(), path))
58 } else {
59 None
60 }
61 })
62 .collect()
63 }
64
65 pub fn check_crate(&mut self, name: &str, path: &Path) -> CrateStatus {
67 let cache_key = match compute_cache_key(path) {
69 Ok(key) => key,
70 Err(e) => {
71 return CrateStatus {
72 name: name.to_string(),
73 local_version: None,
74 crates_io_version: None,
75 git_status: GitStatus::default(),
76 action: PublishAction::Error,
77 path: path.to_path_buf(),
78 error: Some(e.to_string()),
79 };
80 }
81 };
82
83 if let Some(entry) = self.cache.get(name, &cache_key) {
85 if !entry.is_crates_io_stale() {
86 return entry.status.clone();
88 }
89 }
90
91 self.refresh_crate(name, path, &cache_key)
93 }
94
95 pub(crate) fn refresh_crate(
97 &mut self,
98 name: &str,
99 path: &Path,
100 cache_key: &str,
101 ) -> CrateStatus {
102 let local_version = get_local_version(path).ok();
103 let git_status = get_git_status(path).unwrap_or_default();
104
105 let crates_io_version = None; let action =
109 determine_action(local_version.as_deref(), crates_io_version.as_deref(), &git_status);
110
111 let status = CrateStatus {
112 name: name.to_string(),
113 local_version,
114 crates_io_version,
115 git_status,
116 action,
117 path: path.to_path_buf(),
118 error: None,
119 };
120
121 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
123
124 self.cache.insert(
125 name.to_string(),
126 CacheEntry {
127 cache_key: cache_key.to_string(),
128 status: status.clone(),
129 crates_io_checked_at: now,
130 created_at: now,
131 },
132 );
133
134 status
135 }
136
137 #[cfg(feature = "native")]
139 pub async fn scan(&mut self) -> Result<PublishStatusReport> {
140 use std::time::Instant;
141
142 let start = Instant::now();
143 let crate_dirs = self.find_crate_dirs();
144 let mut statuses = Vec::with_capacity(crate_dirs.len());
145 let mut cache_hits = 0;
146
147 for (name, path) in &crate_dirs {
149 let cache_key = compute_cache_key(path).unwrap_or_default();
150
151 if let Some(entry) = self.cache.get(name, &cache_key) {
152 if !entry.is_crates_io_stale() {
153 cache_hits += 1;
154 statuses.push(entry.status.clone());
155 continue;
156 }
157 }
158
159 let mut status = self.refresh_crate(name, path, &cache_key);
161
162 if let Some(ref mut client) = self.crates_io {
164 if let Ok(response) = client.get_crate(name).await {
165 status.crates_io_version = Some(response.krate.max_version.clone());
166 status.action = determine_action(
167 status.local_version.as_deref(),
168 status.crates_io_version.as_deref(),
169 &status.git_status,
170 );
171
172 let now =
174 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
175
176 self.cache.insert(
177 name.clone(),
178 CacheEntry {
179 cache_key: cache_key.clone(),
180 status: status.clone(),
181 crates_io_checked_at: now,
182 created_at: now,
183 },
184 );
185 }
186 }
187
188 statuses.push(status);
189 }
190
191 let _ = self.cache.save();
193
194 let elapsed_ms = start.elapsed().as_millis() as u64;
195 Ok(PublishStatusReport::from_statuses(statuses, cache_hits, elapsed_ms))
196 }
197
198 #[cfg(feature = "native")]
200 pub fn scan_sync(&mut self) -> Result<PublishStatusReport> {
201 let rt = tokio::runtime::Runtime::new()?;
202 rt.block_on(self.scan())
203 }
204}