A package's latest version is its highest, not the last one published
An import publishes every tag at once, so psr/log read as 1.0.0 instead of 3.0.2. Listings now pick the highest stable version by number, then the highest pre-release, and fall back to the newest published.
2 files+50−20/2 viewed
| 62 | 62 | pub version_count: u32, | |
| 63 | 63 | pub bytes: u64, | |
| 64 | 64 | pub latest_tag: Option<String>, | |
| 65 | + | /// Every version, newest published first, one per line: the summary | |
| 66 | + | /// picks the highest of them (see `newest_version`). | |
| 65 | 67 | pub latest_version: Option<String>, | |
| 66 | 68 | } | |
| 67 | 69 | ||
| 70 | + | /// The version a listing calls latest: the highest stable one by number | |
| 71 | + | /// (`v3.0.2` over `1.0.0`, whatever order they were published in, as an | |
| 72 | + | /// import publishes every tag at once), else the highest pre-release, else | |
| 73 | + | /// the newest published when none reads as a number. | |
| 74 | + | pub fn newest_version(versions: &str) -> Option<String> { | |
| 75 | + | // Stable over pre-release, then by number, then pre-releases by label. | |
| 76 | + | let parse = |version: &str| -> Option<(bool, Vec<u64>, String)> { | |
| 77 | + | let bare = version.strip_prefix('v').unwrap_or(version); | |
| 78 | + | let (core, pre) = match bare.split_once(['-', '+']) { | |
| 79 | + | Some((core, rest)) if bare.as_bytes()[core.len()] == b'-' => (core, rest.to_owned()), | |
| 80 | + | Some((core, _)) => (core, String::new()), | |
| 81 | + | None => (bare, String::new()), | |
| 82 | + | }; | |
| 83 | + | let parts = core.split('.').map(|part| part.parse::<u64>().ok()).collect::<Option<Vec<_>>>()?; | |
| 84 | + | Some((pre.is_empty(), parts, pre)) | |
| 85 | + | }; | |
| 86 | + | let list: Vec<&str> = versions.lines().map(str::trim).filter(|v| !v.is_empty()).collect(); | |
| 87 | + | list.iter() | |
| 88 | + | .filter_map(|v| parse(v).map(|key| (key, *v))) | |
| 89 | + | .max_by(|a, b| a.0.cmp(&b.0)) | |
| 90 | + | .map(|(_, v)| v.to_owned()) | |
| 91 | + | .or_else(|| list.first().map(|v| (*v).to_owned())) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | #[cfg(test)] | |
| 95 | + | mod newest_tests { | |
| 96 | + | use super::newest_version; | |
| 97 | + | ||
| 98 | + | #[test] | |
| 99 | + | fn the_latest_is_the_highest_stable_version_not_the_last_published() { | |
| 100 | + | assert_eq!(newest_version("1.0.0 | |
| 101 | + | 3.0.2 | |
| 102 | + | 2.0.0 | |
| 103 | + | 3.0.0").as_deref(), Some("3.0.2")); | |
| 104 | + | assert_eq!(newest_version("v1.10.0 | |
| 105 | + | v1.9.3").as_deref(), Some("v1.10.0")); | |
| 106 | + | assert_eq!(newest_version("4.0.0-beta.1 | |
| 107 | + | 3.0.2").as_deref(), Some("3.0.2")); | |
| 108 | + | assert_eq!(newest_version("4.0.0-beta.1 | |
| 109 | + | 4.0.0-alpha").as_deref(), Some("4.0.0-beta.1")); | |
| 110 | + | assert_eq!(newest_version("dev-main | |
| 111 | + | nightly").as_deref(), Some("dev-main")); | |
| 112 | + | assert_eq!(newest_version(""), None); | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 68 | 116 | #[derive(Clone, Debug, Deserialize)] | |
| 69 | 117 | pub struct BlobRow { | |
| 70 | 118 | pub digest: String, | |
| 234 | 282 | (SELECT COALESCE(SUM(b.size), 0) FROM blobs b WHERE b.digest IN \ | |
| 235 | 283 | (SELECT vf.digest FROM version_files vf JOIN versions v ON v.id = vf.version_id WHERE v.package_id = p.id)) AS bytes, \ | |
| 236 | 284 | (SELECT t.tag FROM tags t WHERE t.package_id = p.id ORDER BY t.tag = 'latest' DESC, t.updated_at DESC LIMIT 1) AS latest_tag, \ | |
| 237 | − | (SELECT v.version FROM versions v WHERE v.package_id = p.id ORDER BY v.published_at DESC LIMIT 1) AS latest_version \ | |
| 285 | + | (SELECT GROUP_CONCAT(version, char(10)) FROM (SELECT v.version FROM versions v WHERE v.package_id = p.id ORDER BY v.published_at DESC)) AS latest_version \ | |
| 238 | 286 | FROM packages p WHERE p.workspace = ? AND p.workspace_deleted_at IS NULL", | |
| 239 | 287 | PACKAGE_COLUMNS.split(", ").map(|c| format!("p.{c}")).collect::<Vec<_>>().join(", ") | |
| 240 | 288 | ); |
| 358 | 358 | }), | |
| 359 | 359 | description: p.description.clone(), | |
| 360 | 360 | versions: row.version_count, | |
| 361 | − | latest: row.latest_tag.clone().or_else(|| row.latest_version.clone()), | |
| 361 | + | latest: row.latest_tag.clone().or_else(|| row.latest_version.as_deref().and_then(db::newest_version)), | |
| 362 | 362 | size: row.bytes, | |
| 363 | 363 | downloads: p.downloads, | |
| 364 | 364 | created_at: p.created_at.clone(), |