Show the detected git binary in the settings

This commit is contained in:
Rūdolfs Ošiņš
2026-09-08 10:04:49 +02:00
parent f6050ecae9
commit 535f5e2a18
6 changed files with 102 additions and 15 deletions
@@ -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<AppState>) -> 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<Config, Error> {
let profile = radicle::Profile::load()?;
+1
View File
@@ -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,
])
@@ -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 };
+56 -15
View File
@@ -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 <https://learn.microsoft.com/windows/win32/procthread/process-creation-flags#flags>.
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub version: Option<String>,
}
#[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<Option<PathBuf>> = OnceLock::new();
static GIT: OnceLock<Option<Git>> = OnceLock::new();
pub fn git() -> Option<PathBuf> {
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> {
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<Item = PathBuf> {
path.into_iter().chain(known)
}
fn search() -> Option<PathBuf> {
fn search() -> Option<Git> {
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<PathBuf> {
/// `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<PathBuf> {
fn detect() -> Option<Git> {
const PROGRAM_FILES: &[&str] = &["ProgramW6432", "ProgramFiles(x86)", "ProgramFiles"];
let program_files = PROGRAM_FILES
@@ -143,25 +184,25 @@ fn detect() -> Option<PathBuf> {
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<PathBuf> {
for candidate in candidates("git") {
fn detect() -> Option<Git> {
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 });
}
}
+5
View File
@@ -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"),
+27
View File
@@ -1,9 +1,11 @@
<script lang="ts">
import type { GitInfo } from "@bindings/config/GitInfo";
import type { ComponentProps } from "svelte";
import type { DiffOptions } from "@app/lib/diffOptions.svelte";
import { diffOptions } from "@app/lib/diffOptions.svelte";
import { hints } from "@app/lib/hints";
import { invoke } from "@app/lib/invoke";
import { hide } from "@app/lib/modal";
import { updateChecker } from "@app/lib/updateChecker.svelte";
import { pluralize } from "@app/lib/utils";
@@ -28,6 +30,10 @@
title?: string;
};
// Shown so that a user seeing the slower fallback can tell which git, if
// any, was picked.
const gitInfo = invoke<GitInfo>("git_info");
const diffStyleOptions: Option<DiffOptions["diffStyle"]>[] = [
{ value: "unified", icon: "diff-unified", title: "Unified" },
{ value: "split", icon: "diff-split", title: "Split" },
@@ -109,6 +115,9 @@
.section {
border-top: 1px solid var(--color-border-subtle);
}
.git {
overflow-wrap: anywhere;
}
.footer {
padding: 4rem 1.5rem 1.5rem;
font: var(--txt-body-m-regular);
@@ -186,6 +195,24 @@
Reset
</Button>
</div>
<div class="row">
<div class="row-label">
<span class="row-title">Git binary</span>
{#await gitInfo}
<span class="row-description">Looking for git…</span>
{:then info}
<span class="row-description git txt-selectable">
{info?.path
? `${info.version ?? "git"} at ${info.path}`
: "None found, using a slower built-in fallback"}
</span>
{:catch}
<span class="row-description">
Could not tell which git is in use
</span>
{/await}
</div>
</div>
</div>
<!-- How every diff in the app is drawn — the commit view, a patch's changes
and a review all read these. Kept apart from the rest because they are