From ecf8b08595580e8a07fc4cf85e4800b1c1213cf3 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Fri, 3 Apr 2026 21:34:20 +0300 Subject: feat: improve update-ready and release notes ux --- src/release_notes.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 11 deletions(-) (limited to 'src/release_notes.rs') diff --git a/src/release_notes.rs b/src/release_notes.rs index fcee9a41..7d83dd15 100644 --- a/src/release_notes.rs +++ b/src/release_notes.rs @@ -16,6 +16,12 @@ pub struct ReleaseNotes { struct StoredReleaseNotes { version: String, body: String, + #[serde(default = "default_show_on_startup")] + show_on_startup: bool, +} + +fn default_show_on_startup() -> bool { + true } pub fn pending_path() -> PathBuf { @@ -34,15 +40,22 @@ fn save_pending_to_path(path: &Path, version: &str, body: &str) -> std::io::Resu return clear_pending_at(path); } + write_stored_to_path( + path, + &StoredReleaseNotes { + version: version.to_string(), + body, + show_on_startup: true, + }, + ) +} + +fn write_stored_to_path(path: &Path, stored: &StoredReleaseNotes) -> std::io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - let stored = StoredReleaseNotes { - version: version.to_string(), - body, - }; - let json = serde_json::to_string_pretty(&stored).map_err(std::io::Error::other)?; + let json = serde_json::to_string_pretty(stored).map_err(std::io::Error::other)?; let tmp_path = path.with_extension(format!("json.tmp.{}", std::process::id())); fs::write(&tmp_path, json)?; if let Err(err) = fs::rename(&tmp_path, path) { @@ -52,31 +65,61 @@ fn save_pending_to_path(path: &Path, version: &str, body: &str) -> std::io::Resu Ok(()) } +fn load_stored_from_path(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() +} + pub fn load_pending_for_current_version() -> Option { load_pending_from_path(&pending_path(), env!("CARGO_PKG_VERSION")) } +pub fn load_latest() -> Option { + load_latest_from_path(&pending_path(), env!("CARGO_PKG_VERSION")) +} + fn load_pending_from_path(path: &Path, current_version: &str) -> Option { - let content = fs::read_to_string(path).ok()?; - let stored: StoredReleaseNotes = serde_json::from_str(&content).ok()?; - if stored.version != current_version { + let stored = load_stored_from_path(path)?; + if stored.version != current_version || !stored.show_on_startup { return None; } + release_notes_from_stored(stored, current_version) +} +fn load_latest_from_path(path: &Path, current_version: &str) -> Option { + let stored = load_stored_from_path(path)?; + release_notes_from_stored(stored, current_version) +} + +fn release_notes_from_stored( + stored: StoredReleaseNotes, + current_version: &str, +) -> Option { let body = normalize_body(&stored.body); if body.is_empty() { return None; } Some(ReleaseNotes { + preview: stored.version != current_version, version: stored.version, body, - preview: false, }) } -pub fn clear_pending() -> std::io::Result<()> { - clear_pending_at(&pending_path()) +pub fn mark_current_version_seen() -> std::io::Result<()> { + mark_current_version_seen_at(&pending_path(), env!("CARGO_PKG_VERSION")) +} + +fn mark_current_version_seen_at(path: &Path, current_version: &str) -> std::io::Result<()> { + let Some(mut stored) = load_stored_from_path(path) else { + return Ok(()); + }; + if stored.version != current_version || !stored.show_on_startup { + return Ok(()); + } + stored.show_on_startup = false; + write_stored_to_path(path, &stored) } fn clear_pending_at(path: &Path) -> std::io::Result<()> { @@ -151,4 +194,64 @@ mod tests { "### Changed\n- One\n\n### Fixed\n- Two" ); } + + #[test] + fn load_latest_keeps_future_version_previewable_before_restart() { + let path = std::env::temp_dir().join(format!( + "herdr-release-notes-{}-{}.json", + std::process::id(), + "preview" + )); + let _ = clear_pending_at(&path); + save_pending_to_path(&path, "0.3.2", "### Changed\n- One").unwrap(); + + let notes = load_latest_from_path(&path, "0.3.1").expect("latest notes"); + assert_eq!(notes.version, "0.3.2"); + assert_eq!(notes.body, "### Changed\n- One"); + assert!(notes.preview); + + clear_pending_at(&path).unwrap(); + } + + #[test] + fn marking_current_version_seen_preserves_latest_notes_without_startup_modal() { + let path = std::env::temp_dir().join(format!( + "herdr-release-notes-{}-{}.json", + std::process::id(), + "seen" + )); + let _ = clear_pending_at(&path); + save_pending_to_path(&path, "0.3.1", "### Changed\n- One").unwrap(); + + let startup = load_pending_from_path(&path, "0.3.1").expect("startup notes"); + assert!(!startup.preview); + + mark_current_version_seen_at(&path, "0.3.1").unwrap(); + + assert!(load_pending_from_path(&path, "0.3.1").is_none()); + let latest = load_latest_from_path(&path, "0.3.1").expect("latest notes"); + assert_eq!(latest.version, "0.3.1"); + assert!(!latest.preview); + + clear_pending_at(&path).unwrap(); + } + + #[test] + fn legacy_notes_without_show_on_startup_default_to_unseen() { + let path = std::env::temp_dir().join(format!( + "herdr-release-notes-{}-{}.json", + std::process::id(), + "legacy" + )); + let _ = clear_pending_at(&path); + fs::write( + &path, + "{\n \"version\": \"0.3.1\",\n \"body\": \"### Changed\\n- One\"\n}", + ) + .unwrap(); + + assert!(load_pending_from_path(&path, "0.3.1").is_some()); + + clear_pending_at(&path).unwrap(); + } } -- cgit v1.2.3