diff --git a/crates/radicle-tauri/src/commands/startup.rs b/crates/radicle-tauri/src/commands/startup.rs index 832657656..73aea4ce0 100644 --- a/crates/radicle-tauri/src/commands/startup.rs +++ b/crates/radicle-tauri/src/commands/startup.rs @@ -3,6 +3,7 @@ use tauri::{AppHandle, Emitter, Manager}; use radicle::cob::cache::COBS_DB_FILE; use radicle::node::{Handle, NOTIFICATIONS_DB_FILE, Node}; +use radicle_types::binaries::GitInfo; use radicle_types::config::{Config, Version}; use radicle_types::error::Error; use radicle_types::traits::Profile; @@ -31,6 +32,11 @@ pub(crate) fn check_radicle_cli(ctx: tauri::State) -> Result<(), Error Err(Error::RadicleNotInstalled) } +#[tauri::command] +pub(crate) fn git_info() -> GitInfo { + radicle_types::binaries::git_info() +} + #[tauri::command] pub(crate) fn startup(app: AppHandle) -> Result { let profile = radicle::Profile::load()?; diff --git a/crates/radicle-tauri/src/lib.rs b/crates/radicle-tauri/src/lib.rs index 7e7e70d1d..d30fad4a4 100644 --- a/crates/radicle-tauri/src/lib.rs +++ b/crates/radicle-tauri/src/lib.rs @@ -94,6 +94,7 @@ pub fn run() { startup::startup, startup::version, startup::check_radicle_cli, + startup::git_info, thread::create_issue_comment, thread::create_patch_comment, ]) diff --git a/crates/radicle-types/bindings/config/GitInfo.ts b/crates/radicle-types/bindings/config/GitInfo.ts new file mode 100644 index 000000000..c0fe91f24 --- /dev/null +++ b/crates/radicle-types/bindings/config/GitInfo.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * The resolved git binary, for display in the settings. Both fields are unset + * when no git was found. + */ +export type GitInfo = { path?: string; version?: string }; diff --git a/crates/radicle-types/src/binaries.rs b/crates/radicle-types/src/binaries.rs index 8da01457e..a2747e961 100644 --- a/crates/radicle-types/src/binaries.rs +++ b/crates/radicle-types/src/binaries.rs @@ -12,6 +12,9 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; +use serde::Serialize; +use ts_rs::TS; + #[cfg(windows)] use std::os::windows::process::CommandExt; // See . @@ -31,15 +34,53 @@ const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); #[cfg(target_os = "macos")] const APPLE_GIT_SHIM: &str = "/usr/bin/git"; +/// The resolved git binary, for display in the settings. Both fields are unset +/// when no git was found. +#[derive(Clone, Debug, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[ts(export_to = "config/")] +pub struct GitInfo { + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub version: Option, +} + +#[derive(Clone, Debug)] +struct Git { + path: PathBuf, + version: String, +} + /// `None` means "searched and found nothing", cached so that a machine without git does not rescan /// on every diff. -static GIT: OnceLock> = OnceLock::new(); +static GIT: OnceLock> = OnceLock::new(); pub fn git() -> Option { + resolve().map(|git| git.path) +} + +pub fn git_info() -> GitInfo { + match resolve() { + Some(git) => GitInfo { + path: Some(git.path.to_string_lossy().into_owned()), + version: Some(git.version), + }, + None => GitInfo { + path: None, + version: None, + }, + } +} + +fn resolve() -> Option { GIT.get_or_init(|| { let git = search(); match &git { - Some(path) => log::info!("Using git binary {}", path.display()), + Some(git) => log::info!("Using git binary {}: {}", git.path.display(), git.version), None => log::warn!( "No working git binary found; falling back to libgit2 for diff stats, \ commit counts and file history, which is slower on large repositories" @@ -108,18 +149,18 @@ fn candidates(name: &str) -> impl Iterator { path.into_iter().chain(known) } -fn search() -> Option { +fn search() -> Option { if let Some(pinned) = std::env::var_os(GIT_ENV) { - let pinned = PathBuf::from(pinned); - match validate(&pinned) { + let path = PathBuf::from(pinned); + match validate(&path) { Some(version) => { - log::info!("{GIT_ENV} pins git to {}: {version}", pinned.display()); + log::info!("{GIT_ENV} pins git to {}", path.display()); - return Some(pinned); + return Some(Git { path, version }); } None => log::warn!( "{GIT_ENV} is set to {}, which does not run git; detecting instead", - pinned.display() + path.display() ), } } @@ -131,7 +172,7 @@ fn search() -> Option { /// `Git\cmd\git.exe` is the wrapper Git for Windows means for others to call, /// as opposed to the bare executable under `Git\mingw64\bin`. #[cfg(windows)] -fn detect() -> Option { +fn detect() -> Option { const PROGRAM_FILES: &[&str] = &["ProgramW6432", "ProgramFiles(x86)", "ProgramFiles"]; let program_files = PROGRAM_FILES @@ -143,25 +184,25 @@ fn detect() -> Option { candidates("git.exe") .chain(program_files) .chain(local) - .find(|candidate| validate(candidate).is_some()) + .find_map(|path| validate(&path).map(|version| Git { path, version })) } #[cfg(not(windows))] -fn detect() -> Option { - for candidate in candidates("git") { +fn detect() -> Option { + for path in candidates("git") { // Skipping the shim has to leave a git further down the list still // reachable, hence a list of candidates rather than just the first // `git` on `PATH`. #[cfg(target_os = "macos")] - if candidate == Path::new(APPLE_GIT_SHIM) && !command_line_tools_installed() { + if path == Path::new(APPLE_GIT_SHIM) && !command_line_tools_installed() { log::info!( "Skipping {APPLE_GIT_SHIM}: the Command Line Developer Tools are not installed, \ so running it would only prompt to install them" ); continue; } - if validate(&candidate).is_some() { - return Some(candidate); + if let Some(version) = validate(&path) { + return Some(Git { path, version }); } } diff --git a/crates/test-http-api/src/api.rs b/crates/test-http-api/src/api.rs index 540f4d5ec..283990a45 100644 --- a/crates/test-http-api/src/api.rs +++ b/crates/test-http-api/src/api.rs @@ -86,6 +86,7 @@ pub fn router(ctx: Context) -> Router { .route("/repo_by_id", post(repo_handler)) .route("/list_repo_refs", post(list_repo_refs_handler)) .route("/version", post(version_handler)) + .route("/git_info", post(git_info_handler)) .route("/diff_stats", post(diff_stats_handler)) .route( "/activity_by_issue", @@ -229,6 +230,10 @@ async fn list_repo_refs_handler( Ok::<_, Error>(Json(refs)) } +async fn git_info_handler() -> impl IntoResponse { + Ok::<_, Error>(Json(types::binaries::git_info())) +} + async fn version_handler() -> impl IntoResponse { let version = Version { version: String::from("0.6.1"), diff --git a/src/modals/Settings.svelte b/src/modals/Settings.svelte index 09c79498a..a6d535576 100644 --- a/src/modals/Settings.svelte +++ b/src/modals/Settings.svelte @@ -1,9 +1,11 @@