Skip to main content

git_worktree_manager/
update.rs

1/// Auto-update check via GitHub Releases.
2///
3/// Mirrors src/git_worktree_manager/update.py — but uses GitHub API instead of PyPI.
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
9
10/// Cache for update check results.
11#[derive(Debug, Serialize, Deserialize, Default)]
12struct UpdateCache {
13    last_check: String,
14    latest_version: Option<String>,
15}
16
17fn get_cache_path() -> PathBuf {
18    dirs::cache_dir()
19        .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")))
20        .join("git-worktree-manager")
21        .join("update_check.json")
22}
23
24fn load_cache() -> UpdateCache {
25    let path = get_cache_path();
26    if !path.exists() {
27        return UpdateCache::default();
28    }
29    std::fs::read_to_string(&path)
30        .ok()
31        .and_then(|c| serde_json::from_str(&c).ok())
32        .unwrap_or_default()
33}
34
35fn save_cache(cache: &UpdateCache) {
36    let path = get_cache_path();
37    if let Some(parent) = path.parent() {
38        let _ = std::fs::create_dir_all(parent);
39    }
40    if let Ok(content) = serde_json::to_string_pretty(cache) {
41        let _ = std::fs::write(&path, content);
42    }
43}
44
45fn today_str() -> String {
46    // Simple date string YYYY-MM-DD
47    crate::session::chrono_now_iso_pub()
48        .split('T')
49        .next()
50        .unwrap_or("")
51        .to_string()
52}
53
54fn should_check() -> bool {
55    let config = crate::config::load_config().unwrap_or_default();
56    if !config.update.auto_check {
57        return false;
58    }
59    let cache = load_cache();
60    cache.last_check != today_str()
61}
62
63/// Check for updates (called on startup, non-blocking).
64pub fn check_for_update_if_needed() {
65    if !should_check() {
66        return;
67    }
68
69    // Check in background — fetch latest from GitHub API
70    if let Some(latest) = fetch_latest_version() {
71        let cache = UpdateCache {
72            last_check: today_str(),
73            latest_version: Some(latest.clone()),
74        };
75        save_cache(&cache);
76
77        if is_newer(&latest, CURRENT_VERSION) {
78            eprintln!(
79                "\ngit-worktree-manager {} is available (current: {})",
80                latest, CURRENT_VERSION
81            );
82            eprintln!("Run 'cw upgrade' to update.\n");
83        }
84    } else {
85        // Save cache even on failure to avoid retrying today
86        let cache = UpdateCache {
87            last_check: today_str(),
88            latest_version: None,
89        };
90        save_cache(&cache);
91    }
92}
93
94/// Fetch latest version from GitHub Releases API.
95fn fetch_latest_version() -> Option<String> {
96    // Use ureq or a simple curl for the HTTP request
97    // For now, use subprocess to call curl (avoids adding ureq dependency)
98    let output = std::process::Command::new("curl")
99        .args([
100            "-s",
101            "-H",
102            "Accept: application/vnd.github+json",
103            "https://api.github.com/repos/DaveDev42/git-worktree-manager/releases/latest",
104        ])
105        .output()
106        .ok()?;
107
108    if !output.status.success() {
109        return None;
110    }
111
112    let body = String::from_utf8_lossy(&output.stdout);
113    let json: serde_json::Value = serde_json::from_str(&body).ok()?;
114    let tag = json.get("tag_name")?.as_str()?;
115
116    // Strip "v" prefix
117    Some(tag.strip_prefix('v').unwrap_or(tag).to_string())
118}
119
120/// Compare version strings (simple semver).
121fn is_newer(latest: &str, current: &str) -> bool {
122    let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
123    let l = parse(latest);
124    let c = parse(current);
125    l > c
126}
127
128/// Manual upgrade command.
129pub fn upgrade() {
130    println!("git-worktree-manager v{}", CURRENT_VERSION);
131
132    if let Some(latest) = fetch_latest_version() {
133        if is_newer(&latest, CURRENT_VERSION) {
134            println!("New version available: v{}", latest);
135            println!("\nTo upgrade:");
136            println!(
137                "  Download from: https://github.com/DaveDev42/git-worktree-manager/releases/latest"
138            );
139
140            #[cfg(target_os = "macos")]
141            println!("  Or: brew upgrade git-worktree-manager");
142        } else {
143            println!("Already up to date.");
144        }
145    } else {
146        println!("Could not check for updates. Check your internet connection.");
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_is_newer() {
156        assert!(is_newer("0.2.0", "0.1.0"));
157        assert!(is_newer("1.0.0", "0.10.0"));
158        assert!(!is_newer("0.1.0", "0.1.0"));
159        assert!(!is_newer("0.1.0", "0.2.0"));
160    }
161}