Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
* self-update command `gitui update` (or `gitui -U`) to update gitui via CLI before opening TUI [[@shuppel](https://github.com/shuppel)]
* supports updating via multiple package managers: cargo, dnf, apt, pacman, homebrew, scoop, chocolatey
* automatically detects installation method by examining binary path and querying system package managers
* `--nightly` / `-n` flag to include pre-release versions (nightly, rc, beta) in update checks
* filters stable vs pre-release versions so users can choose update stability

### Changed
* use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting
* open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805))
Expand Down
85 changes: 47 additions & 38 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::bug_report;
use anyhow::{anyhow, Context, Result};
use crate::update::self_update;
use anyhow::{Context, Result};
use asyncgit::sync::RepoPath;
use clap::{
builder::ArgPredicate, crate_authors, crate_description,
Expand Down Expand Up @@ -36,14 +37,23 @@ pub struct CliArgs {
}

pub fn process_cmdline() -> Result<CliArgs> {
let app = app();

let arg_matches = app.get_matches();
let arg_matches = app().get_matches();

if arg_matches.get_flag(BUG_REPORT_FLAG_ID) {
bug_report::generate_bugreport();
std::process::exit(0);
}

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);
std::process::exit(1);
}
std::process::exit(0);
}

if arg_matches.get_flag(LOGGING_FLAG_ID) {
let logfile = arg_matches.get_one::<String>(LOG_FILE_FLAG_ID);
setup_logging(logfile.map(PathBuf::from))?;
Expand Down Expand Up @@ -81,7 +91,7 @@ pub fn process_cmdline() -> Result<CliArgs> {
})?;
let theme = confpath.join(arg_theme);

let notify_watcher: bool =
let notify_watcher =
*arg_matches.get_one(WATCHER_FLAG_ID).unwrap_or(&false);

let key_bindings_path = arg_matches
Expand Down Expand Up @@ -118,15 +128,15 @@ fn app() -> ClapApp {
{all-args}{after-help}
",
)
.arg(
.arg(
Arg::new(KEY_BINDINGS_FLAG_ID)
.help("Use a custom keybindings file")
.short('k')
.long("key-bindings")
.value_name("KEY_LIST_FILENAME")
.num_args(1),
)
.arg(
.arg(
Arg::new(KEY_SYMBOLS_FLAG_ID)
.help("Use a custom symbols file")
.short('s')
Expand All @@ -145,19 +155,21 @@ fn app() -> ClapApp {
)
.arg(
Arg::new(LOGGING_FLAG_ID)
.help("Store logging output into a file (in the cache directory by default)")
.help("Store logging output into a file")
.short('l')
.long("logging")
.default_value_if("logfile", ArgPredicate::IsPresent, "true")
.default_value_if(LOG_FILE_FLAG_ID, ArgPredicate::IsPresent, "true")
.action(clap::ArgAction::SetTrue),
)
.arg(Arg::new(LOG_FILE_FLAG_ID)
.help("Store logging output into the specified file (implies --logging)")
.long("logfile")
.value_name("LOG_FILE"))
.arg(
Arg::new(LOG_FILE_FLAG_ID)
.help("Store logging output into the specified file")
.long("logfile")
.value_name("LOG_FILE"),
)
.arg(
Arg::new(WATCHER_FLAG_ID)
.help("Use notify-based file system watcher instead of tick-based update. This is more performant, but can cause issues on some platforms. See https://github.com/gitui-org/gitui/blob/master/FAQ.md#watcher for details.")
.help("Use notify-based file system watcher")
.long("watcher")
.action(clap::ArgAction::SetTrue),
)
Expand Down Expand Up @@ -190,49 +202,46 @@ fn app() -> ClapApp {
.env("GIT_WORK_TREE")
.num_args(1),
)
.subcommand(
ClapApp::new("update")
.about("Update gitui to the latest version")
.visible_short_flag_alias('U')
.arg(
Arg::new("nightly")
.help("Include pre-release versions")
.short('n')
.long("nightly")
.action(clap::ArgAction::SetTrue),
),
)
}

fn setup_logging(path_override: Option<PathBuf>) -> Result<()> {
let path = if let Some(path) = path_override {
path
} else {
let mut path = get_app_cache_path()?;
path.push("gitui.log");
path
};
let path = path_override.unwrap_or_else(|| {
let mut p = dirs::cache_dir().expect("cache dir");
p.push("gitui");
p.push("gitui.log");
p
});

println!("Logging enabled. Log written to: {}", path.display());

WriteLogger::init(
LevelFilter::Trace,
Config::default(),
File::create(path)?,
)?;

Ok(())
}

fn get_app_cache_path() -> Result<PathBuf> {
let mut path = dirs::cache_dir()
.ok_or_else(|| anyhow!("failed to find os cache dir."))?;

path.push("gitui");
fs::create_dir_all(&path).with_context(|| {
format!(
"failed to create cache directory: {}",
path.display()
)
})?;
Ok(path)
}

pub fn get_app_config_path() -> Result<PathBuf> {
let mut path = if cfg!(target_os = "macos") {
dirs::home_dir().map(|h| h.join(".config"))
} else {
dirs::config_dir()
}
.ok_or_else(|| anyhow!("failed to find os config dir."))?;
.ok_or_else(|| {
anyhow::anyhow!("failed to find os config dir.")
})?;

path.push("gitui");
Ok(path)
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ mod string_utils;
mod strings;
mod tabs;
mod ui;
mod update;
mod watcher;

use crate::{
Expand Down
127 changes: 127 additions & 0 deletions src/update/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! Executes update commands for supported package managers (cargo, dnf, apt,
//! etc.) using a macro to generate consistent command patterns.

use std::process::Command;

/// Generates an update function for a specific package manager.
///
/// The generated function:
/// - Executes the specified command with given arguments
/// - Checks for success or "already installed" states
/// - Returns descriptive error messages on failure
///
/// # Macro Parameters
///
/// - `$name` - Function name (e.g., `update_via_dnf`)
/// - `$cmd` - Command to execute (e.g., `"sudo"`)
/// - `$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),
)?;

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!(
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!(
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!"
);

#[cfg(not(target_os = "macos"))]
pub fn update_via_homebrew() -> Result<(), 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!"
);

#[cfg(not(target_os = "windows"))]
pub fn update_via_scoop() -> Result<(), 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!"
);

#[cfg(not(target_os = "windows"))]
pub fn update_via_scoop_bucket() -> Result<(), 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!"
);

#[cfg(not(target_os = "windows"))]
pub fn update_via_chocolatey() -> Result<(), 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())
}
Loading