From d947ccd934a76182524ed168302cb066934068eb Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sun, 28 Jun 2026 09:24:20 -0400 Subject: [PATCH] Clarify update checks for releases --- README.md | 10 ++-- install.sh | 4 +- src/bin/dosh-client.rs | 113 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 71ebaef..8cfee64 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ dosh recover HOST # clear cached attach state and re-check Examples: ```sh -dosh palav -dosh palav tm -dosh forward palav -L 8080:127.0.0.1:80 -dosh forward palav -D 1080 -dosh forward palav -R 2222:127.0.0.1:22 +dosh server +dosh server tm +dosh forward server -L 8080:127.0.0.1:80 +dosh forward server -D 1080 +dosh forward server -R 2222:127.0.0.1:22 ``` Agent forwarding is opt-in with `-A` and must be enabled on the server. diff --git a/install.sh b/install.sh index a901666..728f989 100755 --- a/install.sh +++ b/install.sh @@ -547,8 +547,8 @@ EOF fi cat >"$hosts_config" < Option { Some(format!("{web}/releases/latest/download/{artifact}")) } -fn latest_release_tag_download_url(repo: &str, artifact: &str) -> Result> { +fn release_tag_from_effective_url(web: &str, effective: &str) -> Option { + let prefix = format!("{web}/releases/tag/"); + let tag = effective.strip_prefix(&prefix)?; + if tag.is_empty() { + None + } else { + Some(tag.to_string()) + } +} + +fn release_version_from_tag(tag: &str) -> &str { + tag.strip_prefix('v').unwrap_or(tag) +} + +fn update_version_status(local: &str, latest: &str) -> &'static str { + match compare_dotted_versions(latest, local) { + std::cmp::Ordering::Equal => "current", + std::cmp::Ordering::Greater => "update available", + std::cmp::Ordering::Less => "different", + } +} + +fn compare_dotted_versions(left: &str, right: &str) -> std::cmp::Ordering { + let left = parse_dotted_version(left); + let right = parse_dotted_version(right); + let width = left.len().max(right.len()); + for index in 0..width { + let ordering = left + .get(index) + .copied() + .unwrap_or(0) + .cmp(&right.get(index).copied().unwrap_or(0)); + if ordering != std::cmp::Ordering::Equal { + return ordering; + } + } + std::cmp::Ordering::Equal +} + +fn parse_dotted_version(version: &str) -> Vec { + version + .split(|byte: char| !byte.is_ascii_digit()) + .filter(|part| !part.is_empty()) + .map(|part| part.parse::().unwrap_or(0)) + .collect() +} + +fn latest_release_tag(repo: &str) -> Result> { let web = repo .strip_suffix(".git") .unwrap_or(repo) @@ -1525,14 +1572,18 @@ fn latest_release_tag_download_url(repo: &str, artifact: &str) -> Result Result> { + let web = repo + .strip_suffix(".git") + .unwrap_or(repo) + .trim_end_matches('/'); + if !web.starts_with("http://") && !web.starts_with("https://") { return Ok(None); } - Ok(Some(format!("{web}/releases/download/{tag}/{artifact}"))) + Ok(latest_release_tag(repo)?.map(|tag| format!("{web}/releases/download/{tag}/{artifact}"))) } fn url_reachable(url: &str) -> Result { @@ -1556,9 +1607,20 @@ fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<( let installer = format!("{raw_base}/raw/branch/main/install.sh"); let artifact = release_artifact_name(); if check_only { - println!("dosh {}", env!("CARGO_PKG_VERSION")); + let local_version = env!("CARGO_PKG_VERSION"); + println!("dosh {local_version}"); println!("repo: {repo}"); println!("installer: {installer}"); + match latest_release_tag(&repo)? { + Some(tag) => { + let latest_version = release_version_from_tag(&tag); + println!( + "latest: {latest_version} ({})", + update_version_status(local_version, latest_version) + ); + } + None => println!("latest: unknown"), + } if let Some(latest_url) = latest_release_download_url(&repo, artifact) { let mut status = "missing"; let mut display_url = latest_url.clone(); @@ -5557,12 +5619,13 @@ mod tests { load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration, queue_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr, - render_status_clear, render_status_overlay, requested_env, resolved_startup_command, - retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host, - server_version_mismatch, should_hold_during_startup_gate, should_hold_post_submit_input, - split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user, - startup_command, status_ssh_target, strip_stale_mouse_reports, toml_bare_key_or_quoted, - update_check_requested, valid_forward_host, + release_tag_from_effective_url, release_version_from_tag, render_status_clear, + render_status_overlay, requested_env, resolved_startup_command, retransmit_stream_opens, + rewrite_forward_command, selected_predict_mode, selected_udp_host, server_version_mismatch, + should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit, + ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, + strip_stale_mouse_reports, toml_bare_key_or_quoted, update_check_requested, + update_version_status, valid_forward_host, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -6435,6 +6498,28 @@ mod tests { ); } + #[test] + fn release_tag_parses_effective_latest_url() { + assert_eq!( + release_tag_from_effective_url( + "https://example.com/owner/dosh", + "https://example.com/owner/dosh/releases/tag/v1.0.0" + ) + .as_deref(), + Some("v1.0.0") + ); + assert_eq!(release_version_from_tag("v1.0.0"), "1.0.0"); + assert_eq!(release_version_from_tag("2026.06.28"), "2026.06.28"); + assert!(release_tag_from_effective_url("https://example.com/owner/dosh", "").is_none()); + } + + #[test] + fn update_version_status_compares_numeric_parts() { + assert_eq!(update_version_status("0.1.5", "0.1.5"), "current"); + assert_eq!(update_version_status("0.1.9", "0.1.10"), "update available"); + assert_eq!(update_version_status("1.0.0", "0.9.9"), "different"); + } + #[test] fn resolved_startup_command_expands_global_extension() { let mut config = ClientConfig::default();