git_worktree_manager/
update.rs1use std::io::IsTerminal;
4use std::path::PathBuf;
5use std::process::Command;
6
7use console::style;
8use serde::{Deserialize, Serialize};
9
10use crate::constants::home_dir_or_fallback;
11
12const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
13const REPO_OWNER: &str = "DaveDev42";
14const REPO_NAME: &str = "git-worktree-manager";
15
16#[derive(Debug, Serialize, Deserialize, Default)]
18struct UpdateCache {
19 last_check: String,
20 latest_version: Option<String>,
21}
22
23fn get_cache_path() -> PathBuf {
24 dirs::cache_dir()
25 .unwrap_or_else(home_dir_or_fallback)
26 .join("git-worktree-manager")
27 .join("update_check.json")
28}
29
30fn load_cache() -> UpdateCache {
31 let path = get_cache_path();
32 if !path.exists() {
33 return UpdateCache::default();
34 }
35 std::fs::read_to_string(&path)
36 .ok()
37 .and_then(|c| serde_json::from_str(&c).ok())
38 .unwrap_or_default()
39}
40
41fn save_cache(cache: &UpdateCache) {
42 let path = get_cache_path();
43 if let Some(parent) = path.parent() {
44 let _ = std::fs::create_dir_all(parent);
45 }
46 if let Ok(content) = serde_json::to_string_pretty(cache) {
47 let _ = std::fs::write(&path, content);
48 }
49}
50
51fn today_str() -> String {
52 crate::session::chrono_now_iso_pub()
53 .split('T')
54 .next()
55 .unwrap_or("")
56 .to_string()
57}
58
59fn should_check() -> bool {
60 let config = crate::config::load_config().unwrap_or_default();
61 if !config.update.auto_check {
62 return false;
63 }
64 let cache = load_cache();
65 cache.last_check != today_str()
66}
67
68pub fn check_for_update_if_needed() {
70 if !should_check() {
71 return;
72 }
73
74 if let Some(latest) = fetch_latest_version() {
75 let cache = UpdateCache {
76 last_check: today_str(),
77 latest_version: Some(latest.clone()),
78 };
79 save_cache(&cache);
80
81 if is_newer(&latest, CURRENT_VERSION) {
82 eprintln!(
83 "\ngit-worktree-manager {} is available (current: {})",
84 latest, CURRENT_VERSION
85 );
86 eprintln!("Run 'gw upgrade' to update.\n");
87 }
88 } else {
89 let cache = UpdateCache {
90 last_check: today_str(),
91 latest_version: None,
92 };
93 save_cache(&cache);
94 }
95}
96
97fn fetch_latest_version() -> Option<String> {
99 let output = Command::new("curl")
100 .args([
101 "-s",
102 "-H",
103 "Accept: application/vnd.github+json",
104 &format!(
105 "https://api.github.com/repos/{}/{}/releases/latest",
106 REPO_OWNER, REPO_NAME
107 ),
108 ])
109 .output()
110 .ok()?;
111
112 if !output.status.success() {
113 return None;
114 }
115
116 let body = String::from_utf8_lossy(&output.stdout);
117 let json: serde_json::Value = serde_json::from_str(&body).ok()?;
118 let tag = json.get("tag_name")?.as_str()?;
119
120 Some(tag.strip_prefix('v').unwrap_or(tag).to_string())
122}
123
124fn is_newer(latest: &str, current: &str) -> bool {
126 let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
127 let l = parse(latest);
128 let c = parse(current);
129 l > c
130}
131
132fn is_homebrew_install() -> bool {
134 let exe = match std::env::current_exe() {
135 Ok(p) => p,
136 Err(_) => return false,
137 };
138 let real_path = match std::fs::canonicalize(&exe) {
140 Ok(p) => p,
141 Err(_) => exe,
142 };
143 let path_str = real_path.to_string_lossy();
144 path_str.contains("/Cellar/") || path_str.contains("/homebrew/")
146}
147
148pub fn upgrade() {
150 println!("git-worktree-manager v{}", CURRENT_VERSION);
151
152 if is_homebrew_install() {
154 println!(
155 "{}",
156 style("Installed via Homebrew. Use brew to upgrade:").yellow()
157 );
158 println!(" brew upgrade git-worktree-manager");
159 return;
160 }
161
162 let latest_version = match fetch_latest_version() {
163 Some(v) => v,
164 None => {
165 println!(
166 "{}",
167 style("Could not check for updates. Check your internet connection.").red()
168 );
169 return;
170 }
171 };
172
173 if !is_newer(&latest_version, CURRENT_VERSION) {
174 println!("{}", style("Already up to date.").green());
175 return;
176 }
177
178 println!(
179 "New version available: {} → {}",
180 style(format!("v{}", CURRENT_VERSION)).dim(),
181 style(format!("v{}", latest_version)).green().bold()
182 );
183
184 if !std::io::stdin().is_terminal() {
186 println!(
187 "Download from: https://github.com/{}/{}/releases/latest",
188 REPO_OWNER, REPO_NAME
189 );
190 return;
191 }
192
193 let confirm = dialoguer::Confirm::new()
195 .with_prompt("Upgrade now?")
196 .default(true)
197 .interact()
198 .unwrap_or(false);
199
200 if !confirm {
201 println!("Upgrade cancelled.");
202 return;
203 }
204
205 println!("Downloading and installing...");
207 match self_update::backends::github::Update::configure()
208 .repo_owner(REPO_OWNER)
209 .repo_name(REPO_NAME)
210 .bin_name("gw")
211 .current_version(CURRENT_VERSION)
212 .target_version_tag(&format!("v{}", latest_version))
213 .show_download_progress(true)
214 .no_confirm(true) .build()
216 .and_then(|updater| updater.update())
217 {
218 Ok(status) => {
219 update_companion_binary();
221 println!(
222 "{}",
223 style(format!("Upgraded to v{}!", status.version()))
224 .green()
225 .bold()
226 );
227 }
228 Err(e) => {
229 println!("{}", style(format!("Upgrade failed: {}", e)).red());
230 println!(
231 "Download manually: https://github.com/{}/{}/releases/latest",
232 REPO_OWNER, REPO_NAME
233 );
234 }
235 }
236}
237
238fn update_companion_binary() {
243 let current_exe = match std::env::current_exe() {
244 Ok(p) => p,
245 Err(_) => return,
246 };
247 let bin_dir = match current_exe.parent() {
248 Some(d) => d,
249 None => return,
250 };
251
252 let bin_ext = if cfg!(windows) { ".exe" } else { "" };
253 let gw_path = bin_dir.join(format!("gw{}", bin_ext));
254 let cw_path = bin_dir.join(format!("cw{}", bin_ext));
255
256 if cw_path.exists() {
257 let _ = std::fs::copy(&gw_path, &cw_path);
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_is_newer() {
267 assert!(is_newer("0.2.0", "0.1.0"));
268 assert!(is_newer("1.0.0", "0.10.0"));
269 assert!(!is_newer("0.1.0", "0.1.0"));
270 assert!(!is_newer("0.1.0", "0.2.0"));
271 }
272
273 #[test]
274 fn test_is_homebrew_install() {
275 assert!(!is_homebrew_install());
277 }
278}