use std::io::IsTerminal;
use std::path::PathBuf;
use std::process::Command;
use console::style;
use serde::{Deserialize, Serialize};
use crate::constants::home_dir_or_fallback;
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
const REPO_OWNER: &str = "DaveDev42";
const REPO_NAME: &str = "git-worktree-manager";
#[derive(Debug, Serialize, Deserialize, Default)]
struct UpdateCache {
last_check: String,
latest_version: Option<String>,
}
fn get_cache_path() -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(home_dir_or_fallback)
.join("git-worktree-manager")
.join("update_check.json")
}
fn load_cache() -> UpdateCache {
let path = get_cache_path();
if !path.exists() {
return UpdateCache::default();
}
std::fs::read_to_string(&path)
.ok()
.and_then(|c| serde_json::from_str(&c).ok())
.unwrap_or_default()
}
fn save_cache(cache: &UpdateCache) {
let path = get_cache_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(content) = serde_json::to_string_pretty(cache) {
let _ = std::fs::write(&path, content);
}
}
fn today_str() -> String {
crate::session::chrono_now_iso_pub()
.split('T')
.next()
.unwrap_or("")
.to_string()
}
fn should_check() -> bool {
let config = crate::config::load_config().unwrap_or_default();
if !config.update.auto_check {
return false;
}
let cache = load_cache();
cache.last_check != today_str()
}
pub fn check_for_update_if_needed() {
if !should_check() {
return;
}
if let Some(latest) = fetch_latest_version() {
let cache = UpdateCache {
last_check: today_str(),
latest_version: Some(latest.clone()),
};
save_cache(&cache);
if is_newer(&latest, CURRENT_VERSION) {
eprintln!(
"\ngit-worktree-manager {} is available (current: {})",
latest, CURRENT_VERSION
);
eprintln!("Run 'gw upgrade' to update.\n");
}
} else {
let cache = UpdateCache {
last_check: today_str(),
latest_version: None,
};
save_cache(&cache);
}
}
fn fetch_latest_version() -> Option<String> {
let output = Command::new("curl")
.args([
"-s",
"-H",
"Accept: application/vnd.github+json",
&format!(
"https://api.github.com/repos/{}/{}/releases/latest",
REPO_OWNER, REPO_NAME
),
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let body = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&body).ok()?;
let tag = json.get("tag_name")?.as_str()?;
Some(tag.strip_prefix('v').unwrap_or(tag).to_string())
}
fn is_newer(latest: &str, current: &str) -> bool {
let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
let l = parse(latest);
let c = parse(current);
l > c
}
fn is_homebrew_install() -> bool {
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return false,
};
let real_path = match std::fs::canonicalize(&exe) {
Ok(p) => p,
Err(_) => exe,
};
let path_str = real_path.to_string_lossy();
path_str.contains("/Cellar/") || path_str.contains("/homebrew/")
}
pub fn upgrade() {
println!("git-worktree-manager v{}", CURRENT_VERSION);
if is_homebrew_install() {
println!(
"{}",
style("Installed via Homebrew. Use brew to upgrade:").yellow()
);
println!(" brew upgrade git-worktree-manager");
return;
}
let latest_version = match fetch_latest_version() {
Some(v) => v,
None => {
println!(
"{}",
style("Could not check for updates. Check your internet connection.").red()
);
return;
}
};
if !is_newer(&latest_version, CURRENT_VERSION) {
println!("{}", style("Already up to date.").green());
return;
}
println!(
"New version available: {} → {}",
style(format!("v{}", CURRENT_VERSION)).dim(),
style(format!("v{}", latest_version)).green().bold()
);
if !std::io::stdin().is_terminal() {
println!(
"Download from: https://github.com/{}/{}/releases/latest",
REPO_OWNER, REPO_NAME
);
return;
}
let confirm = dialoguer::Confirm::new()
.with_prompt("Upgrade now?")
.default(true)
.interact()
.unwrap_or(false);
if !confirm {
println!("Upgrade cancelled.");
return;
}
println!("Downloading and installing...");
match self_update::backends::github::Update::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name("gw")
.current_version(CURRENT_VERSION)
.target_version_tag(&format!("v{}", latest_version))
.show_download_progress(true)
.no_confirm(true) .build()
.and_then(|updater| updater.update())
{
Ok(status) => {
update_companion_binary();
println!(
"{}",
style(format!("Upgraded to v{}!", status.version()))
.green()
.bold()
);
}
Err(e) => {
println!("{}", style(format!("Upgrade failed: {}", e)).red());
println!(
"Download manually: https://github.com/{}/{}/releases/latest",
REPO_OWNER, REPO_NAME
);
}
}
}
fn update_companion_binary() {
let current_exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
let bin_dir = match current_exe.parent() {
Some(d) => d,
None => return,
};
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let gw_path = bin_dir.join(format!("gw{}", bin_ext));
let cw_path = bin_dir.join(format!("cw{}", bin_ext));
if cw_path.exists() {
let _ = std::fs::copy(&gw_path, &cw_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_newer() {
assert!(is_newer("0.2.0", "0.1.0"));
assert!(is_newer("1.0.0", "0.10.0"));
assert!(!is_newer("0.1.0", "0.1.0"));
assert!(!is_newer("0.1.0", "0.2.0"));
}
#[test]
fn test_is_homebrew_install() {
assert!(!is_homebrew_install());
}
}