diff --git a/src/args.rs b/src/args.rs index 053b0010..dd7c17af 100644 --- a/src/args.rs +++ b/src/args.rs @@ -44,7 +44,8 @@ pub fn process_cmdline() -> Result { std::process::exit(0); } - if let Some(update_cmd) = arg_matches.subcommand_matches("update") { + if let Some(update_cmd) = arg_matches.subcommand_matches("update") + { let include_prerelease = update_cmd.get_flag("nightly"); if let Err(e) = self_update(include_prerelease) { eprintln!("Update failed: {}", e); @@ -61,9 +62,11 @@ pub fn process_cmdline() -> Result { let workdir = arg_matches .get_one::(WORKDIR_FLAG_ID) .map(PathBuf::from); - let gitdir = arg_matches - .get_one::(GIT_DIR_FLAG_ID) - .map_or_else(|| PathBuf::from(DEFAULT_GIT_DIR), PathBuf::from); + let gitdir = + arg_matches.get_one::(GIT_DIR_FLAG_ID).map_or_else( + || PathBuf::from(DEFAULT_GIT_DIR), + PathBuf::from, + ); let select_file = arg_matches .get_one::(FILE_FLAG_ID) @@ -81,11 +84,15 @@ pub fn process_cmdline() -> Result { let confpath = get_app_config_path()?; fs::create_dir_all(&confpath).with_context(|| { - format!("failed to create config directory: {}", confpath.display()) + format!( + "failed to create config directory: {}", + confpath.display() + ) })?; let theme = confpath.join(arg_theme); - let notify_watcher = *arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false); + let notify_watcher = + *arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false); let key_bindings_path = arg_matches .get_one::(KEY_BINDINGS_FLAG_ID) @@ -218,7 +225,11 @@ fn setup_logging(path_override: Option) -> Result<()> { }); println!("Logging enabled. Log written to: {}", path.display()); - WriteLogger::init(LevelFilter::Trace, Config::default(), File::create(path)?)?; + WriteLogger::init( + LevelFilter::Trace, + Config::default(), + File::create(path)?, + )?; Ok(()) } @@ -228,7 +239,9 @@ pub fn get_app_config_path() -> Result { } else { dirs::config_dir() } - .ok_or_else(|| anyhow::anyhow!("failed to find os config dir."))?; + .ok_or_else(|| { + anyhow::anyhow!("failed to find os config dir.") + })?; path.push("gitui"); Ok(path) diff --git a/src/main.rs b/src/main.rs index 3e6ec147..2656936e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,6 @@ mod app; mod args; mod bug_report; -mod update; mod clipboard; mod cmdbar; mod components; @@ -79,6 +78,7 @@ mod string_utils; mod strings; mod tabs; mod ui; +mod update; mod watcher; use crate::{ diff --git a/src/update/commands.rs b/src/update/commands.rs index fdcf49eb..e0ad2e15 100644 --- a/src/update/commands.rs +++ b/src/update/commands.rs @@ -17,109 +17,111 @@ use std::process::Command; /// - `$args` - Arguments array (e.g., `["dnf", "upgrade", "gitui", "-y"]`) /// - `$success_msg` - Message printed on successful update macro_rules! update_via { - ($name:ident, $cmd:expr, $args:expr, $success_msg:literal) => { - pub fn $name() -> Result<(), String> { - let output = Command::new($cmd) - .args($args) - .output() - .map_err(|e| format!("Failed to run {}: {}", $cmd, e))?; + ($name:ident, $cmd:expr, $args:expr, $success_msg:literal) => { + pub fn $name() -> Result<(), String> { + let output = + Command::new($cmd).args($args).output().map_err( + |e| format!("Failed to run {}: {}", $cmd, e), + )?; - if output.status.success() { - println!($success_msg); - Ok(()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - if stderr.contains("already installed") || stderr.contains("already up-to-date") { - println!("Already up to date!"); - Ok(()) - } else { - Err(format!("{} failed:\n{}", $cmd, stderr)) - } - } - } - }; + if output.status.success() { + println!($success_msg); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("already installed") + || stderr.contains("already up-to-date") + { + println!("Already up to date!"); + Ok(()) + } else { + Err(format!("{} failed:\n{}", $cmd, stderr)) + } + } + } + }; } update_via!( - update_via_cargo, - "cargo", - ["install", "gitui", "--force"], - "Successfully updated via cargo!" + update_via_cargo, + "cargo", + ["install", "gitui", "--force"], + "Successfully updated via cargo!" ); update_via!( - update_via_dnf, - "sudo", - ["dnf", "upgrade", "gitui", "-y"], - "Successfully updated via dnf!" + update_via_dnf, + "sudo", + ["dnf", "upgrade", "gitui", "-y"], + "Successfully updated via dnf!" ); update_via!( - update_via_apt, - "sudo", - ["apt", "upgrade", "gitui", "-y"], - "Successfully updated via apt!" + update_via_apt, + "sudo", + ["apt", "upgrade", "gitui", "-y"], + "Successfully updated via apt!" ); update_via!( - update_via_pacman, - "sudo", - ["pacman", "-Syu", "gitui", "--noconfirm"], - "Successfully updated via pacman!" + update_via_pacman, + "sudo", + ["pacman", "-Syu", "gitui", "--noconfirm"], + "Successfully updated via pacman!" ); #[cfg(target_os = "macos")] update_via!( - update_via_homebrew, - "brew", - ["upgrade", "gitui"], - "Successfully updated via homebrew!" + update_via_homebrew, + "brew", + ["upgrade", "gitui"], + "Successfully updated via homebrew!" ); #[cfg(not(target_os = "macos"))] pub fn update_via_homebrew() -> Result<(), String> { - Err("Homebrew is only supported on macOS".to_string()) + Err("Homebrew is only supported on macOS".to_string()) } #[cfg(target_os = "windows")] update_via!( - update_via_scoop, - "scoop", - ["update", "gitui"], - "Successfully updated via scoop!" + update_via_scoop, + "scoop", + ["update", "gitui"], + "Successfully updated via scoop!" ); #[cfg(not(target_os = "windows"))] pub fn update_via_scoop() -> Result<(), String> { - Err("Scoop is only supported on Windows".to_string()) + Err("Scoop is only supported on Windows".to_string()) } #[cfg(target_os = "windows")] update_via!( - update_via_scoop_bucket, - "scoop", - ["update", "gitui"], - "Successfully updated via scoop bucket!" + update_via_scoop_bucket, + "scoop", + ["update", "gitui"], + "Successfully updated via scoop bucket!" ); #[cfg(not(target_os = "windows"))] pub fn update_via_scoop_bucket() -> Result<(), String> { - Err("Scoop is only supported on Windows".to_string()) + Err("Scoop is only supported on Windows".to_string()) } #[cfg(target_os = "windows")] update_via!( - update_via_chocolatey, - "choco", - ["upgrade", "gitui", "-y"], - "Successfully updated via chocolatey!" + update_via_chocolatey, + "choco", + ["upgrade", "gitui", "-y"], + "Successfully updated via chocolatey!" ); #[cfg(not(target_os = "windows"))] pub fn update_via_chocolatey() -> Result<(), String> { - Err("Chocolatey is only supported on Windows".to_string()) + Err("Chocolatey is only supported on Windows".to_string()) } pub fn update_via_windows() -> Result<(), String> { - Err("Windows binary update not supported. Please download the latest release from GitHub.".to_string()) + Err("Windows binary update not supported. Please download the latest release from GitHub.".to_string()) } diff --git a/src/update/detector.rs b/src/update/detector.rs index dc2155a2..20727e7f 100644 --- a/src/update/detector.rs +++ b/src/update/detector.rs @@ -7,140 +7,143 @@ use std::process::Command; /// Installation methods supported by the self-update system. #[derive(Debug, Clone, PartialEq)] pub enum InstallMethod { - Cargo, - Homebrew, - Apt, - Dnf, - Pacman, - Windows, - Scoop, - Chocolatey, - ScoopBucket, - Unknown, + Cargo, + Homebrew, + Apt, + Dnf, + Pacman, + Windows, + Scoop, + Chocolatey, + ScoopBucket, + Unknown, } impl std::fmt::Display for InstallMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InstallMethod::Cargo => write!(f, "cargo"), - InstallMethod::Homebrew => write!(f, "homebrew"), - InstallMethod::Apt => write!(f, "apt"), - InstallMethod::Dnf => write!(f, "dnf"), - InstallMethod::Pacman => write!(f, "pacman"), - InstallMethod::Windows => write!(f, "windows"), - InstallMethod::Scoop => write!(f, "scoop"), - InstallMethod::Chocolatey => write!(f, "chocolatey"), - InstallMethod::ScoopBucket => write!(f, "scoop-bucket"), - InstallMethod::Unknown => write!(f, "unknown"), - } - } + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + match self { + InstallMethod::Cargo => write!(f, "cargo"), + InstallMethod::Homebrew => write!(f, "homebrew"), + InstallMethod::Apt => write!(f, "apt"), + InstallMethod::Dnf => write!(f, "dnf"), + InstallMethod::Pacman => write!(f, "pacman"), + InstallMethod::Windows => write!(f, "windows"), + InstallMethod::Scoop => write!(f, "scoop"), + InstallMethod::Chocolatey => write!(f, "chocolatey"), + InstallMethod::ScoopBucket => write!(f, "scoop-bucket"), + InstallMethod::Unknown => write!(f, "unknown"), + } + } } pub fn detect_install_method() -> InstallMethod { - let current_exe = std::env::current_exe().ok(); - let exe_path = current_exe.as_ref().map(|p| p.as_path()); + let current_exe = std::env::current_exe().ok(); + let exe_path = current_exe.as_ref().map(|p| p.as_path()); - let is_cargo_build = exe_path.map_or(false, |p| { - let s = p.to_string_lossy(); - s.contains(".cargo/bin") - || s.contains("cargo/registry") - || s.contains("target/release") - || s.contains("target/debug") - }); + let is_cargo_build = exe_path.map_or(false, |p| { + let s = p.to_string_lossy(); + s.contains(".cargo/bin") + || s.contains("cargo/registry") + || s.contains("target/release") + || s.contains("target/debug") + }); - if is_cargo_build { - if has_dnf_installation() { - return InstallMethod::Dnf; - } - if has_apt_installation() { - return InstallMethod::Apt; - } - if has_pacman_installation() { - return InstallMethod::Pacman; - } - return InstallMethod::Cargo; - } + if is_cargo_build { + if has_dnf_installation() { + return InstallMethod::Dnf; + } + if has_apt_installation() { + return InstallMethod::Apt; + } + if has_pacman_installation() { + return InstallMethod::Pacman; + } + return InstallMethod::Cargo; + } - exe_path.map_or(InstallMethod::Unknown, |p| { - let s = p.to_string_lossy(); + exe_path.map_or(InstallMethod::Unknown, |p| { + let s = p.to_string_lossy(); - if s.contains("homebrew") || s.contains("Cellar") { - return InstallMethod::Homebrew; - } + if s.contains("homebrew") || s.contains("Cellar") { + return InstallMethod::Homebrew; + } - if s.contains("scoop") { - return if s.contains("scoop-bucket") { - InstallMethod::ScoopBucket - } else { - InstallMethod::Scoop - }; - } + if s.contains("scoop") { + return if s.contains("scoop-bucket") { + InstallMethod::ScoopBucket + } else { + InstallMethod::Scoop + }; + } - if s.contains("chocolatey") { - return InstallMethod::Chocolatey; - } + if s.contains("chocolatey") { + return InstallMethod::Chocolatey; + } - if cfg!(target_os = "windows") { - return InstallMethod::Windows; - } + if cfg!(target_os = "windows") { + return InstallMethod::Windows; + } - if s.contains("/usr/bin") || s.contains("/usr/local/bin") { - if has_dnf_installation() { - return InstallMethod::Dnf; - } - if has_apt_installation() { - return InstallMethod::Apt; - } - if has_pacman_installation() { - return InstallMethod::Pacman; - } - } + if s.contains("/usr/bin") || s.contains("/usr/local/bin") { + if has_dnf_installation() { + return InstallMethod::Dnf; + } + if has_apt_installation() { + return InstallMethod::Apt; + } + if has_pacman_installation() { + return InstallMethod::Pacman; + } + } - InstallMethod::Unknown - }) + InstallMethod::Unknown + }) } #[cfg(target_os = "linux")] fn has_dnf_installation() -> bool { - Path::new("/usr/bin/rpm").exists() - && Command::new("rpm") - .args(["-q", "gitui"]) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + Path::new("/usr/bin/rpm").exists() + && Command::new("rpm") + .args(["-q", "gitui"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } #[cfg(not(target_os = "linux"))] fn has_dnf_installation() -> bool { - false + false } #[cfg(target_os = "linux")] fn has_apt_installation() -> bool { - Path::new("/usr/bin/dpkg").exists() - && Command::new("dpkg") - .args(["-l", "gitui"]) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + Path::new("/usr/bin/dpkg").exists() + && Command::new("dpkg") + .args(["-l", "gitui"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } #[cfg(not(target_os = "linux"))] fn has_apt_installation() -> bool { - false + false } #[cfg(target_os = "linux")] fn has_pacman_installation() -> bool { - Path::new("/usr/bin/pacman").exists() - && Command::new("pacman") - .args(["-Q", "gitui"]) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + Path::new("/usr/bin/pacman").exists() + && Command::new("pacman") + .args(["-Q", "gitui"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } #[cfg(not(target_os = "linux"))] fn has_pacman_installation() -> bool { - false + false } diff --git a/src/update/mod.rs b/src/update/mod.rs index a64f1835..b217d928 100644 --- a/src/update/mod.rs +++ b/src/update/mod.rs @@ -11,130 +11,160 @@ use std::io::{self, Write}; use std::process::Command; pub fn self_update(include_prerelease: bool) -> Result<()> { - let current = get_current_version(); - let method = detect_install_method(); + let current = get_current_version(); + let method = detect_install_method(); - println!("gitui version: {}", current); + println!("gitui version: {}", current); - if is_prerelease(¤t) { - println!("⚠️ Pre-release version detected."); - if !include_prerelease { - println!(" Use 'gitui update -n' to include pre-releases."); - } - } + if is_prerelease(¤t) { + println!("⚠️ Pre-release version detected."); + if !include_prerelease { + println!( + " Use 'gitui update -n' to include pre-releases." + ); + } + } - println!("Installation method: {}", method); - println!("Checking for updates..."); + println!("Installation method: {}", method); + println!("Checking for updates..."); - let latest = if include_prerelease { - fetch_latest_version() - } else { - fetch_latest_stable() - }; + let latest = if include_prerelease { + fetch_latest_version() + } else { + fetch_latest_stable() + }; - match latest { - Some(v) if v == current => { - println!("Already up to date ({})", current); - return Ok(()); - } - Some(v) => { - let kind = if is_prerelease(&v) { "Pre-release" } else { "Stable" }; - println!("{} update available: {} -> {}", kind, current, v); - } - None => println!("Could not determine latest version."), - } + match latest { + Some(v) if v == current => { + println!("Already up to date ({})", current); + return Ok(()); + } + Some(v) => { + let kind = if is_prerelease(&v) { + "Pre-release" + } else { + "Stable" + }; + println!( + "{} update available: {} -> {}", + kind, current, v + ); + } + None => println!("Could not determine latest version."), + } - if !confirm("Do you want to update gitui?")? { - println!("Update cancelled."); - return Ok(()); - } + if !confirm("Do you want to update gitui?")? { + println!("Update cancelled."); + return Ok(()); + } - println!("Updating via {}...", method); + println!("Updating via {}...", method); - let result = match method { - InstallMethod::Cargo => update_via_cargo(), - InstallMethod::Homebrew => update_via_homebrew(), - InstallMethod::Dnf => update_via_dnf(), - InstallMethod::Apt => update_via_apt(), - InstallMethod::Pacman => update_via_pacman(), - InstallMethod::Scoop => update_via_scoop(), - InstallMethod::Chocolatey => update_via_chocolatey(), - InstallMethod::ScoopBucket => update_via_scoop_bucket(), - InstallMethod::Windows => update_via_windows(), - InstallMethod::Unknown => Err("Unknown installation method".to_string()), - }; + let result = match method { + InstallMethod::Cargo => update_via_cargo(), + InstallMethod::Homebrew => update_via_homebrew(), + InstallMethod::Dnf => update_via_dnf(), + InstallMethod::Apt => update_via_apt(), + InstallMethod::Pacman => update_via_pacman(), + InstallMethod::Scoop => update_via_scoop(), + InstallMethod::Chocolatey => update_via_chocolatey(), + InstallMethod::ScoopBucket => update_via_scoop_bucket(), + InstallMethod::Windows => update_via_windows(), + InstallMethod::Unknown => { + Err("Unknown installation method".to_string()) + } + }; - match result { - Ok(_) => { - println!("Update complete! Please restart gitui."); - Ok(()) - } - Err(e) => Err(anyhow!("Update failed: {}", e)), - } + match result { + Ok(_) => { + println!("Update complete! Please restart gitui."); + Ok(()) + } + Err(e) => Err(anyhow!("Update failed: {}", e)), + } } fn get_current_version() -> String { - let build = env!("GITUI_BUILD_NAME"); - build.split_whitespace().next().unwrap_or(build).to_string() + let build = env!("GITUI_BUILD_NAME"); + build.split_whitespace().next().unwrap_or(build).to_string() } fn is_prerelease(v: &str) -> bool { - let lower = v.to_lowercase(); - ["nightly", "-rc", "-beta", "-alpha", "-dev", "preview", "snapshot"] - .iter() - .any(|&s| lower.contains(s)) + let lower = v.to_lowercase(); + [ + "nightly", "-rc", "-beta", "-alpha", "-dev", "preview", + "snapshot", + ] + .iter() + .any(|&s| lower.contains(s)) } fn fetch_latest_version() -> Option { - let output = Command::new("git") - .args(["ls-remote", "--tags", "--sort=-v:refname", "https://github.com/extrawurst/gitui.git"]) - .output() - .ok()?; + let output = Command::new("git") + .args([ + "ls-remote", + "--tags", + "--sort=-v:refname", + "https://github.com/extrawurst/gitui.git", + ]) + .output() + .ok()?; - if !output.status.success() { - return None; - } + if !output.status.success() { + return None; + } - String::from_utf8_lossy(&output.stdout) - .lines() - .filter_map(|line| { - line.split('\t').nth(1)?.strip_prefix("refs/tags/")?.strip_prefix('v') - }) - .next() - .map(String::from) + String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(|line| { + line.split('\t') + .nth(1)? + .strip_prefix("refs/tags/")? + .strip_prefix('v') + }) + .next() + .map(String::from) } fn fetch_latest_stable() -> Option { - let output = Command::new("git") - .args(["ls-remote", "--tags", "--sort=-v:refname", "https://github.com/extrawurst/gitui.git"]) - .output() - .ok()?; + let output = Command::new("git") + .args([ + "ls-remote", + "--tags", + "--sort=-v:refname", + "https://github.com/extrawurst/gitui.git", + ]) + .output() + .ok()?; - if !output.status.success() { - return None; - } + if !output.status.success() { + return None; + } - let version = String::from_utf8_lossy(&output.stdout) - .lines() - .filter_map(|line| { - line.split('\t').nth(1)?.strip_prefix("refs/tags/")?.strip_prefix('v') - }) - .find(|&v| !is_prerelease(v)) - .map(String::from); + let version = String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(|line| { + line.split('\t') + .nth(1)? + .strip_prefix("refs/tags/")? + .strip_prefix('v') + }) + .find(|&v| !is_prerelease(v)) + .map(String::from); - if version.is_none() { - println!("Warning: No stable release found. Use -n for pre-releases."); - } + if version.is_none() { + println!("Warning: No stable release found. Use -n for pre-releases."); + } - version + version } fn confirm(prompt: &str) -> Result { - print!("{} [y/N]: ", prompt); - io::stdout().flush()?; + print!("{} [y/N]: ", prompt); + io::stdout().flush()?; - let mut input = String::new(); - io::stdin().read_line(&mut input)?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; - Ok(input.trim().eq_ignore_ascii_case("y")) + Ok(input.trim().eq_ignore_ascii_case("y")) }