From d63910428f8ebb611b36e96b511aaeac4d4ea1e7 Mon Sep 17 00:00:00 2001 From: Max Gasner Date: Mon, 28 Sep 2026 20:50:59 +0000 Subject: [PATCH] git: send the first real state as the WATCH snapshot run_git_watch opened every subscription with an empty snapshot and only delivered repository state as a later Reset. One-shot `yas git status` printed that empty snapshot, so it always said clean. The first native snapshot now is the initial snapshot. --- crates/server/src/yas.rs | 152 +++++++++++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 20 deletions(-) diff --git a/crates/server/src/yas.rs b/crates/server/src/yas.rs index 7def6b30..c34b28f1 100644 --- a/crates/server/src/yas.rs +++ b/crates/server/src/yas.rs @@ -32149,25 +32149,7 @@ async fn run_git_watch( journal: Option, ) { let mut sent_bytes = 0u64; - if send_git_snapshot( - subscription_id, - revision, - Vec::new(), - event_limit, - &mut sent_bytes, - &control, - &out, - &cancellation, - journal.as_ref(), - ) - .await - .is_err() - { - return; - } - if let Some(journal) = &journal { - journal.state(revision, 0, sent_bytes); - } + let mut initial = true; loop { let event = tokio::select! { event = watch.next() => event, @@ -32191,9 +32173,32 @@ async fn run_git_watch( return; } Ok(super::yas_git_adapter::WatchEvent::Snapshot { state_id, records }) => { - let next = revision.saturating_add(1).max(1); let before = sent_bytes; let count = records.len(); + if std::mem::take(&mut initial) { + if send_git_snapshot( + subscription_id, + revision, + records, + event_limit, + &mut sent_bytes, + &control, + &out, + &cancellation, + journal.as_ref(), + ) + .await + .is_err() + { + return; + } + if let Some(journal) = &journal { + journal.state(revision, count, sent_bytes - before); + } + watch.acknowledge(state_id); + continue; + } + let next = revision.saturating_add(1).max(1); if send_git_state_event( subscription_id, Phase::Reset, @@ -40248,6 +40253,113 @@ mod tests { (client, codec, server_hello, task) } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn git_watch_initial_snapshot_carries_worktree_status() { + let root = tempfile::tempdir().unwrap(); + let git = |args: &[&str]| { + let status = std::process::Command::new("git") + .args(["-c", "user.name=t", "-c", "user.email=t@t"]) + .args(args) + .current_dir(root.path()) + .status() + .unwrap(); + assert!(status.success()); + }; + git(&["init", "-q"]); + std::fs::write(root.path().join("hello.txt"), b"hello\n").unwrap(); + git(&["add", "hello.txt"]); + git(&["commit", "-qm", "init"]); + std::fs::write(root.path().join("hello.txt"), b"changed\n").unwrap(); + + let state = super::super::tests::process_transport::test_state( + super::super::process::Server::new(false, true), + ); + let (mut client, codec, _, server_task) = + start_registered_session(state, &[family::TRANSFER, family::GIT]).await; + write_request( + &mut client, + &codec, + family::GIT, + yas_git_wire::request_kind::OPEN, + 10, + &yas_git_wire::Open { + source: yas_git_wire::RepositorySource::PlatformPath( + root.path().to_string_lossy().as_bytes().to_vec(), + ), + extensions: Extensions::default(), + }, + ) + .await; + let opened = next_sensitive_result( + &mut client, + &codec, + family::GIT, + yas_git_wire::request_kind::OPEN, + 10, + ) + .await; + assert_eq!(opened.status, Status::Ok); + let repository_handle = yas_git_wire::OpenResult::decode(&opened.body) + .unwrap() + .repository_handle; + write_request( + &mut client, + &codec, + family::GIT, + yas_git_wire::request_kind::WATCH, + 11, + &yas_git_wire::Watch { + repository_handle, + datasets: (yas_wire::schema::git::WATCH_HEAD | yas_wire::schema::git::WATCH_STATUS) + as u16, + state: Watch { + initial_credit: 1024 * 1024, + resume: None, + extensions: Extensions::default(), + }, + }, + ) + .await; + let watched = next_sensitive_result( + &mut client, + &codec, + family::GIT, + yas_git_wire::request_kind::WATCH, + 11, + ) + .await; + assert_eq!(watched.status, Status::Ok); + let mut entities = Vec::new(); + loop { + let frame = timeout(TEST_TIMEOUT, next_frame(&mut client, &codec)) + .await + .unwrap(); + assert_eq!(frame.header.kind, yas_git_wire::event_kind::STATE); + let event = StateEvent::decode(&frame.payload).unwrap(); + assert_ne!(event.phase, Phase::Reset); + for record in &event.records { + entities.push(yas_git_wire::EntityRecord::decode(&record.body).unwrap()); + } + if event.phase == Phase::SnapshotEnd { + break; + } + } + assert!( + entities + .iter() + .any(|entity| matches!(entity.body, yas_git_wire::EntityBody::Head(_))) + ); + assert!( + entities + .iter() + .any(|entity| matches!(entity.body, yas_git_wire::EntityBody::Status(_))) + ); + + drop(client); + timeout(TEST_TIMEOUT, server_task).await.unwrap().unwrap(); + } + #[cfg(unix)] #[tokio::test] async fn extension_installation_support_matches_server_policy() {