From 309c2f5f946b67b618db851af90c967187da35f8 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sat, 13 Dec 2025 18:38:48 -0800 Subject: [PATCH] error --- codex-rs/core/src/codex.rs | 75 +++++-- codex-rs/core/src/compact.rs | 16 +- codex-rs/core/src/compact_remote.rs | 11 +- codex-rs/core/src/error.rs | 36 ++++ codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/tasks/ghost_snapshot.rs | 5 +- codex-rs/core/src/version.rs | 247 ++++++++++++++++++++++ codex-rs/tui/src/updates.rs | 88 +------- 8 files changed, 369 insertions(+), 110 deletions(-) create mode 100644 codex-rs/core/src/version.rs diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b74338243a..126138861f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -82,6 +82,8 @@ use crate::context_manager::ContextManager; use crate::environment_context::EnvironmentContext; use crate::error::CodexErr; use crate::error::Result as CodexResult; +use crate::error::error_event_with_update_nudge; +use crate::error::stream_error_event_with_update_nudge; #[cfg(test)] use crate::exec::StreamOutput; use crate::exec_policy::ExecPolicyUpdateError; @@ -108,7 +110,6 @@ use crate::protocol::SessionConfiguredEvent; use crate::protocol::SkillErrorInfo; use crate::protocol::SkillInfo; use crate::protocol::SkillLoadOutcomeInfo; -use crate::protocol::StreamErrorEvent; use crate::protocol::Submission; use crate::protocol::TokenCountEvent; use crate::protocol::TokenUsage; @@ -201,6 +202,13 @@ fn maybe_push_chat_wire_api_deprecation( }); } +fn compute_is_up_to_date(codex_home: &std::path::Path) -> bool { + let version_file = codex_home.join(crate::version::VERSION_FILENAME); + crate::version::read_latest_version(&version_file) + .and_then(|latest| crate::version::is_up_to_date(&latest, env!("CARGO_PKG_VERSION"))) + .unwrap_or(true) +} + impl Codex { /// Spawn a new [`Codex`] and initialize the session. pub async fn spawn( @@ -251,6 +259,7 @@ impl Codex { error!("failed to refresh available models: {err:?}"); } let model = models_manager.get_model(&config.model, &config).await; + let is_up_to_date = compute_is_up_to_date(&config.codex_home); let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), model: model.clone(), @@ -266,6 +275,7 @@ impl Codex { original_config_do_not_use: Arc::clone(&config), exec_policy, session_source, + is_up_to_date, }; // Generate a unique ID for the lifetime of this Codex session. @@ -430,6 +440,9 @@ pub(crate) struct SessionConfiguration { original_config_do_not_use: Arc, /// Source of the session (cli, vscode, exec, mcp, ...) session_source: SessionSource, + /// Whether the CLI is up to date with the latest known version at session start. + #[allow(dead_code)] + is_up_to_date: bool, } impl SessionConfiguration { @@ -762,6 +775,11 @@ impl Session { state.get_total_token_usage() } + pub(crate) async fn is_up_to_date(&self) -> bool { + let state = self.state.lock().await; + state.session_configuration.is_up_to_date + } + async fn record_initial_history(&self, conversation_history: InitialHistory) { let turn_context = self.new_turn(SessionSettingsUpdate::default()).await; match conversation_history { @@ -791,15 +809,11 @@ impl Session { warn!( "resuming session with different model: previous={prev}, current={curr}" ); - self.send_event( - &turn_context, - EventMsg::Warning(WarningEvent { - message: format!( - "This session was recorded with model `{prev}` but is resuming with `{curr}`. \ + let message = format!( + "This session was recorded with model `{prev}` but is resuming with `{curr}`. \ Consider switching back to `{prev}` as it may affect Codex performance." - ), - }), - ) + ); + self.send_event(&turn_context, EventMsg::Warning(WarningEvent { message })) .await; } } @@ -1380,10 +1394,11 @@ impl Session { let codex_error_info = CodexErrorInfo::ResponseStreamDisconnected { http_status_code: codex_error.http_status_code_value(), }; - let event = EventMsg::StreamError(StreamErrorEvent { - message: message.into(), - codex_error_info: Some(codex_error_info), - }); + let event = EventMsg::StreamError(stream_error_event_with_update_nudge( + message.into(), + Some(codex_error_info), + self.is_up_to_date().await, + )); self.send_event(turn_context, event).await; } @@ -1629,6 +1644,7 @@ mod handlers { use crate::codex::spawn_review_thread; use crate::config::Config; + use crate::error::error_event_with_update_nudge; use crate::mcp::auth::compute_auth_statuses; use crate::mcp::collect_mcp_snapshot_from_manager; use crate::review_prompts::resolve_review_request; @@ -1638,7 +1654,6 @@ mod handlers { use crate::tasks::UserShellCommandTask; use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::protocol::CodexErrorInfo; - use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ListCustomPromptsResponseEvent; @@ -1920,12 +1935,14 @@ mod handlers { && let Err(e) = rec.shutdown().await { warn!("failed to shutdown rollout recorder: {e}"); + let is_up_to_date = sess.is_up_to_date().await; let event = Event { id: sub_id.clone(), - msg: EventMsg::Error(ErrorEvent { - message: "Failed to shutdown rollout recorder".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - }), + msg: EventMsg::Error(error_event_with_update_nudge( + "Failed to shutdown rollout recorder".to_string(), + Some(CodexErrorInfo::Other), + is_up_to_date, + )), }; sess.send_event_raw(event).await; } @@ -1959,12 +1976,14 @@ mod handlers { .await; } Err(err) => { + let is_up_to_date = sess.is_up_to_date().await; let event = Event { id: sub_id, - msg: EventMsg::Error(ErrorEvent { - message: err.to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - }), + msg: EventMsg::Error(error_event_with_update_nudge( + err.to_string(), + Some(CodexErrorInfo::Other), + is_up_to_date, + )), }; sess.send_event(&turn_context, event.msg).await; } @@ -2229,7 +2248,13 @@ pub(crate) async fn run_task( } Err(e) => { info!("Turn error: {e:#}"); - let event = EventMsg::Error(e.to_error_event(None)); + let is_up_to_date = sess.is_up_to_date().await; + let error_event = e.to_error_event(None); + let event = EventMsg::Error(error_event_with_update_nudge( + error_event.message, + error_event.codex_error_info, + is_up_to_date, + )); sess.send_event(&turn_context, event).await; // let the user continue the conversation break; @@ -2723,6 +2748,7 @@ mod tests { original_config_do_not_use: Arc::clone(&config), exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())), session_source: SessionSource::Exec, + is_up_to_date: true, }; let mut state = SessionState::new(session_configuration); @@ -2795,6 +2821,7 @@ mod tests { original_config_do_not_use: Arc::clone(&config), exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())), session_source: SessionSource::Exec, + is_up_to_date: true, }; let mut state = SessionState::new(session_configuration); @@ -2999,6 +3026,7 @@ mod tests { original_config_do_not_use: Arc::clone(&config), exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())), session_source: SessionSource::Exec, + is_up_to_date: true, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_family = ModelsManager::construct_model_family_offline( @@ -3089,6 +3117,7 @@ mod tests { original_config_do_not_use: Arc::clone(&config), exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())), session_source: SessionSource::Exec, + is_up_to_date: true, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_family = ModelsManager::construct_model_family_offline( diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 1a90b7b223..9a18319736 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -8,6 +8,7 @@ use crate::codex::TurnContext; use crate::codex::get_last_assistant_message_from_turn; use crate::error::CodexErr; use crate::error::Result as CodexResult; +use crate::error::error_event_with_update_nudge; use crate::features::Feature; use crate::protocol::CompactedItem; use crate::protocol::ContextCompactedEvent; @@ -67,6 +68,7 @@ async fn run_compact_task_inner( input: Vec, ) { let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input); + let is_up_to_date = sess.is_up_to_date().await; let mut history = sess.clone_history().await; history.record_items( @@ -125,7 +127,12 @@ async fn run_compact_task_inner( continue; } sess.set_total_tokens_full(turn_context.as_ref()).await; - let event = EventMsg::Error(e.to_error_event(None)); + let error_event = e.to_error_event(None); + let event = EventMsg::Error(error_event_with_update_nudge( + error_event.message, + error_event.codex_error_info, + is_up_to_date, + )); sess.send_event(&turn_context, event).await; return; } @@ -142,7 +149,12 @@ async fn run_compact_task_inner( tokio::time::sleep(delay).await; continue; } else { - let event = EventMsg::Error(e.to_error_event(None)); + let error_event = e.to_error_event(None); + let event = EventMsg::Error(error_event_with_update_nudge( + error_event.message, + error_event.codex_error_info, + is_up_to_date, + )); sess.send_event(&turn_context, event).await; return; } diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index b855f28d39..22978d371f 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -4,6 +4,7 @@ use crate::Prompt; use crate::codex::Session; use crate::codex::TurnContext; use crate::error::Result as CodexResult; +use crate::error::error_event_with_update_nudge; use crate::protocol::CompactedItem; use crate::protocol::ContextCompactedEvent; use crate::protocol::EventMsg; @@ -29,9 +30,13 @@ pub(crate) async fn run_remote_compact_task(sess: Arc, turn_context: Ar async fn run_remote_compact_task_inner(sess: &Arc, turn_context: &Arc) { if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await { - let event = EventMsg::Error( - err.to_error_event(Some("Error running remote compact task".to_string())), - ); + let is_up_to_date = sess.is_up_to_date().await; + let error_event = err.to_error_event(Some("Error running remote compact task".to_string())); + let event = EventMsg::Error(error_event_with_update_nudge( + error_event.message, + error_event.codex_error_info, + is_up_to_date, + )); sess.send_event(turn_context, event).await; } } diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index e8fa91d26e..b621393c15 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -12,6 +12,7 @@ use codex_protocol::ConversationId; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::RateLimitSnapshot; +use codex_protocol::protocol::StreamErrorEvent; use reqwest::StatusCode; use serde_json; use std::io; @@ -23,6 +24,41 @@ pub type Result = std::result::Result; /// Limit UI error messages to a reasonable size while keeping useful context. const ERROR_MESSAGE_UI_MAX_BYTES: usize = 2 * 1024; // 4 KiB +const UPDATE_AVAILABLE_NUDGE: &str = "Update available. Run `codex update`."; + +pub(crate) fn error_event_with_update_nudge( + message: String, + codex_error_info: Option, + is_up_to_date: bool, +) -> ErrorEvent { + let message = maybe_append_update_nudge(message, is_up_to_date); + ErrorEvent { + message, + codex_error_info, + } +} + +pub(crate) fn stream_error_event_with_update_nudge( + message: String, + codex_error_info: Option, + is_up_to_date: bool, +) -> StreamErrorEvent { + StreamErrorEvent { + message: maybe_append_update_nudge(message, is_up_to_date), + codex_error_info, + } +} + +fn maybe_append_update_nudge(message: String, is_up_to_date: bool) -> String { + if is_up_to_date { + return message; + } + if message.is_empty() { + UPDATE_AVAILABLE_NUDGE.to_string() + } else { + format!("{message}\n{UPDATE_AVAILABLE_NUDGE}") + } +} #[derive(Error, Debug)] pub enum SandboxErr { diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 11b49c78c4..a4a7a9ecd7 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -80,6 +80,7 @@ pub mod spawn; pub mod terminal; mod tools; pub mod turn_diff_tracker; +pub mod version; pub use rollout::ARCHIVED_SESSIONS_SUBDIR; pub use rollout::INTERACTIVE_SESSION_SOURCES; pub use rollout::RolloutRecorder; diff --git a/codex-rs/core/src/tasks/ghost_snapshot.rs b/codex-rs/core/src/tasks/ghost_snapshot.rs index 7e84c330f6..e9ea0ae95c 100644 --- a/codex-rs/core/src/tasks/ghost_snapshot.rs +++ b/codex-rs/core/src/tasks/ghost_snapshot.rs @@ -54,11 +54,12 @@ impl SessionTask for GhostSnapshotTask { tokio::task::spawn(async move { tokio::select! { _ = tokio::time::sleep(SNAPSHOT_WARNING_THRESHOLD) => { - session_for_warning.session + session_for_warning + .session .send_event( &ctx_for_warning, EventMsg::Warning(WarningEvent { - message: "Repository snapshot is taking longer than expected. Large untracked or ignored files can slow snapshots; consider adding large files or directories to .gitignore or disabling `undo` in your config.".to_string() + message: "Repository snapshot is taking longer than expected. Large untracked or ignored files can slow snapshots; consider adding large files or directories to .gitignore or disabling `undo` in your config.".to_string(), }), ) .await; diff --git a/codex-rs/core/src/version.rs b/codex-rs/core/src/version.rs new file mode 100644 index 0000000000..9a5a0ea6f5 --- /dev/null +++ b/codex-rs/core/src/version.rs @@ -0,0 +1,247 @@ +use std::path::Path; + +use chrono::DateTime; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; + +pub const VERSION_FILENAME: &str = "version.json"; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VersionInfo { + pub latest_version: String, + // ISO-8601 timestamp (RFC3339) + pub last_checked_at: DateTime, + #[serde(default)] + pub dismissed_version: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Version { + major: u64, + minor: u64, + patch: u64, + pre: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum PrereleaseIdent { + Numeric(u64), + Alpha(String), +} + +impl Version { + pub fn parse(input: &str) -> Option { + let mut input = input.trim(); + if let Some(stripped) = input.strip_prefix("rust-v") { + input = stripped; + } + if let Some(stripped) = input.strip_prefix('v') { + input = stripped; + } + let input = input.splitn(2, '+').next().unwrap_or(input); + let mut parts = input.splitn(2, '-'); + let core = parts.next()?; + let pre = parts.next(); + let mut nums = core.split('.'); + let major = nums.next()?.parse::().ok()?; + let minor = nums.next()?.parse::().ok()?; + let patch = nums.next()?.parse::().ok()?; + if nums.next().is_some() { + return None; + } + let pre = match pre { + None => None, + Some(value) if value.is_empty() => None, + Some(value) => { + let mut idents = Vec::new(); + for ident in value.split('.') { + if ident.is_empty() { + return None; + } + let parsed = if ident.chars().all(|c| c.is_ascii_digit()) { + ident.parse::().ok().map(PrereleaseIdent::Numeric) + } else { + Some(PrereleaseIdent::Alpha(ident.to_string())) + }; + idents.push(parsed?); + } + Some(idents) + } + }; + Some(Self { + major, + minor, + patch, + pre, + }) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.major.cmp(&other.major) { + std::cmp::Ordering::Equal => {} + ordering => return ordering, + } + match self.minor.cmp(&other.minor) { + std::cmp::Ordering::Equal => {} + ordering => return ordering, + } + match self.patch.cmp(&other.patch) { + std::cmp::Ordering::Equal => {} + ordering => return ordering, + } + match (&self.pre, &other.pre) { + (None, None) => std::cmp::Ordering::Equal, + (None, Some(_)) => std::cmp::Ordering::Greater, + (Some(_), None) => std::cmp::Ordering::Less, + (Some(left), Some(right)) => compare_prerelease_idents(left, right), + } + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +pub fn is_newer(latest: &str, current: &str) -> Option { + let latest = Version::parse(latest)?; + if latest.pre.is_some() { + return Some(false); + } + let current = Version::parse(current)?; + let current = Version { + pre: None, + ..current + }; + Some(latest > current) +} + +pub fn is_up_to_date(latest: &str, current: &str) -> Option { + let latest = Version::parse(latest)?; + if latest.pre.is_some() { + return Some(true); + } + let current = Version::parse(current)?; + let current = Version { + pre: None, + ..current + }; + Some(current >= latest) +} + +pub fn read_version_info(version_file: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(version_file)?; + Ok(serde_json::from_str(&contents)?) +} + +pub fn read_latest_version(version_file: &Path) -> Option { + read_version_info(version_file) + .ok() + .map(|info| info.latest_version) +} + +pub fn extract_version_from_cask(cask_contents: &str) -> anyhow::Result { + cask_contents + .lines() + .find_map(|line| { + let line = line.trim(); + line.strip_prefix("version \"") + .and_then(|rest| rest.strip_suffix('"')) + .map(ToString::to_string) + }) + .ok_or_else(|| anyhow::anyhow!("Failed to find version in Homebrew cask file")) +} + +pub fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result { + latest_tag_name + .strip_prefix("rust-v") + .map(str::to_owned) + .ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'")) +} + +fn compare_prerelease_idents( + left: &[PrereleaseIdent], + right: &[PrereleaseIdent], +) -> std::cmp::Ordering { + for (l, r) in left.iter().zip(right.iter()) { + let ordering = match (l, r) { + (PrereleaseIdent::Numeric(a), PrereleaseIdent::Numeric(b)) => a.cmp(b), + (PrereleaseIdent::Alpha(a), PrereleaseIdent::Alpha(b)) => a.cmp(b), + (PrereleaseIdent::Numeric(_), PrereleaseIdent::Alpha(_)) => std::cmp::Ordering::Less, + (PrereleaseIdent::Alpha(_), PrereleaseIdent::Numeric(_)) => std::cmp::Ordering::Greater, + }; + if ordering != std::cmp::Ordering::Equal { + return ordering; + } + } + left.len().cmp(&right.len()) +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn prerelease_current_is_ignored() { + assert_eq!(is_newer("1.2.3", "1.2.3-alpha.1"), Some(false)); + assert_eq!(is_up_to_date("1.2.3", "1.2.3-alpha.1"), Some(true)); + } + + #[test] + fn prerelease_latest_is_ignored() { + assert_eq!(is_newer("1.2.4-alpha.1", "1.2.3"), Some(false)); + assert_eq!(is_up_to_date("1.2.4-alpha.1", "1.2.3"), Some(true)); + } + + #[test] + fn prerelease_latest_is_not_considered_newer() { + assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), Some(false)); + assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), Some(false)); + } + + #[test] + fn plain_semver_comparisons_work() { + assert_eq!(is_newer("0.11.1", "0.11.0"), Some(true)); + assert_eq!(is_newer("0.11.0", "0.11.1"), Some(false)); + assert_eq!(is_newer("1.0.0", "0.9.9"), Some(true)); + assert_eq!(is_newer("0.9.9", "1.0.0"), Some(false)); + } + + #[test] + fn whitespace_is_ignored() { + assert_eq!(Version::parse(" 1.2.3 \n").is_some(), true); + assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true)); + } + + #[test] + fn parses_version_from_cask_contents() { + let cask = r#" + cask "codex" do + version "0.55.0" + end + "#; + assert_eq!( + extract_version_from_cask(cask).expect("failed to parse version"), + "0.55.0" + ); + } + + #[test] + fn extracts_version_from_latest_tag() { + assert_eq!( + extract_version_from_latest_tag("rust-v1.5.0").expect("failed to parse version"), + "1.5.0" + ); + } + + #[test] + fn latest_tag_without_prefix_is_invalid() { + assert!(extract_version_from_latest_tag("v1.5.0").is_err()); + } +} diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index 89fd6f32f6..f36ea0518e 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -2,13 +2,17 @@ use crate::update_action; use crate::update_action::UpdateAction; -use chrono::DateTime; use chrono::Duration; use chrono::Utc; use codex_core::config::Config; use codex_core::default_client::create_client; +use codex_core::version::VERSION_FILENAME; +use codex_core::version::VersionInfo; +use codex_core::version::extract_version_from_cask; +use codex_core::version::extract_version_from_latest_tag; +use codex_core::version::is_newer; +use codex_core::version::read_version_info; use serde::Deserialize; -use serde::Serialize; use std::path::Path; use std::path::PathBuf; @@ -45,16 +49,6 @@ pub fn get_upgrade_version(config: &Config) -> Option { }) } -#[derive(Serialize, Deserialize, Debug, Clone)] -struct VersionInfo { - latest_version: String, - // ISO-8601 timestamp (RFC3339) - last_checked_at: DateTime, - #[serde(default)] - dismissed_version: Option, -} - -const VERSION_FILENAME: &str = "version.json"; // We use the latest version from the cask if installation is via homebrew - homebrew does not immediately pick up the latest release and can lag behind. const HOMEBREW_CASK_URL: &str = "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/codex.rb"; @@ -69,11 +63,6 @@ fn version_filepath(config: &Config) -> PathBuf { config.codex_home.join(VERSION_FILENAME) } -fn read_version_info(version_file: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(version_file)?; - Ok(serde_json::from_str(&contents)?) -} - async fn check_for_update(version_file: &Path) -> anyhow::Result<()> { let latest_version = match update_action::get_update_action() { Some(UpdateAction::BrewUpgrade) => { @@ -116,32 +105,6 @@ async fn check_for_update(version_file: &Path) -> anyhow::Result<()> { Ok(()) } -fn is_newer(latest: &str, current: &str) -> Option { - match (parse_version(latest), parse_version(current)) { - (Some(l), Some(c)) => Some(l > c), - _ => None, - } -} - -fn extract_version_from_cask(cask_contents: &str) -> anyhow::Result { - cask_contents - .lines() - .find_map(|line| { - let line = line.trim(); - line.strip_prefix("version \"") - .and_then(|rest| rest.strip_suffix('"')) - .map(ToString::to_string) - }) - .ok_or_else(|| anyhow::anyhow!("Failed to find version in Homebrew cask file")) -} - -fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result { - latest_tag_name - .strip_prefix("rust-v") - .map(str::to_owned) - .ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'")) -} - /// Returns the latest version to show in a popup, if it should be shown. /// This respects the user's dismissal choice for the current latest version. pub fn get_upgrade_version_for_popup(config: &Config) -> Option { @@ -177,48 +140,14 @@ pub async fn dismiss_version(config: &Config, version: &str) -> anyhow::Result<( Ok(()) } -fn parse_version(v: &str) -> Option<(u64, u64, u64)> { - let mut iter = v.trim().split('.'); - let maj = iter.next()?.parse::().ok()?; - let min = iter.next()?.parse::().ok()?; - let pat = iter.next()?.parse::().ok()?; - Some((maj, min, pat)) -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn parses_version_from_cask_contents() { - let cask = r#" - cask "codex" do - version "0.55.0" - end - "#; - assert_eq!( - extract_version_from_cask(cask).expect("failed to parse version"), - "0.55.0" - ); - } - - #[test] - fn extracts_version_from_latest_tag() { - assert_eq!( - extract_version_from_latest_tag("rust-v1.5.0").expect("failed to parse version"), - "1.5.0" - ); - } - - #[test] - fn latest_tag_without_prefix_is_invalid() { - assert!(extract_version_from_latest_tag("v1.5.0").is_err()); - } - #[test] fn prerelease_version_is_not_considered_newer() { - assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), None); - assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), None); + assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), Some(false)); + assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), Some(false)); } #[test] @@ -231,7 +160,6 @@ mod tests { #[test] fn whitespace_is_ignored() { - assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3))); assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true)); } }