diff --git a/crates/git/src/lib.rs b/crates/git/src/lib.rs index 2e0fbdbb..59b09caf 100644 --- a/crates/git/src/lib.rs +++ b/crates/git/src/lib.rs @@ -28,7 +28,9 @@ mod state; pub use state::{StateHandle, StateOptions}; #[doc(hidden)] -pub use state::{debug_engine_refs, debug_status_recomputes, debug_worktree_watches}; +pub use state::{ + debug_engine_refs, debug_status_recomputes, debug_wait_worktree_watches, debug_worktree_watches, +}; /// The slash-separated Git path of `abs` relative to `workdir`, if any. /// On Windows this converts backslashes so that ignore rules and index diff --git a/crates/git/src/state.rs b/crates/git/src/state.rs index 13b51336..33ae7bf1 100644 --- a/crates/git/src/state.rs +++ b/crates/git/src/state.rs @@ -15,7 +15,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender}; -use std::sync::{Arc, Mutex, OnceLock, Weak}; +use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak}; use std::time::{Duration, Instant}; use crate::model::{ @@ -223,11 +223,27 @@ fn status_recomputes() -> &'static Mutex> { /// arm. Test/diagnostic hook. #[doc(hidden)] pub fn debug_worktree_watches(gitdir: &Path) -> Option> { - worktree_watch_sets().lock().unwrap().get(gitdir).cloned() + worktree_watch_sets().0.lock().unwrap().get(gitdir).cloned() } -fn worktree_watch_sets() -> &'static Mutex>> { - static SETS: OnceLock>>> = OnceLock::new(); +/// Blocks until the engine publishes a watch set for `gitdir` that +/// satisfies `done`. Test/diagnostic hook. +#[doc(hidden)] +pub fn debug_wait_worktree_watches(gitdir: &Path, mut done: impl FnMut(&[PathBuf]) -> bool) { + let (sets, published) = worktree_watch_sets(); + drop( + published + .wait_while(sets.lock().unwrap(), |sets| { + !done(sets.get(gitdir).map_or(&[], Vec::as_slice)) + }) + .unwrap(), + ); +} + +type WatchSets = (Mutex>>, Condvar); + +fn worktree_watch_sets() -> &'static WatchSets { + static SETS: OnceLock = OnceLock::new(); SETS.get_or_init(Default::default) } @@ -1241,10 +1257,11 @@ impl Engine { } arms.watch_set_changed = false; let dirs = arms.worktree_dirs.iter().cloned().collect(); - worktree_watch_sets() - .lock() + let (sets, published) = worktree_watch_sets(); + sets.lock() .unwrap() .insert((*self.repo.gitdir).clone(), dirs); + published.notify_all(); } /// Directories to arm for the ignore sources that live *outside* every diff --git a/crates/git/tests/engine.rs b/crates/git/tests/engine.rs index 4e60fd2d..d8069321 100644 --- a/crates/git/tests/engine.rs +++ b/crates/git/tests/engine.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::path::PathBuf; use std::process::Command; use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, Instant}; +use std::time::Duration; use yas_git::native::{ self, BlameRecord, DiffRecord, DiffRequest, Endpoint, LogRequest, PatchRecord, PatchRequest, @@ -79,6 +79,19 @@ impl Repository { .map(|path| std::fs::canonicalize(path).unwrap()) .collect() } + + /// Waits for the engine to arm or disarm `dir`. The status snapshot can + /// reflect an index written after its pass reconciled the watch set, so + /// the set catches up on the pass that index event triggers. + fn wait_for_watch(&self, dir: &str, armed: bool) { + let path = self.0.join(dir); + yas_git::debug_wait_worktree_watches(&self.0.join(".git"), |watches| { + watches + .iter() + .any(|watch| std::fs::canonicalize(watch).is_ok_and(|watch| watch == path)) + == armed + }); + } } impl Drop for Repository { @@ -466,13 +479,13 @@ fn index_changes_reconcile_tracked_exceptions_to_ignore_pruning() { repository.git(&["add", "-f", "generated/nested/tracked"]); assert_eq!(state.status()["generated/nested/tracked"], (b'A', b' ')); - assert!(watches().contains(&repository.0.join("generated/nested"))); + repository.wait_for_watch("generated/nested", true); repository.write("generated/nested/tracked", b"modified after force-add\n"); assert_eq!(state.status()["generated/nested/tracked"], (b'A', b'M')); repository.git(&["rm", "--cached", "-f", "generated/nested/tracked"]); assert!(state.status().is_empty()); - assert!(!watches().contains(&repository.0.join("generated"))); + repository.wait_for_watch("generated", false); } #[test] @@ -594,17 +607,7 @@ fn reattached_status_selection_observes_changes_made_while_pruned() { // Wait for the asynchronous detach to disarm this directory. Writes // inside it then cannot invalidate a cached ignored-status segment. - let deadline = Instant::now() + Duration::from_secs(5); - while repository - .worktree_watches() - .contains(&repository.0.join("ignored")) - { - assert!( - Instant::now() < deadline, - "ignored directory was not pruned" - ); - std::thread::sleep(Duration::from_millis(5)); - } + repository.wait_for_watch("ignored", false); repository.write("ignored/second", b"new ignored file\n"); let broader = StateStream::new(&handle, status_options(true, true)); assert_eq!(broader.status().len(), 2); diff --git a/crates/server/src/yas_process.rs b/crates/server/src/yas_process.rs index 712c1959..c027e6aa 100644 --- a/crates/server/src/yas_process.rs +++ b/crates/server/src/yas_process.rs @@ -2704,11 +2704,7 @@ mod windows_tests { ) -> T { let mut output = String::new(); loop { - match tokio::time::timeout(Duration::from_secs(20), attachment.next()) - .await - .expect("output in time") - .expect("the attachment is open") - { + match attachment.next().await.expect("the attachment is open") { Event::Output { stream, data, .. } => { *acked += data.len() as u64; attachment.acknowledge_output(stream, *acked).unwrap(); @@ -2955,7 +2951,6 @@ mod windows_tests { let server = Server::new(false, true); let runtime = Runtime::new(server.clone()); let session = runtime.session([34; 16], None).unwrap(); - let started = std::time::Instant::now(); let mut attachment = session .spawn( &with_residue_grace( @@ -2975,6 +2970,7 @@ mod windows_tests { .unwrap(); let mut acked = 0; let pid = read_until(&mut attachment, &mut acked, residue_pid).await; + let started = std::time::Instant::now(); let exit = exit_of(&mut attachment, &mut acked, Duration::from_secs(20)).await; assert_eq!( (exit.kind, exit.code),