mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Show the detected git binary in the settings
This commit is contained in:
@@ -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()?;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user