diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-09 21:57:26 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-09 21:57:26 +0300 |
| commit | 819a71ea7a939e45234648c6ee09aa3bcaa6ff77 (patch) | |
| tree | 49013b4736d9569a4e7c4e3ae2fa3c95cdad0299 /src/app | |
| parent | d984ab2a3cb4b24788e24b3aaba24adea38b510f (diff) | |
fix: refresh git status off the main loop
fixes #85
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 92 | ||||
| -rw-r--r-- | src/app/api.rs | 11 | ||||
| -rw-r--r-- | src/app/creation.rs | 3 | ||||
| -rw-r--r-- | src/app/mod.rs | 50 | ||||
| -rw-r--r-- | src/app/runtime.rs | 49 |
5 files changed, 189 insertions, 16 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 506930ca..b8777cdc 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -7,6 +7,7 @@ use crate::detect::{Agent, AgentState}; use crate::events::AppEvent; use crate::layout::{find_in_direction, NavDirection, PaneId}; use crate::pane::EffectiveStateChange; +use crate::workspace::WorkspaceGitStatus; use super::state::{AppState, Mode, ToastKind, ToastNotification, ToastTarget, ViewLayout}; @@ -534,6 +535,33 @@ impl AppState { // --------------------------------------------------------------------------- impl AppState { + pub fn apply_workspace_git_statuses(&mut self, results: Vec<WorkspaceGitStatus>) -> bool { + let mut changed = false; + for result in results { + let Some(ws) = self + .workspaces + .iter_mut() + .find(|ws| ws.id == result.workspace_id) + else { + continue; + }; + + if ws.resolved_identity_cwd().as_ref() != Some(&result.resolved_identity_cwd) { + continue; + } + + if ws.cached_git_branch != result.branch { + ws.cached_git_branch = result.branch; + changed = true; + } + if ws.cached_git_ahead_behind != result.ahead_behind { + ws.cached_git_ahead_behind = result.ahead_behind; + changed = true; + } + } + changed + } + pub fn handle_app_event(&mut self, event: AppEvent) -> Vec<PaneStateUpdate> { match event { AppEvent::PaneDied { pane_id } => { @@ -593,6 +621,10 @@ impl AppState { // Intercepted in App::handle_internal_event before reaching this // dispatch; never touches AppState. AppEvent::ClipboardWrite { .. } => Vec::new(), + AppEvent::GitStatusRefreshed { results } => { + self.apply_workspace_git_statuses(results); + Vec::new() + } } } @@ -753,6 +785,66 @@ mod tests { } #[test] + fn apply_workspace_git_statuses_updates_matching_workspace() { + let mut state = app_with_workspaces(&["one", "two"]); + let first_id = state.workspaces[0].id.clone(); + let first_cwd = state.workspaces[0].resolved_identity_cwd().unwrap(); + let second_id = state.workspaces[1].id.clone(); + + let changed = state.apply_workspace_git_statuses(vec![WorkspaceGitStatus { + workspace_id: first_id, + resolved_identity_cwd: first_cwd, + branch: Some("main".into()), + ahead_behind: Some((2, 1)), + }]); + + assert!(changed); + assert_eq!(state.workspaces[0].branch().as_deref(), Some("main")); + assert_eq!(state.workspaces[0].git_ahead_behind(), Some((2, 1))); + assert_eq!(state.workspaces[1].id, second_id); + assert_eq!(state.workspaces[1].git_ahead_behind(), None); + } + + #[test] + fn apply_workspace_git_statuses_ignores_stale_cwd() { + let mut state = app_with_workspaces(&["one"]); + let workspace_id = state.workspaces[0].id.clone(); + state.workspaces[0].cached_git_branch = Some("old".into()); + state.workspaces[0].cached_git_ahead_behind = Some((1, 0)); + + let changed = state.apply_workspace_git_statuses(vec![WorkspaceGitStatus { + workspace_id, + resolved_identity_cwd: std::path::PathBuf::from("/definitely/not/current"), + branch: Some("main".into()), + ahead_behind: Some((0, 1)), + }]); + + assert!(!changed); + assert_eq!(state.workspaces[0].branch().as_deref(), Some("old")); + assert_eq!(state.workspaces[0].git_ahead_behind(), Some((1, 0))); + } + + #[test] + fn apply_workspace_git_statuses_clears_missing_git_status() { + let mut state = app_with_workspaces(&["one"]); + let workspace_id = state.workspaces[0].id.clone(); + let cwd = state.workspaces[0].resolved_identity_cwd().unwrap(); + state.workspaces[0].cached_git_branch = Some("main".into()); + state.workspaces[0].cached_git_ahead_behind = Some((1, 2)); + + let changed = state.apply_workspace_git_statuses(vec![WorkspaceGitStatus { + workspace_id, + resolved_identity_cwd: cwd, + branch: None, + ahead_behind: None, + }]); + + assert!(changed); + assert_eq!(state.workspaces[0].branch(), None); + assert_eq!(state.workspaces[0].git_ahead_behind(), None); + } + + #[test] fn update_ready_sets_explicit_upgrade_toast() { let mut state = AppState::test_new(); state.toast_config.delivery = crate::config::ToastDelivery::Herdr; diff --git a/src/app/api.rs b/src/app/api.rs index ab8058cf..bda7fc16 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; use super::{ @@ -16,6 +17,16 @@ impl App { return; } + if let AppEvent::GitStatusRefreshed { results } = ev { + self.git_refresh_in_flight = false; + self.last_git_remote_status_refresh = Instant::now(); + if self.state.apply_workspace_git_statuses(results) { + self.render_dirty.store(true, Ordering::Release); + self.render_notify.notify_one(); + } + return; + } + let overlay_state = if let AppEvent::PaneDied { pane_id } = &ev { self.overlay_panes.remove(pane_id) } else { diff --git a/src/app/creation.rs b/src/app/creation.rs index 508bf023..4e38084c 100644 --- a/src/app/creation.rs +++ b/src/app/creation.rs @@ -105,7 +105,7 @@ impl App { focus: bool, ) -> std::io::Result<usize> { let (rows, cols) = self.state.estimate_pane_size(); - let mut ws = Workspace::new( + let ws = Workspace::new( initial_cwd, rows, cols, @@ -115,7 +115,6 @@ impl App { self.render_notify.clone(), self.render_dirty.clone(), )?; - ws.refresh_git_ahead_behind(); self.state.workspaces.push(ws); let idx = self.state.workspaces.len() - 1; let workspace_id = self.state.workspaces[idx].id.clone(); diff --git a/src/app/mod.rs b/src/app/mod.rs index 1d09230a..1ea315cf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -66,6 +66,7 @@ pub struct App { pub(crate) config_diagnostic_deadline: Option<Instant>, pub(crate) toast_deadline: Option<Instant>, pub(crate) last_git_remote_status_refresh: Instant, + pub(crate) git_refresh_in_flight: bool, pub(crate) last_sidebar_divider_click: Option<Instant>, pub(crate) next_resize_poll: Instant, pub(crate) next_animation_tick: Option<Instant>, @@ -368,7 +369,7 @@ impl App { }; for ws in &mut state.workspaces { - ws.refresh_git_ahead_behind(); + ws.refresh_git_branch(); } // Background auto-update is disabled in monolithic no-session mode @@ -395,7 +396,8 @@ impl App { state, event_tx, event_rx, - last_git_remote_status_refresh: Instant::now(), + last_git_remote_status_refresh: Instant::now() - GIT_REMOTE_STATUS_REFRESH_INTERVAL, + git_refresh_in_flight: false, last_sidebar_divider_click: None, next_resize_poll: Instant::now() + RESIZE_POLL_INTERVAL, next_animation_tick: None, @@ -909,6 +911,50 @@ mod tests { } #[test] + fn git_refresh_deadline_is_suppressed_while_in_flight() { + let mut app = test_app(); + app.state.workspaces.push(Workspace::test_new("one")); + app.git_refresh_in_flight = true; + + assert_eq!(app.git_refresh_deadline(), None); + } + + #[test] + fn git_status_event_clears_in_flight_refresh() { + let mut app = test_app(); + app.git_refresh_in_flight = true; + let previous_refresh = Instant::now() - Duration::from_secs(10); + app.last_git_remote_status_refresh = previous_refresh; + + app.handle_internal_event(AppEvent::GitStatusRefreshed { + results: Vec::new(), + }); + + assert!(!app.git_refresh_in_flight); + assert!(app.last_git_remote_status_refresh > previous_refresh); + } + + #[test] + fn git_status_event_marks_render_dirty_when_status_changes() { + let mut app = test_app(); + app.state.workspaces.push(Workspace::test_new("one")); + app.render_dirty.store(false, Ordering::Release); + let workspace_id = app.state.workspaces[0].id.clone(); + let resolved_identity_cwd = app.state.workspaces[0].resolved_identity_cwd().unwrap(); + + app.handle_internal_event(AppEvent::GitStatusRefreshed { + results: vec![crate::workspace::WorkspaceGitStatus { + workspace_id, + resolved_identity_cwd, + branch: Some("render-dirty-test".into()), + ahead_behind: Some((1, 0)), + }], + }); + + assert!(app.render_dirty.load(Ordering::Acquire)); + } + + #[test] fn startup_uses_configured_agent_panel_scope() { let mut config = Config::default(); config.ui.agent_panel_scope = crate::config::AgentPanelScopeConfig::Current; diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 27c061bd..a4e93d3b 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -7,7 +7,8 @@ use super::{ AUTO_UPDATE_CHECK_INTERVAL, GIT_REMOTE_STATUS_REFRESH_INTERVAL, MIN_RENDER_INTERVAL, RESIZE_POLL_INTERVAL, }; -use crate::workspace::Workspace; +use crate::events::AppEvent; +use crate::workspace::{Workspace, WorkspaceGitStatus}; impl App { pub(crate) fn drain_api_requests(&mut self) -> bool { @@ -149,16 +150,7 @@ impl App { changed = true; } - if self - .git_refresh_deadline() - .is_some_and(|deadline| now >= deadline) - { - for ws in &mut self.state.workspaces { - ws.refresh_git_ahead_behind(); - } - self.last_git_remote_status_refresh = now; - changed = true; - } + self.start_git_status_refresh_if_due(now); if self .next_auto_update_check @@ -236,8 +228,41 @@ impl App { std::thread::spawn(move || crate::update::auto_update(update_tx)); } + pub(crate) fn start_git_status_refresh_if_due(&mut self, now: Instant) { + if !self + .git_refresh_deadline() + .is_some_and(|deadline| now >= deadline) + { + return; + } + + let workspaces: Vec<_> = self + .state + .workspaces + .iter() + .filter_map(|ws| ws.resolved_identity_cwd().map(|cwd| (ws.id.clone(), cwd))) + .collect(); + + if workspaces.is_empty() { + self.last_git_remote_status_refresh = now; + return; + } + + self.git_refresh_in_flight = true; + let event_tx = self.event_tx.clone(); + std::thread::spawn(move || { + let results = workspaces + .into_iter() + .map(|(workspace_id, resolved_identity_cwd)| { + Workspace::git_status_for_cwd(workspace_id, resolved_identity_cwd) + }) + .collect::<Vec<WorkspaceGitStatus>>(); + let _ = event_tx.blocking_send(AppEvent::GitStatusRefreshed { results }); + }); + } + pub(crate) fn git_refresh_deadline(&self) -> Option<Instant> { - (!self.state.workspaces.is_empty()) + (!self.git_refresh_in_flight && !self.state.workspaces.is_empty()) .then_some(self.last_git_remote_status_refresh + GIT_REMOTE_STATUS_REFRESH_INTERVAL) } |
