Add live git status indicators (#458)

This commit is contained in:
Gregory Schier
2026-05-08 11:25:39 -07:00
committed by GitHub
parent 1b154ba550
commit d7e67cf13c
35 changed files with 1702 additions and 578 deletions

View File

@@ -30,6 +30,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
http = { version = "1.2.0", default-features = false }
log = { workspace = true }
md5 = "0.8.0"
notify = "8.0.0"
pretty_graphql = "0.2"
r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0"

View File

@@ -12,6 +12,8 @@ export type UpdateResponseAction = "install" | "skip";
export type WatchResult = { unlistenEvent: string, };
export type GitWatchResult = { unlistenEvent: string, };
export type YaakNotification = { timestamp: string, timeout: number | null, id: string, title: string | null, message: string, color: string | null, action: YaakNotificationAction | null, };
export type YaakNotificationAction = { label: string, url: string, };

View File

@@ -3,14 +3,18 @@
//! This module provides the Tauri commands for git functionality.
use crate::error::Result;
use crate::git_watcher::{GitWatchResult, watch_git_worktree_status};
use std::path::{Path, PathBuf};
use tauri::command;
use tauri::ipc::Channel;
use tauri::{AppHandle, Runtime, command};
use yaak_git::{
BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult,
PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone,
git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all,
git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push,
git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, git_unstage,
BranchDeleteResult, CloneResult, GitBranchInfo, GitCommit, GitFileDiff, GitRemote,
GitStatusSummary, GitWorktreeStatus, PullResult, PushResult, git_add, git_add_credential,
git_add_remote, git_branch_info, git_checkout_branch, git_clone, git_commit, git_create_branch,
git_delete_branch, git_delete_remote_branch, git_fetch_all, git_file_diff_for_commit, git_init,
git_log, git_log_for_file, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge,
git_push, git_remotes, git_rename_branch, git_reset_changes, git_restore,
git_restore_file_from_commit, git_rm_remote, git_status, git_unstage, git_worktree_status,
};
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
@@ -54,11 +58,44 @@ pub async fn cmd_git_status(dir: &Path) -> Result<GitStatusSummary> {
Ok(git_status(dir)?)
}
#[command]
pub async fn cmd_git_branch_info(dir: &Path) -> Result<GitBranchInfo> {
Ok(git_branch_info(dir)?)
}
#[command]
pub async fn cmd_git_worktree_status(dir: &Path) -> Result<GitWorktreeStatus> {
Ok(git_worktree_status(dir)?)
}
#[command]
pub async fn cmd_git_watch_worktree_status<R: Runtime>(
app_handle: AppHandle<R>,
dir: &Path,
channel: Channel<GitWorktreeStatus>,
) -> Result<GitWatchResult> {
watch_git_worktree_status(app_handle, dir, channel).await
}
#[command]
pub async fn cmd_git_log(dir: &Path) -> Result<Vec<GitCommit>> {
Ok(git_log(dir)?)
}
#[command]
pub async fn cmd_git_log_for_file(dir: &Path, rela_path: PathBuf) -> Result<Vec<GitCommit>> {
Ok(git_log_for_file(dir, &rela_path)?)
}
#[command]
pub async fn cmd_git_file_diff_for_commit(
dir: &Path,
commit_oid: &str,
rela_path: PathBuf,
) -> Result<GitFileDiff> {
Ok(git_file_diff_for_commit(dir, commit_oid, &rela_path)?)
}
#[command]
pub async fn cmd_git_initialize(dir: &Path) -> Result<()> {
Ok(git_init(dir)?)
@@ -124,6 +161,23 @@ pub async fn cmd_git_reset_changes(dir: &Path) -> Result<()> {
Ok(git_reset_changes(dir).await?)
}
#[command]
pub async fn cmd_git_restore_files(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
for path in rela_paths {
git_restore(dir, &path)?;
}
Ok(())
}
#[command]
pub async fn cmd_git_restore_file_from_commit(
dir: &Path,
commit_oid: &str,
rela_path: PathBuf,
) -> Result<()> {
Ok(git_restore_file_from_commit(dir, commit_oid, &rela_path)?)
}
#[command]
pub async fn cmd_git_add_credential(
remote_url: &str,

View File

@@ -0,0 +1,172 @@
use crate::error::{Error, Result};
use chrono::Utc;
use log::{debug, error, warn};
use notify::Watcher;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
use tauri::ipc::Channel;
use tauri::{AppHandle, Listener, Runtime};
use tokio::select;
use tokio::sync::watch;
use tokio::time::sleep;
use ts_rs::TS;
use yaak_git::{GitWorktreeStatus, git_path_is_ignored, git_repository_paths, git_worktree_status};
const GIT_STATUS_COALESCE_WINDOW: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "index.ts")]
pub(crate) struct GitWatchResult {
unlisten_event: String,
}
pub(crate) async fn watch_git_worktree_status<R: Runtime>(
app_handle: AppHandle<R>,
dir: &Path,
channel: Channel<GitWorktreeStatus>,
) -> Result<GitWatchResult> {
let paths = git_repository_paths(dir)?;
let repo_dir = dir.to_path_buf();
let workdir = paths.workdir;
let gitdir = paths.gitdir;
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
let mut watcher = notify::recommended_watcher(tx)
.map_err(|e| Error::GenericError(format!("Failed to watch Git repository: {e}")))?;
watcher
.watch(&workdir, notify::RecursiveMode::Recursive)
.map_err(|e| Error::GenericError(format!("Failed to watch Git worktree: {e}")))?;
if gitdir != workdir {
watcher
.watch(&gitdir, notify::RecursiveMode::Recursive)
.map_err(|e| Error::GenericError(format!("Failed to watch Git metadata: {e}")))?;
}
let (async_tx, mut async_rx) = tokio::sync::mpsc::channel::<notify::Result<notify::Event>>(100);
std::thread::spawn(move || {
for res in rx {
if async_tx.blocking_send(res).is_err() {
break;
}
}
});
let (cancel_tx, cancel_rx) = watch::channel(());
let mut cancel_rx = cancel_rx;
send_worktree_status(&repo_dir, &channel);
tauri::async_runtime::spawn(async move {
let _watcher = watcher;
loop {
select! {
Some(event_res) = async_rx.recv() => {
handle_git_watch_event(
event_res,
&mut async_rx,
&repo_dir,
&workdir,
&gitdir,
&channel,
).await;
}
_ = cancel_rx.changed() => {
break;
}
}
}
});
let app_handle_inner = app_handle.clone();
let unlisten_event = format!("git-watch-unlisten-{}", Utc::now().timestamp_millis());
app_handle.listen_any(unlisten_event.clone(), move |event| {
app_handle_inner.unlisten(event.id());
if let Err(e) = cancel_tx.send(()) {
warn!("Failed to send git watch cancel signal {e:?}");
}
});
Ok(GitWatchResult { unlisten_event })
}
async fn handle_git_watch_event(
event_res: notify::Result<notify::Event>,
async_rx: &mut tokio::sync::mpsc::Receiver<notify::Result<notify::Event>>,
repo_dir: &Path,
workdir: &Path,
gitdir: &Path,
channel: &Channel<GitWorktreeStatus>,
) {
if !is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir) {
return;
}
send_worktree_status(repo_dir, channel);
let settle_window = sleep(GIT_STATUS_COALESCE_WINDOW);
tokio::pin!(settle_window);
loop {
select! {
Some(event_res) = async_rx.recv() => {
let _ = is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir);
}
_ = &mut settle_window => {
break;
}
}
}
send_worktree_status(repo_dir, channel);
}
fn is_relevant_git_watch_event(
event_res: notify::Result<notify::Event>,
repo_dir: &Path,
workdir: &Path,
gitdir: &Path,
) -> bool {
let event = match event_res {
Ok(event) => event,
Err(e) => {
error!("Git watch error: {:?}", e);
return false;
}
};
for path in event.paths {
if path.strip_prefix(gitdir).is_ok() {
return true;
}
let Ok(rela_path) = path.strip_prefix(workdir) else {
continue;
};
match git_path_is_ignored(repo_dir, rela_path) {
Ok(true) => {}
Ok(false) => return true,
Err(e) => {
debug!("Failed to check Git ignore status for {:?}: {e}", rela_path);
return true;
}
}
}
false
}
fn send_worktree_status(repo_dir: &Path, channel: &Channel<GitWorktreeStatus>) {
match git_worktree_status(repo_dir) {
Ok(status) => {
if let Err(e) = channel.send(status) {
warn!("Failed to send git worktree status: {:?}", e);
}
}
Err(e) => {
warn!("Failed to get git worktree status: {e}");
}
}
}

View File

@@ -67,6 +67,7 @@ mod commands;
mod encoding;
mod error;
mod git_ext;
mod git_watcher;
mod grpc;
mod history;
mod http_request;
@@ -1831,8 +1832,13 @@ pub fn run() {
git_ext::cmd_git_delete_remote_branch,
git_ext::cmd_git_merge_branch,
git_ext::cmd_git_rename_branch,
git_ext::cmd_git_branch_info,
git_ext::cmd_git_status,
git_ext::cmd_git_worktree_status,
git_ext::cmd_git_watch_worktree_status,
git_ext::cmd_git_log,
git_ext::cmd_git_log_for_file,
git_ext::cmd_git_file_diff_for_commit,
git_ext::cmd_git_initialize,
git_ext::cmd_git_clone,
git_ext::cmd_git_commit,
@@ -1844,6 +1850,8 @@ pub fn run() {
git_ext::cmd_git_add,
git_ext::cmd_git_unstage,
git_ext::cmd_git_reset_changes,
git_ext::cmd_git_restore_files,
git_ext::cmd_git_restore_file_from_commit,
git_ext::cmd_git_add_credential,
git_ext::cmd_git_remotes,
git_ext::cmd_git_add_remote,