upd-cli 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {upd_cli-0.0.2 → upd_cli-0.0.3}/Cargo.lock +1 -1
- {upd_cli-0.0.2 → upd_cli-0.0.3}/Cargo.toml +1 -1
- {upd_cli-0.0.2 → upd_cli-0.0.3}/PKG-INFO +1 -1
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/cache.rs +78 -21
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/main.rs +13 -16
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/registry/crates_io.rs +5 -2
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/registry/go_proxy.rs +6 -3
- upd_cli-0.0.3/src/registry/mod.rs +85 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/registry/npm.rs +38 -23
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/registry/pypi.rs +5 -2
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/go_mod.rs +60 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/requirements.rs +35 -2
- upd_cli-0.0.3/src/version/semver_util.rs +98 -0
- upd_cli-0.0.2/src/registry/mod.rs +0 -40
- upd_cli-0.0.2/src/version/semver_util.rs +0 -53
- {upd_cli-0.0.2 → upd_cli-0.0.3}/.mise.toml +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/CHANGELOG.md +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/LICENSE +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/Makefile +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/README.md +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/pyproject.toml +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/rust-toolchain.toml +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/cli.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/lib.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/mod.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/package_json.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/version/mod.rs +0 -0
- {upd_cli-0.0.2 → upd_cli-0.0.3}/src/version/pep440.rs +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
use crate::registry::Registry;
|
|
1
2
|
use anyhow::Result;
|
|
3
|
+
use async_trait::async_trait;
|
|
2
4
|
use directories::ProjectDirs;
|
|
3
5
|
use serde::{Deserialize, Serialize};
|
|
4
6
|
use std::collections::HashMap;
|
|
5
7
|
use std::fs;
|
|
6
8
|
use std::path::PathBuf;
|
|
9
|
+
use std::sync::{Arc, Mutex};
|
|
7
10
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
8
11
|
|
|
9
12
|
const CACHE_TTL_HOURS: u64 = 24;
|
|
@@ -52,6 +55,19 @@ impl Cache {
|
|
|
52
55
|
Ok(())
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/// Create a shared cache wrapped in `Arc<Mutex>` for thread-safe access
|
|
59
|
+
pub fn new_shared() -> Arc<Mutex<Self>> {
|
|
60
|
+
Arc::new(Mutex::new(Self::load().unwrap_or_default()))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Save a shared cache to disk
|
|
64
|
+
pub fn save_shared(cache: &Arc<Mutex<Cache>>) -> Result<()> {
|
|
65
|
+
cache
|
|
66
|
+
.lock()
|
|
67
|
+
.map_err(|e| anyhow::anyhow!("Cache lock poisoned: {}", e))?
|
|
68
|
+
.save()
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
pub fn get(&self, registry: &str, package: &str) -> Option<String> {
|
|
56
72
|
let entries = match registry {
|
|
57
73
|
"pypi" => &self.pypi,
|
|
@@ -136,45 +152,86 @@ impl Cache {
|
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
///
|
|
155
|
+
/// Thread-safe cached registry wrapper that implements the Registry trait.
|
|
156
|
+
/// Checks cache before making network requests, storing results for future lookups.
|
|
140
157
|
pub struct CachedRegistry<R> {
|
|
141
158
|
inner: R,
|
|
142
|
-
cache: Cache
|
|
143
|
-
registry_name: &'static str,
|
|
159
|
+
cache: Arc<Mutex<Cache>>,
|
|
144
160
|
enabled: bool,
|
|
145
161
|
}
|
|
146
162
|
|
|
147
|
-
impl<R:
|
|
148
|
-
pub fn new(inner: R, enabled: bool) -> Self {
|
|
149
|
-
let cache = Cache::load().unwrap_or_default();
|
|
150
|
-
let registry_name = inner.name();
|
|
151
|
-
|
|
163
|
+
impl<R: Registry> CachedRegistry<R> {
|
|
164
|
+
pub fn new(inner: R, cache: Arc<Mutex<Cache>>, enabled: bool) -> Self {
|
|
152
165
|
Self {
|
|
153
166
|
inner,
|
|
154
167
|
cache,
|
|
155
|
-
registry_name,
|
|
156
168
|
enabled,
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if self.enabled
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
return Ok(version);
|
|
172
|
+
/// Get from cache (returns None if disabled, expired, or missing)
|
|
173
|
+
fn cache_get(&self, package: &str) -> Option<String> {
|
|
174
|
+
if !self.enabled {
|
|
175
|
+
return None;
|
|
166
176
|
}
|
|
177
|
+
self.cache.lock().ok()?.get(self.inner.name(), package)
|
|
178
|
+
}
|
|
167
179
|
|
|
168
|
-
|
|
180
|
+
/// Set in cache (no-op if disabled). Does NOT save to disk - caller saves once at end.
|
|
181
|
+
fn cache_set(&self, package: &str, version: &str) {
|
|
182
|
+
if !self.enabled {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if let Ok(mut cache) = self.cache.lock() {
|
|
186
|
+
cache.set(self.inner.name(), package, version.to_string());
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[async_trait]
|
|
192
|
+
impl<R: Registry> Registry for CachedRegistry<R> {
|
|
193
|
+
async fn get_latest_version(&self, package: &str) -> Result<String> {
|
|
194
|
+
if let Some(v) = self.cache_get(package) {
|
|
195
|
+
return Ok(v);
|
|
196
|
+
}
|
|
169
197
|
let version = self.inner.get_latest_version(package).await?;
|
|
198
|
+
self.cache_set(package, &version);
|
|
199
|
+
Ok(version)
|
|
200
|
+
}
|
|
170
201
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
async fn get_latest_version_including_prereleases(&self, package: &str) -> Result<String> {
|
|
203
|
+
// Pre-releases use separate cache key to avoid returning stable when pre-release needed
|
|
204
|
+
let cache_key = format!("{}:prerelease", package);
|
|
205
|
+
if let Some(v) = self.cache_get(&cache_key) {
|
|
206
|
+
return Ok(v);
|
|
176
207
|
}
|
|
208
|
+
let version = self
|
|
209
|
+
.inner
|
|
210
|
+
.get_latest_version_including_prereleases(package)
|
|
211
|
+
.await?;
|
|
212
|
+
self.cache_set(&cache_key, &version);
|
|
213
|
+
Ok(version)
|
|
214
|
+
}
|
|
177
215
|
|
|
216
|
+
async fn get_latest_version_matching(
|
|
217
|
+
&self,
|
|
218
|
+
package: &str,
|
|
219
|
+
constraints: &str,
|
|
220
|
+
) -> Result<String> {
|
|
221
|
+
// Constraint-matching uses composite key to cache per-constraint results
|
|
222
|
+
let cache_key = format!("{}:match:{}", package, constraints);
|
|
223
|
+
if let Some(v) = self.cache_get(&cache_key) {
|
|
224
|
+
return Ok(v);
|
|
225
|
+
}
|
|
226
|
+
let version = self
|
|
227
|
+
.inner
|
|
228
|
+
.get_latest_version_matching(package, constraints)
|
|
229
|
+
.await?;
|
|
230
|
+
self.cache_set(&cache_key, &version);
|
|
178
231
|
Ok(version)
|
|
179
232
|
}
|
|
233
|
+
|
|
234
|
+
fn name(&self) -> &'static str {
|
|
235
|
+
self.inner.name()
|
|
236
|
+
}
|
|
180
237
|
}
|
|
@@ -2,7 +2,8 @@ use anyhow::Result;
|
|
|
2
2
|
use clap::Parser;
|
|
3
3
|
use colored::Colorize;
|
|
4
4
|
|
|
5
|
-
use
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use upd::cache::{Cache, CachedRegistry};
|
|
6
7
|
use upd::cli::{Cli, Command};
|
|
7
8
|
use upd::registry::{CratesIoRegistry, GoProxyRegistry, NpmRegistry, PyPiRegistry};
|
|
8
9
|
use upd::updater::{
|
|
@@ -90,11 +91,14 @@ async fn run_update(cli: &Cli) -> Result<()> {
|
|
|
90
91
|
);
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
// Create registries
|
|
94
|
-
let
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
let
|
|
94
|
+
// Create shared cache and wrap registries with caching layer
|
|
95
|
+
let cache = Cache::new_shared();
|
|
96
|
+
let cache_enabled = !cli.no_cache;
|
|
97
|
+
|
|
98
|
+
let pypi = CachedRegistry::new(PyPiRegistry::new(), Arc::clone(&cache), cache_enabled);
|
|
99
|
+
let npm = CachedRegistry::new(NpmRegistry::new(), Arc::clone(&cache), cache_enabled);
|
|
100
|
+
let crates_io = CachedRegistry::new(CratesIoRegistry::new(), Arc::clone(&cache), cache_enabled);
|
|
101
|
+
let go_proxy = CachedRegistry::new(GoProxyRegistry::new(), Arc::clone(&cache), cache_enabled);
|
|
98
102
|
|
|
99
103
|
// Create updaters
|
|
100
104
|
let requirements_updater = RequirementsUpdater::new();
|
|
@@ -103,13 +107,6 @@ async fn run_update(cli: &Cli) -> Result<()> {
|
|
|
103
107
|
let cargo_toml_updater = CargoTomlUpdater::new();
|
|
104
108
|
let go_mod_updater = GoModUpdater::new();
|
|
105
109
|
|
|
106
|
-
// Load cache if enabled
|
|
107
|
-
let mut cache = if !cli.no_cache {
|
|
108
|
-
Cache::load().ok()
|
|
109
|
-
} else {
|
|
110
|
-
None
|
|
111
|
-
};
|
|
112
|
-
|
|
113
110
|
let mut total_result = UpdateResult::default();
|
|
114
111
|
|
|
115
112
|
for (path, file_type) in files {
|
|
@@ -143,9 +140,9 @@ async fn run_update(cli: &Cli) -> Result<()> {
|
|
|
143
140
|
}
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
// Save cache
|
|
147
|
-
if
|
|
148
|
-
let _ = cache
|
|
143
|
+
// Save cache to disk
|
|
144
|
+
if cache_enabled {
|
|
145
|
+
let _ = Cache::save_shared(&cache);
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
// Print summary
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
use super::Registry;
|
|
1
|
+
use super::{Registry, get_with_retry};
|
|
2
2
|
use anyhow::{Result, anyhow};
|
|
3
3
|
use async_trait::async_trait;
|
|
4
4
|
use reqwest::Client;
|
|
5
5
|
use serde::Deserialize;
|
|
6
|
+
use std::time::Duration;
|
|
6
7
|
|
|
7
8
|
pub struct CratesIoRegistry {
|
|
8
9
|
client: Client,
|
|
@@ -37,6 +38,8 @@ impl CratesIoRegistry {
|
|
|
37
38
|
.gzip(true)
|
|
38
39
|
// crates.io requires a descriptive User-Agent
|
|
39
40
|
.user_agent("upd/0.1.0 (https://github.com/rvben/upd)")
|
|
41
|
+
.timeout(Duration::from_secs(30))
|
|
42
|
+
.connect_timeout(Duration::from_secs(10))
|
|
40
43
|
.build()
|
|
41
44
|
.expect("Failed to create HTTP client");
|
|
42
45
|
|
|
@@ -55,7 +58,7 @@ impl CratesIoRegistry {
|
|
|
55
58
|
|
|
56
59
|
async fn fetch_crate(&self, name: &str) -> Result<CratesResponse> {
|
|
57
60
|
let url = format!("{}/{}", self.registry_url, name);
|
|
58
|
-
let response = self.client
|
|
61
|
+
let response = get_with_retry(&self.client, &url).await?;
|
|
59
62
|
|
|
60
63
|
if !response.status().is_success() {
|
|
61
64
|
return Err(anyhow!(
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
use super::Registry;
|
|
1
|
+
use super::{Registry, get_with_retry};
|
|
2
2
|
use anyhow::{Result, anyhow};
|
|
3
3
|
use async_trait::async_trait;
|
|
4
4
|
use reqwest::Client;
|
|
5
5
|
use serde::Deserialize;
|
|
6
|
+
use std::time::Duration;
|
|
6
7
|
|
|
7
8
|
pub struct GoProxyRegistry {
|
|
8
9
|
client: Client,
|
|
@@ -24,6 +25,8 @@ impl GoProxyRegistry {
|
|
|
24
25
|
let client = Client::builder()
|
|
25
26
|
.gzip(true)
|
|
26
27
|
.user_agent("upd/0.1.0")
|
|
28
|
+
.timeout(Duration::from_secs(30))
|
|
29
|
+
.connect_timeout(Duration::from_secs(10))
|
|
27
30
|
.build()
|
|
28
31
|
.expect("Failed to create HTTP client");
|
|
29
32
|
|
|
@@ -49,7 +52,7 @@ impl GoProxyRegistry {
|
|
|
49
52
|
let escaped = Self::escape_module_path(module);
|
|
50
53
|
let url = format!("{}/{}/@v/list", self.proxy_url, escaped);
|
|
51
54
|
|
|
52
|
-
let response = self.client
|
|
55
|
+
let response = get_with_retry(&self.client, &url).await?;
|
|
53
56
|
|
|
54
57
|
if !response.status().is_success() {
|
|
55
58
|
return Err(anyhow!(
|
|
@@ -110,7 +113,7 @@ impl Registry for GoProxyRegistry {
|
|
|
110
113
|
let escaped = Self::escape_module_path(package);
|
|
111
114
|
let url = format!("{}/{}/@latest", self.proxy_url, escaped);
|
|
112
115
|
|
|
113
|
-
if let Ok(response) = self.client
|
|
116
|
+
if let Ok(response) = get_with_retry(&self.client, &url).await
|
|
114
117
|
&& response.status().is_success()
|
|
115
118
|
&& let Ok(data) = response.json::<LatestResponse>().await
|
|
116
119
|
{
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
mod crates_io;
|
|
2
|
+
mod go_proxy;
|
|
3
|
+
mod npm;
|
|
4
|
+
mod pypi;
|
|
5
|
+
|
|
6
|
+
pub use crates_io::CratesIoRegistry;
|
|
7
|
+
pub use go_proxy::GoProxyRegistry;
|
|
8
|
+
pub use npm::NpmRegistry;
|
|
9
|
+
pub use pypi::PyPiRegistry;
|
|
10
|
+
|
|
11
|
+
use anyhow::Result;
|
|
12
|
+
use async_trait::async_trait;
|
|
13
|
+
use reqwest::{Client, Response};
|
|
14
|
+
use std::time::Duration;
|
|
15
|
+
|
|
16
|
+
/// Maximum number of retry attempts for failed HTTP requests
|
|
17
|
+
const MAX_RETRIES: u32 = 3;
|
|
18
|
+
|
|
19
|
+
/// Base delay for exponential backoff (100ms, 200ms, 400ms)
|
|
20
|
+
const BASE_DELAY_MS: u64 = 100;
|
|
21
|
+
|
|
22
|
+
/// Execute an HTTP GET request with retry and exponential backoff.
|
|
23
|
+
/// Retries on transient errors (network issues, 5xx server errors).
|
|
24
|
+
pub async fn get_with_retry(client: &Client, url: &str) -> Result<Response, reqwest::Error> {
|
|
25
|
+
let mut last_error = None;
|
|
26
|
+
|
|
27
|
+
for attempt in 0..MAX_RETRIES {
|
|
28
|
+
match client.get(url).send().await {
|
|
29
|
+
Ok(response) => {
|
|
30
|
+
// Don't retry client errors (4xx) - they won't succeed on retry
|
|
31
|
+
if response.status().is_client_error() || response.status().is_success() {
|
|
32
|
+
return Ok(response);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Retry server errors (5xx)
|
|
36
|
+
if response.status().is_server_error() && attempt < MAX_RETRIES - 1 {
|
|
37
|
+
let delay = Duration::from_millis(BASE_DELAY_MS * (1 << attempt));
|
|
38
|
+
tokio::time::sleep(delay).await;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Ok(response);
|
|
43
|
+
}
|
|
44
|
+
Err(e) => {
|
|
45
|
+
last_error = Some(e);
|
|
46
|
+
|
|
47
|
+
// Don't retry on the last attempt
|
|
48
|
+
if attempt < MAX_RETRIES - 1 {
|
|
49
|
+
let delay = Duration::from_millis(BASE_DELAY_MS * (1 << attempt));
|
|
50
|
+
tokio::time::sleep(delay).await;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Err(last_error.unwrap())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[async_trait]
|
|
60
|
+
pub trait Registry: Send + Sync {
|
|
61
|
+
/// Get the latest stable version of a package
|
|
62
|
+
async fn get_latest_version(&self, package: &str) -> Result<String>;
|
|
63
|
+
|
|
64
|
+
/// Get the latest version including pre-releases
|
|
65
|
+
/// Used when the user's current version is a pre-release
|
|
66
|
+
async fn get_latest_version_including_prereleases(&self, package: &str) -> Result<String> {
|
|
67
|
+
// Default: fall back to stable-only
|
|
68
|
+
self.get_latest_version(package).await
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Get the latest version matching the given constraints (e.g., ">=2.8.0,<9")
|
|
72
|
+
/// Default implementation falls back to get_latest_version
|
|
73
|
+
async fn get_latest_version_matching(
|
|
74
|
+
&self,
|
|
75
|
+
package: &str,
|
|
76
|
+
constraints: &str,
|
|
77
|
+
) -> Result<String> {
|
|
78
|
+
// Default: ignore constraints and return latest
|
|
79
|
+
let _ = constraints;
|
|
80
|
+
self.get_latest_version(package).await
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Registry name for display
|
|
84
|
+
fn name(&self) -> &'static str;
|
|
85
|
+
}
|
|
@@ -3,18 +3,22 @@ use anyhow::{Result, anyhow};
|
|
|
3
3
|
use async_trait::async_trait;
|
|
4
4
|
use reqwest::Client;
|
|
5
5
|
use serde::Deserialize;
|
|
6
|
-
use
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
use std::time::Duration;
|
|
7
8
|
|
|
8
9
|
pub struct NpmRegistry {
|
|
9
10
|
client: Client,
|
|
10
11
|
registry_url: String,
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/// Abbreviated npm response (smaller, faster)
|
|
15
|
+
/// Uses Accept: application/vnd.npm.install-v1+json
|
|
13
16
|
#[derive(Debug, Deserialize)]
|
|
14
|
-
struct
|
|
17
|
+
struct NpmAbbreviatedResponse {
|
|
15
18
|
#[serde(rename = "dist-tags")]
|
|
16
19
|
dist_tags: DistTags,
|
|
17
|
-
|
|
20
|
+
/// Version keys only (we parse them dynamically to avoid large struct)
|
|
21
|
+
versions: Value,
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
#[derive(Debug, Deserialize)]
|
|
@@ -22,11 +26,6 @@ struct DistTags {
|
|
|
22
26
|
latest: Option<String>,
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
#[derive(Debug, Deserialize)]
|
|
26
|
-
struct VersionInfo {
|
|
27
|
-
deprecated: Option<String>,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
29
|
impl NpmRegistry {
|
|
31
30
|
pub fn new() -> Self {
|
|
32
31
|
Self::with_registry_url("https://registry.npmjs.org".to_string())
|
|
@@ -36,6 +35,8 @@ impl NpmRegistry {
|
|
|
36
35
|
let client = Client::builder()
|
|
37
36
|
.gzip(true)
|
|
38
37
|
.user_agent("upd/0.1.0")
|
|
38
|
+
.timeout(Duration::from_secs(30))
|
|
39
|
+
.connect_timeout(Duration::from_secs(10))
|
|
39
40
|
.build()
|
|
40
41
|
.expect("Failed to create HTTP client");
|
|
41
42
|
|
|
@@ -50,10 +51,18 @@ impl NpmRegistry {
|
|
|
50
51
|
std::env::var("NPM_REGISTRY").ok().filter(|s| !s.is_empty())
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/// Fetch package metadata from npm
|
|
54
|
-
|
|
54
|
+
/// Fetch abbreviated package metadata from npm
|
|
55
|
+
/// Uses the install-v1 format which is smaller and faster
|
|
56
|
+
async fn fetch_package(&self, package: &str) -> Result<NpmAbbreviatedResponse> {
|
|
55
57
|
let url = format!("{}/{}", self.registry_url, package);
|
|
56
|
-
|
|
58
|
+
|
|
59
|
+
// Use abbreviated metadata format (much smaller for large packages like react)
|
|
60
|
+
let response = self
|
|
61
|
+
.client
|
|
62
|
+
.get(&url)
|
|
63
|
+
.header("Accept", "application/vnd.npm.install-v1+json")
|
|
64
|
+
.send()
|
|
65
|
+
.await?;
|
|
57
66
|
|
|
58
67
|
if !response.status().is_success() {
|
|
59
68
|
return Err(anyhow!(
|
|
@@ -63,16 +72,24 @@ impl NpmRegistry {
|
|
|
63
72
|
));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
response
|
|
76
|
+
.json()
|
|
77
|
+
.await
|
|
78
|
+
.map_err(|e| anyhow!("Failed to parse npm response for '{}': {}", package, e))
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
/// Get all stable (non-prerelease
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
/// Get all stable (non-prerelease) versions sorted descending
|
|
82
|
+
/// Note: abbreviated metadata doesn't include deprecated info, but dist-tags.latest
|
|
83
|
+
/// is authoritative for the recommended version
|
|
84
|
+
fn get_stable_versions(data: &NpmAbbreviatedResponse) -> Vec<(semver::Version, String)> {
|
|
85
|
+
let versions_obj = match data.versions.as_object() {
|
|
86
|
+
Some(obj) => obj,
|
|
87
|
+
None => return Vec::new(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let mut versions: Vec<_> = versions_obj
|
|
91
|
+
.keys()
|
|
92
|
+
.filter_map(|ver_str| {
|
|
76
93
|
semver::Version::parse(ver_str).ok().and_then(|v| {
|
|
77
94
|
// Skip pre-release versions
|
|
78
95
|
if v.pre.is_empty() {
|
|
@@ -100,17 +117,15 @@ impl Registry for NpmRegistry {
|
|
|
100
117
|
async fn get_latest_version(&self, package: &str) -> Result<String> {
|
|
101
118
|
let data = self.fetch_package(package).await?;
|
|
102
119
|
|
|
103
|
-
// Use the 'latest' dist-tag
|
|
120
|
+
// Use the 'latest' dist-tag (this is the authoritative answer from npm)
|
|
104
121
|
if let Some(latest) = &data.dist_tags.latest
|
|
105
|
-
&& let Some(version_info) = data.versions.get(latest)
|
|
106
|
-
&& version_info.deprecated.is_none()
|
|
107
122
|
&& let Ok(v) = semver::Version::parse(latest)
|
|
108
123
|
&& v.pre.is_empty()
|
|
109
124
|
{
|
|
110
125
|
return Ok(latest.clone());
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
// Fall back to finding the latest stable version
|
|
128
|
+
// Fall back to finding the latest stable version from the versions list
|
|
114
129
|
let versions = Self::get_stable_versions(&data);
|
|
115
130
|
versions
|
|
116
131
|
.first()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use super::Registry;
|
|
1
|
+
use super::{Registry, get_with_retry};
|
|
2
2
|
use anyhow::{Result, anyhow};
|
|
3
3
|
use async_trait::async_trait;
|
|
4
4
|
use pep440_rs::{Version, VersionSpecifiers};
|
|
@@ -6,6 +6,7 @@ use reqwest::Client;
|
|
|
6
6
|
use serde::Deserialize;
|
|
7
7
|
use std::collections::HashMap;
|
|
8
8
|
use std::str::FromStr;
|
|
9
|
+
use std::time::Duration;
|
|
9
10
|
|
|
10
11
|
pub struct PyPiRegistry {
|
|
11
12
|
client: Client,
|
|
@@ -31,6 +32,8 @@ impl PyPiRegistry {
|
|
|
31
32
|
let client = Client::builder()
|
|
32
33
|
.gzip(true)
|
|
33
34
|
.user_agent("upd/0.1.0")
|
|
35
|
+
.timeout(Duration::from_secs(30))
|
|
36
|
+
.connect_timeout(Duration::from_secs(10))
|
|
34
37
|
.build()
|
|
35
38
|
.expect("Failed to create HTTP client");
|
|
36
39
|
|
|
@@ -78,7 +81,7 @@ impl PyPiRegistry {
|
|
|
78
81
|
let normalized = package.to_lowercase().replace('_', "-");
|
|
79
82
|
let url = format!("{}/{}/json", self.index_url, normalized);
|
|
80
83
|
|
|
81
|
-
let response = self.client
|
|
84
|
+
let response = get_with_retry(&self.client, &url).await?;
|
|
82
85
|
|
|
83
86
|
if !response.status().is_success() {
|
|
84
87
|
return Err(anyhow!(
|
|
@@ -79,6 +79,36 @@ impl GoModUpdater {
|
|
|
79
79
|
.map(|v| !v.pre.is_empty())
|
|
80
80
|
.unwrap_or(false)
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/// Check if a version is a pseudo-version (commit-based, not a real tag).
|
|
84
|
+
/// Pseudo-versions have the format: v0.0.0-YYYYMMDDHHMMSS-abcdefabcdef
|
|
85
|
+
/// Or for pre-release: v1.2.4-0.YYYYMMDDHHMMSS-abcdefabcdef
|
|
86
|
+
/// These modules have no semver tags (or point to commits), so updating them via registry fails.
|
|
87
|
+
fn is_pseudo_version(version: &str) -> bool {
|
|
88
|
+
// Pseudo-version patterns:
|
|
89
|
+
// 1. v0.0.0-20241217172646-ca3f786aa774 (base version is 0.0.0)
|
|
90
|
+
// 2. v1.2.4-0.20220331215641-2d8c0ab7ef04 (pre-release pseudo after real version)
|
|
91
|
+
//
|
|
92
|
+
// Look for timestamp pattern: 14 digits (YYYYMMDDHHMMSS)
|
|
93
|
+
let contains_timestamp = |s: &str| s.len() == 14 && s.chars().all(|c| c.is_ascii_digit());
|
|
94
|
+
|
|
95
|
+
// Split by dash and look for the timestamp part
|
|
96
|
+
let parts: Vec<&str> = version.split('-').collect();
|
|
97
|
+
|
|
98
|
+
for part in &parts {
|
|
99
|
+
if contains_timestamp(part) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// Handle "0.20220331215641" format (pre-release pseudo)
|
|
103
|
+
if let Some(after_dot) = part.strip_prefix("0.")
|
|
104
|
+
&& contains_timestamp(after_dot)
|
|
105
|
+
{
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
false
|
|
111
|
+
}
|
|
82
112
|
}
|
|
83
113
|
|
|
84
114
|
impl Default for GoModUpdater {
|
|
@@ -149,6 +179,13 @@ impl Updater for GoModUpdater {
|
|
|
149
179
|
continue;
|
|
150
180
|
}
|
|
151
181
|
|
|
182
|
+
// Skip pseudo-versions (commit-based, no semver tags available)
|
|
183
|
+
if Self::is_pseudo_version(current_version) {
|
|
184
|
+
new_lines.push(line.to_string());
|
|
185
|
+
result.unchanged += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
// Fetch latest version
|
|
153
190
|
let version_result = if Self::is_prerelease(current_version) {
|
|
154
191
|
registry
|
|
@@ -276,4 +313,27 @@ replace (
|
|
|
276
313
|
assert!(GoModUpdater::is_prerelease("v1.0.0-rc1"));
|
|
277
314
|
assert!(GoModUpdater::is_prerelease("v1.0.0-beta"));
|
|
278
315
|
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn test_is_pseudo_version() {
|
|
319
|
+
// Standard pseudo-versions (commit-based, no semver tags)
|
|
320
|
+
assert!(GoModUpdater::is_pseudo_version(
|
|
321
|
+
"v0.0.0-20241217172646-ca3f786aa774"
|
|
322
|
+
));
|
|
323
|
+
assert!(GoModUpdater::is_pseudo_version(
|
|
324
|
+
"v0.0.0-20220331215641-2d8c0ab7ef04"
|
|
325
|
+
));
|
|
326
|
+
|
|
327
|
+
// Pre-release pseudo-versions (e.g., for modules with tagged releases)
|
|
328
|
+
assert!(GoModUpdater::is_pseudo_version(
|
|
329
|
+
"v1.2.4-0.20220331215641-2d8c0ab7ef04"
|
|
330
|
+
));
|
|
331
|
+
|
|
332
|
+
// Normal versions should NOT be detected as pseudo-versions
|
|
333
|
+
assert!(!GoModUpdater::is_pseudo_version("v1.0.0"));
|
|
334
|
+
assert!(!GoModUpdater::is_pseudo_version("v1.0.0-alpha.1"));
|
|
335
|
+
assert!(!GoModUpdater::is_pseudo_version("v1.0.0-rc1"));
|
|
336
|
+
assert!(!GoModUpdater::is_pseudo_version("v2.0.0+incompatible"));
|
|
337
|
+
assert!(!GoModUpdater::is_pseudo_version("v1.0.0-beta"));
|
|
338
|
+
}
|
|
279
339
|
}
|
|
@@ -83,9 +83,30 @@ impl RequirementsUpdater {
|
|
|
83
83
|
None
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/// Check if constraint is a simple single-version constraint
|
|
86
|
+
/// Check if constraint is a simple single-version constraint that doesn't need
|
|
87
|
+
/// constraint-aware lookup (i.e., no upper bounds that could be violated)
|
|
87
88
|
fn is_simple_constraint(constraint: &str) -> bool {
|
|
88
|
-
|
|
89
|
+
// If there are multiple constraints (comma-separated), need constraint-aware lookup
|
|
90
|
+
if constraint.contains(',') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If the constraint has an upper-bound operator, need constraint-aware lookup
|
|
95
|
+
// Examples: "<4.2", "<=2.0", "~=1.4" (compatible release - allows only patch updates)
|
|
96
|
+
if constraint.starts_with('<')
|
|
97
|
+
|| constraint.starts_with("<=")
|
|
98
|
+
|| constraint.starts_with("~=")
|
|
99
|
+
{
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Also check for != which could affect version selection
|
|
104
|
+
if constraint.starts_with("!=") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Simple constraints like "==1.0.0", ">=1.0.0", ">1.0.0" are fine
|
|
109
|
+
true
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
fn update_line(&self, line: &str, new_version: &str) -> String {
|
|
@@ -232,10 +253,22 @@ mod tests {
|
|
|
232
253
|
|
|
233
254
|
#[test]
|
|
234
255
|
fn test_is_simple_constraint() {
|
|
256
|
+
// Simple constraints - no upper bound, no exclusions
|
|
235
257
|
assert!(RequirementsUpdater::is_simple_constraint("==1.0.0"));
|
|
236
258
|
assert!(RequirementsUpdater::is_simple_constraint(">=1.0.0"));
|
|
259
|
+
assert!(RequirementsUpdater::is_simple_constraint(">1.0.0"));
|
|
260
|
+
|
|
261
|
+
// Multiple constraints with comma
|
|
237
262
|
assert!(!RequirementsUpdater::is_simple_constraint(">=1.0.0,<2.0.0"));
|
|
238
263
|
assert!(!RequirementsUpdater::is_simple_constraint(">=2.8.0,<9"));
|
|
264
|
+
|
|
265
|
+
// Upper-bound operators (need constraint-aware lookup)
|
|
266
|
+
assert!(!RequirementsUpdater::is_simple_constraint("<4.2"));
|
|
267
|
+
assert!(!RequirementsUpdater::is_simple_constraint("<=2.0"));
|
|
268
|
+
assert!(!RequirementsUpdater::is_simple_constraint("~=1.4"));
|
|
269
|
+
|
|
270
|
+
// Exclusion operator
|
|
271
|
+
assert!(!RequirementsUpdater::is_simple_constraint("!=1.5.0"));
|
|
239
272
|
}
|
|
240
273
|
|
|
241
274
|
#[test]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
use semver::Version;
|
|
2
|
+
|
|
3
|
+
/// Normalize a version string to full semver format (MAJOR.MINOR.PATCH)
|
|
4
|
+
/// "1" -> "1.0.0", "1.2" -> "1.2.0", "1.2.3" -> "1.2.3"
|
|
5
|
+
fn normalize_version(version_str: &str) -> String {
|
|
6
|
+
// Handle prerelease suffix (e.g., "1.0-alpha" -> keep as is after normalization)
|
|
7
|
+
let (base, suffix) = if let Some(idx) = version_str.find('-') {
|
|
8
|
+
(&version_str[..idx], &version_str[idx..])
|
|
9
|
+
} else {
|
|
10
|
+
(version_str, "")
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let parts: Vec<&str> = base.split('.').collect();
|
|
14
|
+
let normalized = match parts.len() {
|
|
15
|
+
1 => format!("{}.0.0", parts[0]),
|
|
16
|
+
2 => format!("{}.{}.0", parts[0], parts[1]),
|
|
17
|
+
_ => base.to_string(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
format!("{}{}", normalized, suffix)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Check if a semver version string represents a stable release
|
|
24
|
+
/// Handles incomplete versions like "0.9" by normalizing to "0.9.0"
|
|
25
|
+
pub fn is_stable_semver(version_str: &str) -> bool {
|
|
26
|
+
let normalized = normalize_version(version_str);
|
|
27
|
+
if let Ok(version) = Version::parse(&normalized) {
|
|
28
|
+
version.pre.is_empty()
|
|
29
|
+
} else {
|
|
30
|
+
// If it still can't parse, assume it's stable (e.g., "*" or complex constraints)
|
|
31
|
+
// This is safer than treating unknown formats as prereleases
|
|
32
|
+
true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Compare two semver version strings
|
|
37
|
+
/// Returns None if either version is invalid
|
|
38
|
+
pub fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
|
|
39
|
+
let va = Version::parse(a).ok()?;
|
|
40
|
+
let vb = Version::parse(b).ok()?;
|
|
41
|
+
Some(va.cmp(&vb))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[cfg(test)]
|
|
45
|
+
mod tests {
|
|
46
|
+
use super::*;
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn test_normalize_version() {
|
|
50
|
+
assert_eq!(normalize_version("1"), "1.0.0");
|
|
51
|
+
assert_eq!(normalize_version("1.2"), "1.2.0");
|
|
52
|
+
assert_eq!(normalize_version("1.2.3"), "1.2.3");
|
|
53
|
+
assert_eq!(normalize_version("0.9"), "0.9.0");
|
|
54
|
+
assert_eq!(normalize_version("1.0-alpha"), "1.0.0-alpha");
|
|
55
|
+
assert_eq!(normalize_version("1-beta.1"), "1.0.0-beta.1");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn test_stable_versions() {
|
|
60
|
+
assert!(is_stable_semver("1.0.0"));
|
|
61
|
+
assert!(is_stable_semver("2.31.0"));
|
|
62
|
+
assert!(is_stable_semver("0.1.0"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn test_incomplete_versions_are_stable() {
|
|
67
|
+
// Incomplete versions like "0.9" should be treated as stable
|
|
68
|
+
assert!(is_stable_semver("0.9"));
|
|
69
|
+
assert!(is_stable_semver("1"));
|
|
70
|
+
assert!(is_stable_semver("2.0"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn test_prerelease_versions() {
|
|
75
|
+
assert!(!is_stable_semver("1.0.0-alpha.1"));
|
|
76
|
+
assert!(!is_stable_semver("1.0.0-beta.2"));
|
|
77
|
+
assert!(!is_stable_semver("1.0.0-rc.1"));
|
|
78
|
+
// Incomplete versions with prerelease suffix
|
|
79
|
+
assert!(!is_stable_semver("1.0-alpha"));
|
|
80
|
+
assert!(!is_stable_semver("0.9-rc1"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[test]
|
|
84
|
+
fn test_version_comparison() {
|
|
85
|
+
assert_eq!(
|
|
86
|
+
compare_versions("1.0.0", "2.0.0"),
|
|
87
|
+
Some(std::cmp::Ordering::Less)
|
|
88
|
+
);
|
|
89
|
+
assert_eq!(
|
|
90
|
+
compare_versions("2.0.0", "1.0.0"),
|
|
91
|
+
Some(std::cmp::Ordering::Greater)
|
|
92
|
+
);
|
|
93
|
+
assert_eq!(
|
|
94
|
+
compare_versions("1.0.0", "1.0.0"),
|
|
95
|
+
Some(std::cmp::Ordering::Equal)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
mod crates_io;
|
|
2
|
-
mod go_proxy;
|
|
3
|
-
mod npm;
|
|
4
|
-
mod pypi;
|
|
5
|
-
|
|
6
|
-
pub use crates_io::CratesIoRegistry;
|
|
7
|
-
pub use go_proxy::GoProxyRegistry;
|
|
8
|
-
pub use npm::NpmRegistry;
|
|
9
|
-
pub use pypi::PyPiRegistry;
|
|
10
|
-
|
|
11
|
-
use anyhow::Result;
|
|
12
|
-
use async_trait::async_trait;
|
|
13
|
-
|
|
14
|
-
#[async_trait]
|
|
15
|
-
pub trait Registry: Send + Sync {
|
|
16
|
-
/// Get the latest stable version of a package
|
|
17
|
-
async fn get_latest_version(&self, package: &str) -> Result<String>;
|
|
18
|
-
|
|
19
|
-
/// Get the latest version including pre-releases
|
|
20
|
-
/// Used when the user's current version is a pre-release
|
|
21
|
-
async fn get_latest_version_including_prereleases(&self, package: &str) -> Result<String> {
|
|
22
|
-
// Default: fall back to stable-only
|
|
23
|
-
self.get_latest_version(package).await
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// Get the latest version matching the given constraints (e.g., ">=2.8.0,<9")
|
|
27
|
-
/// Default implementation falls back to get_latest_version
|
|
28
|
-
async fn get_latest_version_matching(
|
|
29
|
-
&self,
|
|
30
|
-
package: &str,
|
|
31
|
-
constraints: &str,
|
|
32
|
-
) -> Result<String> {
|
|
33
|
-
// Default: ignore constraints and return latest
|
|
34
|
-
let _ = constraints;
|
|
35
|
-
self.get_latest_version(package).await
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// Registry name for display
|
|
39
|
-
fn name(&self) -> &'static str;
|
|
40
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
use semver::Version;
|
|
2
|
-
|
|
3
|
-
/// Check if a semver version string represents a stable release
|
|
4
|
-
pub fn is_stable_semver(version_str: &str) -> bool {
|
|
5
|
-
if let Ok(version) = Version::parse(version_str) {
|
|
6
|
-
version.pre.is_empty()
|
|
7
|
-
} else {
|
|
8
|
-
false
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/// Compare two semver version strings
|
|
13
|
-
/// Returns None if either version is invalid
|
|
14
|
-
pub fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
|
|
15
|
-
let va = Version::parse(a).ok()?;
|
|
16
|
-
let vb = Version::parse(b).ok()?;
|
|
17
|
-
Some(va.cmp(&vb))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
#[cfg(test)]
|
|
21
|
-
mod tests {
|
|
22
|
-
use super::*;
|
|
23
|
-
|
|
24
|
-
#[test]
|
|
25
|
-
fn test_stable_versions() {
|
|
26
|
-
assert!(is_stable_semver("1.0.0"));
|
|
27
|
-
assert!(is_stable_semver("2.31.0"));
|
|
28
|
-
assert!(is_stable_semver("0.1.0"));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
#[test]
|
|
32
|
-
fn test_prerelease_versions() {
|
|
33
|
-
assert!(!is_stable_semver("1.0.0-alpha.1"));
|
|
34
|
-
assert!(!is_stable_semver("1.0.0-beta.2"));
|
|
35
|
-
assert!(!is_stable_semver("1.0.0-rc.1"));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#[test]
|
|
39
|
-
fn test_version_comparison() {
|
|
40
|
-
assert_eq!(
|
|
41
|
-
compare_versions("1.0.0", "2.0.0"),
|
|
42
|
-
Some(std::cmp::Ordering::Less)
|
|
43
|
-
);
|
|
44
|
-
assert_eq!(
|
|
45
|
-
compare_versions("2.0.0", "1.0.0"),
|
|
46
|
-
Some(std::cmp::Ordering::Greater)
|
|
47
|
-
);
|
|
48
|
-
assert_eq!(
|
|
49
|
-
compare_versions("1.0.0", "1.0.0"),
|
|
50
|
-
Some(std::cmp::Ordering::Equal)
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|