diff --git a/Cargo.lock b/Cargo.lock index c6d0987103..a7cbf1ec4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2440,6 +2440,7 @@ dependencies = [ "parking_lot 0.11.2", "regex", "rope", + "rpc", "serde", "serde_derive", "serde_json", diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index 89f8fda3ee..bc936fcb99 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -66,6 +66,7 @@ impl<'a> AddAssign<&'a Local> for Local { } } +/// A vector clock #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[u32; 8]>); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index b0b2a684d9..595d841d07 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -76,6 +76,7 @@ CREATE TABLE "worktree_entries" ( "is_symlink" BOOL NOT NULL, "is_ignored" BOOL NOT NULL, "is_deleted" BOOL NOT NULL, + "git_status" INTEGER, PRIMARY KEY(project_id, worktree_id, id), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE ); @@ -96,22 +97,6 @@ CREATE TABLE "worktree_repositories" ( CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); -CREATE TABLE "worktree_repository_statuses" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INTEGER NOT NULL, - "work_directory_id" INTEGER NOT NULL, - "repo_path" VARCHAR NOT NULL, - "status" INTEGER NOT NULL, - "scan_id" INTEGER NOT NULL, - "is_deleted" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, - FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); -CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); -CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); - CREATE TABLE "worktree_settings_files" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, diff --git a/crates/collab/migrations/20230605191135_remove_repository_statuses.sql b/crates/collab/migrations/20230605191135_remove_repository_statuses.sql new file mode 100644 index 0000000000..02ebc48870 --- /dev/null +++ b/crates/collab/migrations/20230605191135_remove_repository_statuses.sql @@ -0,0 +1,4 @@ +DROP TABLE "worktree_repository_statuses"; + +ALTER TABLE "worktree_entries" +ADD "git_status" INT8; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 1deca1baa8..7e2c376bc2 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1539,6 +1539,7 @@ impl Database { }), is_symlink: db_entry.is_symlink, is_ignored: db_entry.is_ignored, + git_status: db_entry.git_status.map(|status| status as i32), }); } } @@ -1573,54 +1574,6 @@ impl Database { worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, - removed_repo_paths: Default::default(), - updated_statuses: Default::default(), - }); - } - } - } - - // Repository Status Entries - for repository in worktree.updated_repositories.iter_mut() { - let repository_status_entry_filter = - if let Some(rejoined_worktree) = rejoined_worktree { - worktree_repository_statuses::Column::ScanId - .gt(rejoined_worktree.scan_id) - } else { - worktree_repository_statuses::Column::IsDeleted.eq(false) - }; - - let mut db_repository_statuses = - worktree_repository_statuses::Entity::find() - .filter( - Condition::all() - .add( - worktree_repository_statuses::Column::ProjectId - .eq(project.id), - ) - .add( - worktree_repository_statuses::Column::WorktreeId - .eq(worktree.id), - ) - .add( - worktree_repository_statuses::Column::WorkDirectoryId - .eq(repository.work_directory_id), - ) - .add(repository_status_entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_status_entry) = db_repository_statuses.next().await { - let db_status_entry = db_status_entry?; - if db_status_entry.is_deleted { - repository - .removed_repo_paths - .push(db_status_entry.repo_path); - } else { - repository.updated_statuses.push(proto::StatusEntry { - repo_path: db_status_entry.repo_path, - status: db_status_entry.status as i32, }); } } @@ -2396,6 +2349,7 @@ impl Database { mtime_nanos: ActiveValue::set(mtime.nanos as i32), is_symlink: ActiveValue::set(entry.is_symlink), is_ignored: ActiveValue::set(entry.is_ignored), + git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), is_deleted: ActiveValue::set(false), scan_id: ActiveValue::set(update.scan_id as i64), } @@ -2414,6 +2368,7 @@ impl Database { worktree_entry::Column::MtimeNanos, worktree_entry::Column::IsSymlink, worktree_entry::Column::IsIgnored, + worktree_entry::Column::GitStatus, worktree_entry::Column::ScanId, ]) .to_owned(), @@ -2467,68 +2422,6 @@ impl Database { ) .exec(&*tx) .await?; - - for repository in update.updated_repositories.iter() { - if !repository.updated_statuses.is_empty() { - worktree_repository_statuses::Entity::insert_many( - repository.updated_statuses.iter().map(|status_entry| { - worktree_repository_statuses::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - work_directory_id: ActiveValue::set( - repository.work_directory_id as i64, - ), - repo_path: ActiveValue::set(status_entry.repo_path.clone()), - status: ActiveValue::set(status_entry.status as i64), - scan_id: ActiveValue::set(update.scan_id as i64), - is_deleted: ActiveValue::set(false), - } - }), - ) - .on_conflict( - OnConflict::columns([ - worktree_repository_statuses::Column::ProjectId, - worktree_repository_statuses::Column::WorktreeId, - worktree_repository_statuses::Column::WorkDirectoryId, - worktree_repository_statuses::Column::RepoPath, - ]) - .update_columns([ - worktree_repository_statuses::Column::ScanId, - worktree_repository_statuses::Column::Status, - worktree_repository_statuses::Column::IsDeleted, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !repository.removed_repo_paths.is_empty() { - worktree_repository_statuses::Entity::update_many() - .filter( - worktree_repository_statuses::Column::ProjectId - .eq(project_id) - .and( - worktree_repository_statuses::Column::WorktreeId - .eq(worktree_id), - ) - .and( - worktree_repository_statuses::Column::WorkDirectoryId - .eq(repository.work_directory_id as i64), - ) - .and(worktree_repository_statuses::Column::RepoPath.is_in( - repository.removed_repo_paths.iter().map(String::as_str), - )), - ) - .set(worktree_repository_statuses::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - } } if !update.removed_repositories.is_empty() { @@ -2812,6 +2705,7 @@ impl Database { }), is_symlink: db_entry.is_symlink, is_ignored: db_entry.is_ignored, + git_status: db_entry.git_status.map(|status| status as i32), }); } } @@ -2837,41 +2731,12 @@ impl Database { proto::RepositoryEntry { work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, - removed_repo_paths: Default::default(), - updated_statuses: Default::default(), }, ); } } } - { - let mut db_status_entries = worktree_repository_statuses::Entity::find() - .filter( - Condition::all() - .add(worktree_repository_statuses::Column::ProjectId.eq(project_id)) - .add(worktree_repository_statuses::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - - while let Some(db_status_entry) = db_status_entries.next().await { - let db_status_entry = db_status_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64)) - { - if let Some(repository_entry) = worktree - .repository_entries - .get_mut(&(db_status_entry.work_directory_id as u64)) - { - repository_entry.updated_statuses.push(proto::StatusEntry { - repo_path: db_status_entry.repo_path, - status: db_status_entry.status as i32, - }); - } - } - } - } - // Populate worktree diagnostic summaries. { let mut db_summaries = worktree_diagnostic_summary::Entity::find() diff --git a/crates/collab/src/db/worktree_entry.rs b/crates/collab/src/db/worktree_entry.rs index 4eb1648b81..f2df808ee3 100644 --- a/crates/collab/src/db/worktree_entry.rs +++ b/crates/collab/src/db/worktree_entry.rs @@ -15,6 +15,7 @@ pub struct Model { pub inode: i64, pub mtime_seconds: i64, pub mtime_nanos: i32, + pub git_status: Option, pub is_symlink: bool, pub is_ignored: bool, pub is_deleted: bool, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5bc1a4e414..ce4ede8684 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2415,14 +2415,10 @@ async fn test_git_diff_base_change( " .unindent(); - client_a - .fs - .as_fake() - .set_index_for_repo( - Path::new("/dir/.git"), - &[(Path::new("a.txt"), diff_base.clone())], - ) - .await; + client_a.fs.as_fake().set_index_for_repo( + Path::new("/dir/.git"), + &[(Path::new("a.txt"), diff_base.clone())], + ); // Create the buffer let buffer_local_a = project_local @@ -2464,14 +2460,10 @@ async fn test_git_diff_base_change( ); }); - client_a - .fs - .as_fake() - .set_index_for_repo( - Path::new("/dir/.git"), - &[(Path::new("a.txt"), new_diff_base.clone())], - ) - .await; + client_a.fs.as_fake().set_index_for_repo( + Path::new("/dir/.git"), + &[(Path::new("a.txt"), new_diff_base.clone())], + ); // Wait for buffer_local_a to receive it deterministic.run_until_parked(); @@ -2513,14 +2505,10 @@ async fn test_git_diff_base_change( " .unindent(); - client_a - .fs - .as_fake() - .set_index_for_repo( - Path::new("/dir/sub/.git"), - &[(Path::new("b.txt"), diff_base.clone())], - ) - .await; + client_a.fs.as_fake().set_index_for_repo( + Path::new("/dir/sub/.git"), + &[(Path::new("b.txt"), diff_base.clone())], + ); // Create the buffer let buffer_local_b = project_local @@ -2562,14 +2550,10 @@ async fn test_git_diff_base_change( ); }); - client_a - .fs - .as_fake() - .set_index_for_repo( - Path::new("/dir/sub/.git"), - &[(Path::new("b.txt"), new_diff_base.clone())], - ) - .await; + client_a.fs.as_fake().set_index_for_repo( + Path::new("/dir/sub/.git"), + &[(Path::new("b.txt"), new_diff_base.clone())], + ); // Wait for buffer_local_b to receive it deterministic.run_until_parked(); @@ -2646,8 +2630,7 @@ async fn test_git_branch_name( client_a .fs .as_fake() - .set_branch_name(Path::new("/dir/.git"), Some("branch-1")) - .await; + .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); // Wait for it to catch up to the new branch deterministic.run_until_parked(); @@ -2673,8 +2656,7 @@ async fn test_git_branch_name( client_a .fs .as_fake() - .set_branch_name(Path::new("/dir/.git"), Some("branch-2")) - .await; + .set_branch_name(Path::new("/dir/.git"), Some("branch-2")); // Wait for buffer_local_a to receive it deterministic.run_until_parked(); @@ -2726,17 +2708,13 @@ async fn test_git_status_sync( const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; - client_a - .fs - .as_fake() - .set_status_for_repo( - Path::new("/dir/.git"), - &[ - (&Path::new(A_TXT), GitFileStatus::Added), - (&Path::new(B_TXT), GitFileStatus::Added), - ], - ) - .await; + client_a.fs.as_fake().set_status_for_repo_via_git_operation( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitFileStatus::Added), + (&Path::new(B_TXT), GitFileStatus::Added), + ], + ); let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = active_call_a @@ -2763,8 +2741,7 @@ async fn test_git_status_sync( assert_eq!(worktrees.len(), 1); let worktree = worktrees[0].clone(); let snapshot = worktree.read(cx).snapshot(); - let root_entry = snapshot.root_git_entry().unwrap(); - assert_eq!(root_entry.status_for_file(&snapshot, file), status); + assert_eq!(snapshot.status_for_file(file), status); } // Smoke test status reading @@ -2780,14 +2757,13 @@ async fn test_git_status_sync( client_a .fs .as_fake() - .set_status_for_repo( + .set_status_for_repo_via_working_copy_change( Path::new("/dir/.git"), &[ (&Path::new(A_TXT), GitFileStatus::Modified), (&Path::new(B_TXT), GitFileStatus::Modified), ], - ) - .await; + ); // Wait for buffer_local_a to receive it deterministic.run_until_parked(); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 3beff6942a..a95938f6b8 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -422,7 +422,7 @@ async fn apply_client_operation( ); ensure_project_shared(&project, client, cx).await; - if !client.fs.paths().contains(&new_root_path) { + if !client.fs.paths(false).contains(&new_root_path) { client.fs.create_dir(&new_root_path).await.unwrap(); } project @@ -628,12 +628,13 @@ async fn apply_client_operation( ensure_project_shared(&project, client, cx).await; let requested_version = buffer.read_with(cx, |buffer, _| buffer.version()); - let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx)); - let save = cx.background().spawn(async move { - let (saved_version, _, _) = save - .await + let save = project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); + let save = cx.spawn(|cx| async move { + save.await .map_err(|err| anyhow!("save request failed: {:?}", err))?; - assert!(saved_version.observed_all(&requested_version)); + assert!(buffer + .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() }) + .observed_all(&requested_version)); anyhow::Ok(()) }); if detach { @@ -743,7 +744,7 @@ async fn apply_client_operation( } => { if !client .fs - .directories() + .directories(false) .contains(&path.parent().unwrap().to_owned()) { return Err(TestError::Inapplicable); @@ -770,10 +771,16 @@ async fn apply_client_operation( repo_path, contents, } => { - if !client.fs.directories().contains(&repo_path) { + if !client.fs.directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } + for (path, _) in contents.iter() { + if !client.fs.files().contains(&repo_path.join(path)) { + return Err(TestError::Inapplicable); + } + } + log::info!( "{}: writing git index for repo {:?}: {:?}", client.username, @@ -789,13 +796,13 @@ async fn apply_client_operation( if client.fs.metadata(&dot_git_dir).await?.is_none() { client.fs.create_dir(&dot_git_dir).await?; } - client.fs.set_index_for_repo(&dot_git_dir, &contents).await; + client.fs.set_index_for_repo(&dot_git_dir, &contents); } GitOperation::WriteGitBranch { repo_path, new_branch, } => { - if !client.fs.directories().contains(&repo_path) { + if !client.fs.directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } @@ -810,15 +817,21 @@ async fn apply_client_operation( if client.fs.metadata(&dot_git_dir).await?.is_none() { client.fs.create_dir(&dot_git_dir).await?; } - client.fs.set_branch_name(&dot_git_dir, new_branch).await; + client.fs.set_branch_name(&dot_git_dir, new_branch); } GitOperation::WriteGitStatuses { repo_path, statuses, + git_operation, } => { - if !client.fs.directories().contains(&repo_path) { + if !client.fs.directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } + for (path, _) in statuses.iter() { + if !client.fs.files().contains(&repo_path.join(path)) { + return Err(TestError::Inapplicable); + } + } log::info!( "{}: writing git statuses for repo {:?}: {:?}", @@ -838,10 +851,16 @@ async fn apply_client_operation( client.fs.create_dir(&dot_git_dir).await?; } - client - .fs - .set_status_for_repo(&dot_git_dir, statuses.as_slice()) - .await; + if git_operation { + client + .fs + .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice()); + } else { + client.fs.set_status_for_repo_via_working_copy_change( + &dot_git_dir, + statuses.as_slice(), + ); + } } }, } @@ -913,9 +932,10 @@ fn check_consistency_between_clients(clients: &[(Rc, TestAppContext) assert_eq!( guest_snapshot.entries(false).collect::>(), host_snapshot.entries(false).collect::>(), - "{} has different snapshot than the host for worktree {:?} and project {:?}", + "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}", client.username, host_snapshot.abs_path(), + id, guest_project.remote_id(), ); assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), @@ -1230,6 +1250,7 @@ enum GitOperation { WriteGitStatuses { repo_path: PathBuf, statuses: Vec<(PathBuf, GitFileStatus)>, + git_operation: bool, }, } @@ -1575,7 +1596,7 @@ impl TestPlan { .choose(&mut self.rng) .cloned() else { continue }; let project_root_name = root_name_for_project(&project, cx); - let mut paths = client.fs.paths(); + let mut paths = client.fs.paths(false); paths.remove(0); let new_root_path = if paths.is_empty() || self.rng.gen() { Path::new("/").join(&self.next_root_dir_name(user_id)) @@ -1755,7 +1776,7 @@ impl TestPlan { let is_dir = self.rng.gen::(); let content; let mut path; - let dir_paths = client.fs.directories(); + let dir_paths = client.fs.directories(false); if is_dir { content = String::new(); @@ -1809,7 +1830,7 @@ impl TestPlan { let repo_path = client .fs - .directories() + .directories(false) .choose(&mut self.rng) .unwrap() .clone(); @@ -1855,9 +1876,12 @@ impl TestPlan { }) .collect::>(); + let git_operation = self.rng.gen::(); + GitOperation::WriteGitStatuses { repo_path, statuses, + git_operation, } } _ => unreachable!(), diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 54c6ce362a..7dda3f7273 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -14,6 +14,8 @@ lsp = { path = "../lsp" } rope = { path = "../rope" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } +rpc = { path = "../rpc" } + anyhow.workspace = true async-trait.workspace = true futures.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 4c6b6c24d3..fee7765d49 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -29,6 +29,8 @@ use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] use repository::{FakeGitRepositoryState, GitFileStatus}; #[cfg(any(test, feature = "test-support"))] +use std::ffi::OsStr; +#[cfg(any(test, feature = "test-support"))] use std::sync::Weak; lazy_static! { @@ -501,6 +503,11 @@ impl FakeFsState { } } +#[cfg(any(test, feature = "test-support"))] +lazy_static! { + pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git"); +} + #[cfg(any(test, feature = "test-support"))] impl FakeFs { pub fn new(executor: Arc) -> Arc { @@ -619,7 +626,7 @@ impl FakeFs { .boxed() } - pub fn with_git_state(&self, dot_git: &Path, f: F) + pub fn with_git_state(&self, dot_git: &Path, emit_git_event: bool, f: F) where F: FnOnce(&mut FakeGitRepositoryState), { @@ -633,18 +640,22 @@ impl FakeFs { f(&mut repo_state); - state.emit_event([dot_git]); + if emit_git_event { + state.emit_event([dot_git]); + } } else { panic!("not a directory"); } } - pub async fn set_branch_name(&self, dot_git: &Path, branch: Option>) { - self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into)) + pub fn set_branch_name(&self, dot_git: &Path, branch: Option>) { + self.with_git_state(dot_git, true, |state| { + state.branch_name = branch.map(Into::into) + }) } - pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) { - self.with_git_state(dot_git, |state| { + pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) { + self.with_git_state(dot_git, true, |state| { state.index_contents.clear(); state.index_contents.extend( head_state @@ -654,8 +665,32 @@ impl FakeFs { }); } - pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) { - self.with_git_state(dot_git, |state| { + pub fn set_status_for_repo_via_working_copy_change( + &self, + dot_git: &Path, + statuses: &[(&Path, GitFileStatus)], + ) { + self.with_git_state(dot_git, false, |state| { + state.worktree_statuses.clear(); + state.worktree_statuses.extend( + statuses + .iter() + .map(|(path, content)| ((**path).into(), content.clone())), + ); + }); + self.state.lock().emit_event( + statuses + .iter() + .map(|(path, _)| dot_git.parent().unwrap().join(path)), + ); + } + + pub fn set_status_for_repo_via_git_operation( + &self, + dot_git: &Path, + statuses: &[(&Path, GitFileStatus)], + ) { + self.with_git_state(dot_git, true, |state| { state.worktree_statuses.clear(); state.worktree_statuses.extend( statuses @@ -665,7 +700,7 @@ impl FakeFs { }); } - pub fn paths(&self) -> Vec { + pub fn paths(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); queue.push_back((PathBuf::from("/"), self.state.lock().root.clone())); @@ -675,12 +710,18 @@ impl FakeFs { queue.push_back((path.join(name), entry.clone())); } } - result.push(path); + if include_dot_git + || !path + .components() + .any(|component| component.as_os_str() == *FS_DOT_GIT) + { + result.push(path); + } } result } - pub fn directories(&self) -> Vec { + pub fn directories(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); queue.push_back((PathBuf::from("/"), self.state.lock().root.clone())); @@ -689,7 +730,13 @@ impl FakeFs { for (name, entry) in entries { queue.push_back((path.join(name), entry.clone())); } - result.push(path); + if include_dot_git + || !path + .components() + .any(|component| component.as_os_str() == *FS_DOT_GIT) + { + result.push(path); + } } } result diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 2c309351fc..488262887f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,8 @@ use anyhow::Result; use collections::HashMap; +use git2::ErrorCode; use parking_lot::Mutex; +use rpc::proto; use serde_derive::{Deserialize, Serialize}; use std::{ cmp::Ordering, @@ -24,7 +26,7 @@ pub trait GitRepository: Send { fn statuses(&self) -> Option>; - fn status(&self, path: &RepoPath) -> Option; + fn status(&self, path: &RepoPath) -> Result>; } impl std::fmt::Debug for dyn GitRepository { @@ -91,9 +93,18 @@ impl GitRepository for LibGitRepository { Some(map) } - fn status(&self, path: &RepoPath) -> Option { - let status = self.status_file(path).log_err()?; - read_status(status) + fn status(&self, path: &RepoPath) -> Result> { + let status = self.status_file(path); + match status { + Ok(status) => Ok(read_status(status)), + Err(e) => { + if e.code() == ErrorCode::NotFound { + Ok(None) + } else { + Err(e.into()) + } + } + } } } @@ -155,9 +166,9 @@ impl GitRepository for FakeGitRepository { Some(map) } - fn status(&self, path: &RepoPath) -> Option { + fn status(&self, path: &RepoPath) -> Result> { let state = self.state.lock(); - state.worktree_statuses.get(path).cloned() + Ok(state.worktree_statuses.get(path).cloned()) } } @@ -197,8 +208,51 @@ pub enum GitFileStatus { Conflict, } +impl GitFileStatus { + pub fn merge( + this: Option, + other: Option, + prefer_other: bool, + ) -> Option { + if prefer_other { + return other; + } else { + match (this, other) { + (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => { + Some(GitFileStatus::Conflict) + } + (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => { + Some(GitFileStatus::Modified) + } + (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => { + Some(GitFileStatus::Added) + } + _ => None, + } + } + } + + pub fn from_proto(git_status: Option) -> Option { + git_status.and_then(|status| { + proto::GitStatus::from_i32(status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) + }) + } + + pub fn to_proto(self) -> i32 { + match self { + GitFileStatus::Added => proto::GitStatus::Added as i32, + GitFileStatus::Modified => proto::GitStatus::Modified as i32, + GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, + } + } +} + #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(PathBuf); +pub struct RepoPath(pub PathBuf); impl RepoPath { pub fn new(path: PathBuf) -> Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2b2271bf1e..3406a3248c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -37,8 +37,8 @@ use language::{ range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch, - PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16, - Transaction, Unclipped, + PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use log::error; use lsp::{ @@ -69,7 +69,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use terminals::Terminals; use util::{ @@ -1617,7 +1617,7 @@ impl Project { &self, buffer: ModelHandle, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { let Some(file) = File::from_dyn(buffer.read(cx).file()) else { return Task::ready(Err(anyhow!("buffer doesn't have a file"))); }; @@ -5157,9 +5157,9 @@ impl Project { return None; } let path = &project_path.path; - changed_repos.iter().find(|(work_dir, change)| { - path.starts_with(work_dir) && change.git_dir_changed - })?; + changed_repos + .iter() + .find(|(work_dir, _)| path.starts_with(work_dir))?; let receiver = receiver.clone(); let path = path.clone(); Some(async move { @@ -5182,9 +5182,9 @@ impl Project { return None; } let path = file.path(); - changed_repos.iter().find(|(work_dir, change)| { - path.starts_with(work_dir) && change.git_dir_changed - })?; + changed_repos + .iter() + .find(|(work_dir, _)| path.starts_with(work_dir))?; Some((buffer, path.clone())) }) .collect::>(); @@ -5985,16 +5985,15 @@ impl Project { .await?; let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id()); - let (saved_version, fingerprint, mtime) = this - .update(&mut cx, |this, cx| this.save_buffer(buffer, cx)) + this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx)) .await?; - Ok(proto::BufferSaved { + Ok(buffer.read_with(&cx, |buffer, _| proto::BufferSaved { project_id, buffer_id, - version: serialize_version(&saved_version), - mtime: Some(mtime.into()), - fingerprint: language::proto::serialize_fingerprint(fingerprint), - }) + version: serialize_version(buffer.saved_version()), + mtime: Some(buffer.saved_mtime().into()), + fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()), + })) } async fn handle_reload_buffers( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 97656b7b10..ee190e1a31 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,7 +7,7 @@ use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; use fs::{ - repository::{GitFileStatus, GitRepository, RepoPath, RepoPathDescendants}, + repository::{GitFileStatus, GitRepository, RepoPath}, Fs, LineEnding, }; use futures::{ @@ -45,7 +45,7 @@ use std::{ fmt, future::Future, mem, - ops::{Deref, DerefMut}, + ops::{AddAssign, Deref, DerefMut, Sub}, path::{Path, PathBuf}, pin::Pin, sync::{ @@ -55,7 +55,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{paths::HOME, ResultExt, TakeUntilExt}; +use util::{paths::HOME, ResultExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -124,15 +124,6 @@ pub struct Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - pub(crate) statuses: TreeMap, -} - -fn read_git_status(git_status: i32) -> Option { - proto::GitStatus::from_i32(git_status).map(|status| match status { - proto::GitStatus::Added => GitFileStatus::Added, - proto::GitStatus::Modified => GitFileStatus::Modified, - proto::GitStatus::Conflict => GitFileStatus::Conflict, - }) } impl RepositoryEntry { @@ -150,115 +141,19 @@ impl RepositoryEntry { .map(|entry| RepositoryWorkDirectory(entry.path.clone())) } - pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option { - self.work_directory - .relativize(snapshot, path) - .and_then(|repo_path| { - self.statuses - .iter_from(&repo_path) - .take_while(|(key, _)| key.starts_with(&repo_path)) - // Short circuit once we've found the highest level - .take_until(|(_, status)| status == &&GitFileStatus::Conflict) - .map(|(_, status)| status) - .reduce( - |status_first, status_second| match (status_first, status_second) { - (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { - &GitFileStatus::Conflict - } - (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => { - &GitFileStatus::Modified - } - _ => &GitFileStatus::Added, - }, - ) - .copied() - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option { - self.work_directory - .relativize(snapshot, path) - .and_then(|repo_path| (&self.statuses).get(&repo_path)) - .cloned() - } - - pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry { - let mut updated_statuses: Vec = Vec::new(); - let mut removed_statuses: Vec = Vec::new(); - - let mut self_statuses = self.statuses.iter().peekable(); - let mut other_statuses = other.statuses.iter().peekable(); - loop { - match (self_statuses.peek(), other_statuses.peek()) { - (Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => { - match Ord::cmp(self_repo_path, other_repo_path) { - Ordering::Less => { - updated_statuses.push(make_status_entry(self_repo_path, self_status)); - self_statuses.next(); - } - Ordering::Equal => { - if self_status != other_status { - updated_statuses - .push(make_status_entry(self_repo_path, self_status)); - } - - self_statuses.next(); - other_statuses.next(); - } - Ordering::Greater => { - removed_statuses.push(make_repo_path(other_repo_path)); - other_statuses.next(); - } - } - } - (Some((self_repo_path, self_status)), None) => { - updated_statuses.push(make_status_entry(self_repo_path, self_status)); - self_statuses.next(); - } - (None, Some((other_repo_path, _))) => { - removed_statuses.push(make_repo_path(other_repo_path)); - other_statuses.next(); - } - (None, None) => break, - } - } - + pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id().to_proto(), branch: self.branch.as_ref().map(|str| str.to_string()), - removed_repo_paths: removed_statuses, - updated_statuses, } } } -fn make_repo_path(path: &RepoPath) -> String { - path.as_os_str().to_string_lossy().to_string() -} - -fn make_status_entry(path: &RepoPath, status: &GitFileStatus) -> proto::StatusEntry { - proto::StatusEntry { - repo_path: make_repo_path(path), - status: match status { - GitFileStatus::Added => proto::GitStatus::Added.into(), - GitFileStatus::Modified => proto::GitStatus::Modified.into(), - GitFileStatus::Conflict => proto::GitStatus::Conflict.into(), - }, - } -} - impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), - updated_statuses: value - .statuses - .iter() - .map(|(repo_path, status)| make_status_entry(repo_path, status)) - .collect(), - removed_repo_paths: Default::default(), } } } @@ -330,7 +225,6 @@ pub struct BackgroundScannerState { #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { - pub(crate) work_dir_scan_id: usize, pub(crate) git_dir_scan_id: usize, pub(crate) repo_ptr: Arc>, /// Path to the actual .git folder. @@ -864,18 +758,13 @@ impl LocalWorktree { entry.path.clone(), GitRepositoryChange { old_repository: None, - git_dir_changed: true, }, )); } new_repos.next(); } Ordering::Equal => { - let git_dir_changed = - new_repo.git_dir_scan_id != old_repo.git_dir_scan_id; - let work_dir_changed = - new_repo.work_dir_scan_id != old_repo.work_dir_scan_id; - if git_dir_changed || work_dir_changed { + if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id { if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) { let old_repo = old_snapshot .repository_entries @@ -885,7 +774,6 @@ impl LocalWorktree { entry.path.clone(), GitRepositoryChange { old_repository: old_repo, - git_dir_changed, }, )); } @@ -903,7 +791,6 @@ impl LocalWorktree { entry.path.clone(), GitRepositoryChange { old_repository: old_repo, - git_dir_changed: true, }, )); } @@ -917,7 +804,6 @@ impl LocalWorktree { entry.path.clone(), GitRepositoryChange { old_repository: None, - git_dir_changed: true, }, )); } @@ -933,7 +819,6 @@ impl LocalWorktree { entry.path.clone(), GitRepositoryChange { old_repository: old_repo, - git_dir_changed: true, }, )); } @@ -1038,7 +923,7 @@ impl LocalWorktree { path: Arc, has_changed_file: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { let handle = cx.handle(); let buffer = buffer_handle.read(cx); @@ -1094,7 +979,7 @@ impl LocalWorktree { buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); }); - Ok((version, fingerprint, entry.mtime)) + Ok(()) }) } @@ -1405,7 +1290,7 @@ impl RemoteWorktree { &self, buffer_handle: ModelHandle, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { let buffer = buffer_handle.read(cx); let buffer_id = buffer.remote_id(); let version = buffer.version(); @@ -1430,7 +1315,7 @@ impl RemoteWorktree { buffer.did_save(version.clone(), fingerprint, mtime, cx); }); - Ok((version, fingerprint, mtime)) + Ok(()) }) } @@ -1574,7 +1459,7 @@ impl Snapshot { fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option> { let removed_entry = self.entries_by_id.remove(&entry_id, &())?; self.entries_by_path = { - let mut cursor = self.entries_by_path.cursor(); + let mut cursor = self.entries_by_path.cursor::(); let mut new_entries_by_path = cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &()); while let Some(entry) = cursor.item() { @@ -1592,6 +1477,14 @@ impl Snapshot { Some(removed_entry.path) } + #[cfg(any(test, feature = "test-support"))] + pub fn status_for_file(&self, path: impl Into) -> Option { + let path = path.into(); + self.entries_by_path + .get(&PathKey(Arc::from(path)), &()) + .and_then(|entry| entry.git_status) + } + pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); @@ -1643,26 +1536,10 @@ impl Snapshot { ProjectEntryId::from_proto(repository.work_directory_id).into(); if let Some(entry) = self.entry_for_id(*work_directory_entry) { - let mut statuses = TreeMap::default(); - for status_entry in repository.updated_statuses { - let Some(git_file_status) = read_git_status(status_entry.status) else { - continue; - }; - - let repo_path = RepoPath::new(status_entry.repo_path.into()); - statuses.insert(repo_path, git_file_status); - } - let work_directory = RepositoryWorkDirectory(entry.path.clone()); if self.repository_entries.get(&work_directory).is_some() { self.repository_entries.update(&work_directory, |repo| { repo.branch = repository.branch.map(Into::into); - repo.statuses.insert_tree(statuses); - - for repo_path in repository.removed_repo_paths { - let repo_path = RepoPath::new(repo_path.into()); - repo.statuses.remove(&repo_path); - } }); } else { self.repository_entries.insert( @@ -1670,7 +1547,6 @@ impl Snapshot { RepositoryEntry { work_directory: work_directory_entry, branch: repository.branch.map(Into::into), - statuses, }, ) } @@ -1799,6 +1675,64 @@ impl Snapshot { }) } + /// Update the `git_status` of the given entries such that files' + /// statuses bubble up to their ancestor directories. + pub fn propagate_git_statuses(&self, result: &mut [Entry]) { + let mut cursor = self + .entries_by_path + .cursor::<(TraversalProgress, GitStatuses)>(); + let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); + + let mut result_ix = 0; + loop { + let next_entry = result.get(result_ix); + let containing_entry = entry_stack.last().map(|(ix, _)| &result[*ix]); + + let entry_to_finish = match (containing_entry, next_entry) { + (Some(_), None) => entry_stack.pop(), + (Some(containing_entry), Some(next_path)) => { + if !next_path.path.starts_with(&containing_entry.path) { + entry_stack.pop() + } else { + None + } + } + (None, Some(_)) => None, + (None, None) => break, + }; + + if let Some((entry_ix, prev_statuses)) = entry_to_finish { + cursor.seek_forward( + &TraversalTarget::PathSuccessor(&result[entry_ix].path), + Bias::Left, + &(), + ); + + let statuses = cursor.start().1 - prev_statuses; + + result[entry_ix].git_status = if statuses.conflict > 0 { + Some(GitFileStatus::Conflict) + } else if statuses.modified > 0 { + Some(GitFileStatus::Modified) + } else if statuses.added > 0 { + Some(GitFileStatus::Added) + } else { + None + }; + } else { + if result[result_ix].is_dir() { + cursor.seek_forward( + &TraversalTarget::Path(&result[result_ix].path), + Bias::Left, + &(), + ); + entry_stack.push((result_ix, cursor.start().1)); + } + result_ix += 1; + } + } + } + pub fn paths(&self) -> impl Iterator> { let empty_path = Path::new(""); self.entries_by_path @@ -1895,6 +1829,14 @@ impl LocalSnapshot { self.git_repositories.get(&repo.work_directory.0) } + pub(crate) fn local_repo_for_path( + &self, + path: &Path, + ) -> Option<(RepositoryWorkDirectory, &LocalRepositoryEntry)> { + let (path, repo) = self.repository_and_work_directory_for_path(path)?; + Some((path, self.git_repositories.get(&repo.work_directory_id())?)) + } + pub(crate) fn repo_for_metadata( &self, path: &Path, @@ -2039,7 +1981,8 @@ impl LocalSnapshot { entry } - fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option<()> { + #[must_use = "Changed paths must be used for diffing later"] + fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option>> { let abs_path = self.abs_path.join(&parent_path); let work_dir: Arc = parent_path.parent().unwrap().into(); @@ -2056,35 +1999,67 @@ impl LocalSnapshot { .entry_for_path(work_dir.clone()) .map(|entry| entry.id)?; - if self.git_repositories.get(&work_dir_id).is_none() { - let repo = fs.open_repo(abs_path.as_path())?; - let work_directory = RepositoryWorkDirectory(work_dir.clone()); - let scan_id = self.scan_id; - - let repo_lock = repo.lock(); - - self.repository_entries.insert( - work_directory, - RepositoryEntry { - work_directory: work_dir_id.into(), - branch: repo_lock.branch_name().map(Into::into), - statuses: repo_lock.statuses().unwrap_or_default(), - }, - ); - drop(repo_lock); - - self.git_repositories.insert( - work_dir_id, - LocalRepositoryEntry { - work_dir_scan_id: scan_id, - git_dir_scan_id: scan_id, - repo_ptr: repo, - git_dir_path: parent_path.clone(), - }, - ) + if self.git_repositories.get(&work_dir_id).is_some() { + return None; } - Some(()) + let repo = fs.open_repo(abs_path.as_path())?; + let work_directory = RepositoryWorkDirectory(work_dir.clone()); + + let repo_lock = repo.lock(); + + self.repository_entries.insert( + work_directory.clone(), + RepositoryEntry { + work_directory: work_dir_id.into(), + branch: repo_lock.branch_name().map(Into::into), + }, + ); + + let changed_paths = self.scan_statuses(repo_lock.deref(), &work_directory); + + drop(repo_lock); + + self.git_repositories.insert( + work_dir_id, + LocalRepositoryEntry { + git_dir_scan_id: 0, + repo_ptr: repo, + git_dir_path: parent_path.clone(), + }, + ); + + Some(changed_paths) + } + + #[must_use = "Changed paths must be used for diffing later"] + fn scan_statuses( + &mut self, + repo_ptr: &dyn GitRepository, + work_directory: &RepositoryWorkDirectory, + ) -> Vec> { + let mut changes = vec![]; + let mut edits = vec![]; + for mut entry in self + .descendent_entries(false, false, &work_directory.0) + .cloned() + { + let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else { + continue; + }; + let git_file_status = repo_ptr + .status(&RepoPath(repo_path.into())) + .log_err() + .flatten(); + if entry.git_status != git_file_status { + entry.git_status = git_file_status; + changes.push(entry.path.clone()); + edits.push(Edit::Insert(entry)); + } + } + + self.entries_by_path.edit(edits, &()); + changes } fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet { @@ -2139,13 +2114,14 @@ impl BackgroundScannerState { self.snapshot.insert_entry(entry, fs) } + #[must_use = "Changed paths must be used for diffing later"] fn populate_dir( &mut self, parent_path: Arc, entries: impl IntoIterator, ignore: Option>, fs: &dyn Fs, - ) { + ) -> Option>> { let mut parent_entry = if let Some(parent_entry) = self .snapshot .entries_by_path @@ -2157,7 +2133,7 @@ impl BackgroundScannerState { "populating a directory {:?} that has been removed", parent_path ); - return; + return None; }; match parent_entry.kind { @@ -2165,7 +2141,7 @@ impl BackgroundScannerState { parent_entry.kind = EntryKind::Dir; } EntryKind::Dir => {} - _ => return, + _ => return None, } if let Some(ignore) = ignore { @@ -2175,10 +2151,6 @@ impl BackgroundScannerState { .insert(abs_parent_path, (ignore, false)); } - if parent_path.file_name() == Some(&DOT_GIT) { - self.snapshot.build_repo(parent_path, fs); - } - let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); @@ -2197,6 +2169,11 @@ impl BackgroundScannerState { .entries_by_path .edit(entries_by_path_edits, &()); self.snapshot.entries_by_id.edit(entries_by_id_edits, &()); + + if parent_path.file_name() == Some(&DOT_GIT) { + return self.snapshot.build_repo(parent_path, fs); + } + None } fn remove_path(&mut self, path: &Path) { @@ -2518,6 +2495,7 @@ pub struct Entry { pub mtime: SystemTime, pub is_symlink: bool, pub is_ignored: bool, + pub git_status: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -2546,9 +2524,6 @@ pub enum PathChange { pub struct GitRepositoryChange { /// The previous state of the repository, if it already existed. pub old_repository: Option, - /// Whether the content of the .git directory changed. This will be false - /// if only the repository's work directory changed. - pub git_dir_changed: bool, } pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; @@ -2573,6 +2548,7 @@ impl Entry { mtime: metadata.mtime, is_symlink: metadata.is_symlink, is_ignored: false, + git_status: None, } } @@ -2583,6 +2559,10 @@ impl Entry { pub fn is_file(&self) -> bool { matches!(self.kind, EntryKind::File(_)) } + + pub fn git_status(&self) -> Option { + self.git_status + } } impl sum_tree::Item for Entry { @@ -2600,12 +2580,23 @@ impl sum_tree::Item for Entry { visible_file_count = 0; } + let mut statuses = GitStatuses::default(); + match self.git_status { + Some(status) => match status { + GitFileStatus::Added => statuses.added = 1, + GitFileStatus::Modified => statuses.modified = 1, + GitFileStatus::Conflict => statuses.conflict = 1, + }, + None => {} + } + EntrySummary { max_path: self.path.clone(), count: 1, visible_count, file_count, visible_file_count, + statuses, } } } @@ -2625,6 +2616,7 @@ pub struct EntrySummary { visible_count: usize, file_count: usize, visible_file_count: usize, + statuses: GitStatuses, } impl Default for EntrySummary { @@ -2635,6 +2627,7 @@ impl Default for EntrySummary { visible_count: 0, file_count: 0, visible_file_count: 0, + statuses: Default::default(), } } } @@ -2648,6 +2641,7 @@ impl sum_tree::Summary for EntrySummary { self.visible_count += rhs.visible_count; self.file_count += rhs.file_count; self.visible_file_count += rhs.visible_file_count; + self.statuses += rhs.statuses; } } @@ -2807,6 +2801,7 @@ impl BackgroundScanner { let mut state = self.state.lock(); state.snapshot.completed_scan_id = state.snapshot.scan_id; } + self.send_status_update(false, None); // Process any any FS events that occurred while performing the initial scan. @@ -2862,14 +2857,16 @@ impl BackgroundScanner { self.update_ignore_statuses().await; { - let mut snapshot = &mut self.state.lock().snapshot; + let mut state = self.state.lock(); if let Some(paths) = paths { for path in paths { - self.reload_repo_for_file_path(&path, &mut *snapshot, self.fs.as_ref()); + self.reload_git_repo(&path, &mut *state, self.fs.as_ref()); } } + let mut snapshot = &mut state.snapshot; + let mut git_repositories = mem::take(&mut snapshot.git_repositories); git_repositories.retain(|work_directory_id, _| { snapshot @@ -2993,14 +2990,18 @@ impl BackgroundScanner { let mut new_jobs: Vec> = Vec::new(); let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore = None; - let (root_abs_path, root_char_bag, next_entry_id) = { + let (root_abs_path, root_char_bag, next_entry_id, repository) = { let snapshot = &self.state.lock().snapshot; ( snapshot.abs_path().clone(), snapshot.root_char_bag, self.next_entry_id.clone(), + snapshot + .local_repo_for_path(&job.path) + .map(|(work_dir, repo)| (work_dir, repo.clone())), ) }; + let mut child_paths = self.fs.read_dir(&job.abs_path).await?; while let Some(child_abs_path) = child_paths.next().await { let child_abs_path: Arc = match child_abs_path { @@ -3093,6 +3094,18 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); + if !child_entry.is_ignored { + if let Some((repo_path, repo)) = &repository { + if let Ok(path) = child_path.strip_prefix(&repo_path.0) { + child_entry.git_status = repo + .repo_ptr + .lock() + .status(&RepoPath(path.into())) + .log_err() + .flatten(); + } + } + } } new_entries.push(child_entry); @@ -3100,10 +3113,19 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - state.populate_dir(job.path.clone(), new_entries, new_ignore, self.fs.as_ref()); + let changed_paths = + state.populate_dir(job.path.clone(), new_entries, new_ignore, self.fs.as_ref()); if let Err(ix) = state.changed_paths.binary_search(&job.path) { state.changed_paths.insert(ix, job.path.clone()); } + if let Some(changed_paths) = changed_paths { + util::extend_sorted( + &mut state.changed_paths, + changed_paths, + usize::MAX, + Ord::cmp, + ) + } } for new_job in new_jobs { @@ -3147,12 +3169,14 @@ impl BackgroundScanner { // refreshed. Do this before adding any new entries, so that renames can be // detected regardless of the order of the paths. let mut event_paths = Vec::>::with_capacity(abs_paths.len()); + let mut event_metadata = Vec::<_>::with_capacity(abs_paths.len()); for (abs_path, metadata) in abs_paths.iter().zip(metadata.iter()) { if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { if matches!(metadata, Ok(None)) || doing_recursive_update { state.remove_path(path); } event_paths.push(path.into()); + event_metadata.push(metadata); } else { log::error!( "unexpected event {:?} for root path {:?}", @@ -3162,7 +3186,7 @@ impl BackgroundScanner { } } - for (path, metadata) in event_paths.iter().cloned().zip(metadata.into_iter()) { + for (path, metadata) in event_paths.iter().cloned().zip(event_metadata.into_iter()) { let abs_path: Arc = root_abs_path.join(&path).into(); match metadata { @@ -3170,6 +3194,7 @@ impl BackgroundScanner { let ignore_stack = state .snapshot .ignore_stack_for_abs_path(&abs_path, metadata.is_dir); + let mut fs_entry = Entry::new( path.clone(), &metadata, @@ -3177,6 +3202,24 @@ impl BackgroundScanner { state.snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); + + if !fs_entry.is_ignored { + if !fs_entry.is_dir() { + if let Some((work_dir, repo)) = + state.snapshot.local_repo_for_path(&path) + { + if let Ok(path) = path.strip_prefix(work_dir.0) { + fs_entry.git_status = repo + .repo_ptr + .lock() + .status(&RepoPath(path.into())) + .log_err() + .flatten() + } + } + } + } + state.insert_entry(fs_entry, self.fs.as_ref()); if let Some(scan_queue_tx) = &scan_queue_tx { @@ -3219,8 +3262,6 @@ impl BackgroundScanner { .components() .any(|component| component.as_os_str() == *DOT_GIT) { - let scan_id = snapshot.scan_id; - if let Some(repository) = snapshot.repository_for_work_directory(path) { let entry = repository.work_directory.0; snapshot.git_repositories.remove(&entry); @@ -3230,113 +3271,80 @@ impl BackgroundScanner { .remove(&RepositoryWorkDirectory(path.into())); return Some(()); } - - let repo = snapshot.repository_for_path(&path)?; - let repo_path = repo.work_directory.relativize(&snapshot, &path)?; - let work_dir = repo.work_directory(snapshot)?; - let work_dir_id = repo.work_directory; - - snapshot - .git_repositories - .update(&work_dir_id, |entry| entry.work_dir_scan_id = scan_id); - - snapshot.repository_entries.update(&work_dir, |entry| { - entry - .statuses - .remove_range(&repo_path, &RepoPathDescendants(&repo_path)) - }); } + // TODO statuses + // Track when a .git is removed and iterate over the file system there + Some(()) } - fn reload_repo_for_file_path( + fn reload_git_repo( &self, path: &Path, - snapshot: &mut LocalSnapshot, + state: &mut BackgroundScannerState, fs: &dyn Fs, ) -> Option<()> { - let scan_id = snapshot.scan_id; + let scan_id = state.snapshot.scan_id; if path .components() .any(|component| component.as_os_str() == *DOT_GIT) { let (entry_id, repo_ptr) = { - let Some((entry_id, repo)) = snapshot.repo_for_metadata(&path) else { + let Some((entry_id, repo)) = state.snapshot.repo_for_metadata(&path) else { let dot_git_dir = path.ancestors() .skip_while(|ancestor| ancestor.file_name() != Some(&*DOT_GIT)) .next()?; - snapshot.build_repo(dot_git_dir.into(), fs); + let changed_paths = state.snapshot.build_repo(dot_git_dir.into(), fs); + if let Some(changed_paths) = changed_paths { + util::extend_sorted( + &mut state.changed_paths, + changed_paths, + usize::MAX, + Ord::cmp, + ); + } + return None; }; if repo.git_dir_scan_id == scan_id { return None; } + (*entry_id, repo.repo_ptr.to_owned()) }; - let work_dir = snapshot + let work_dir = state + .snapshot .entry_for_id(entry_id) .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; let repo = repo_ptr.lock(); repo.reload_index(); let branch = repo.branch_name(); - let statuses = repo.statuses().unwrap_or_default(); - snapshot.git_repositories.update(&entry_id, |entry| { - entry.work_dir_scan_id = scan_id; + state.snapshot.git_repositories.update(&entry_id, |entry| { entry.git_dir_scan_id = scan_id; }); - snapshot.repository_entries.update(&work_dir, |entry| { - entry.branch = branch.map(Into::into); - entry.statuses = statuses; - }); - } else { - if snapshot - .entry_for_path(&path) - .map(|entry| entry.is_ignored) - .unwrap_or(false) - { - self.remove_repo_path(&path, snapshot); - return None; - } + state + .snapshot + .snapshot + .repository_entries + .update(&work_dir, |entry| { + entry.branch = branch.map(Into::into); + }); - let repo = snapshot.repository_for_path(&path)?; + let changed_paths = state.snapshot.scan_statuses(repo.deref(), &work_dir); - let work_dir = repo.work_directory(snapshot)?; - let work_dir_id = repo.work_directory.clone(); - - let (local_repo, git_dir_scan_id) = - snapshot.git_repositories.update(&work_dir_id, |entry| { - entry.work_dir_scan_id = scan_id; - (entry.repo_ptr.clone(), entry.git_dir_scan_id) - })?; - - // Short circuit if we've already scanned everything - if git_dir_scan_id == scan_id { - return None; - } - - let mut repository = snapshot.repository_entries.remove(&work_dir)?; - - for entry in snapshot.descendent_entries(false, false, path) { - let Some(repo_path) = repo.work_directory.relativize(snapshot, &entry.path) else { - continue; - }; - - let status = local_repo.lock().status(&repo_path); - if let Some(status) = status { - repository.statuses.insert(repo_path.clone(), status); - } else { - repository.statuses.remove(&repo_path); - } - } - - snapshot.repository_entries.insert(work_dir, repository) + util::extend_sorted( + &mut state.changed_paths, + changed_paths, + usize::MAX, + Ord::cmp, + ) } Some(()) @@ -3501,7 +3509,6 @@ impl BackgroundScanner { if new_paths.item().map_or(false, |e| e.path < path.0) { new_paths.seek_forward(&path, Bias::Left, &()); } - loop { match (old_paths.item(), new_paths.item()) { (Some(old_entry), Some(new_entry)) => { @@ -3520,6 +3527,13 @@ impl BackgroundScanner { } Ordering::Equal => { if self.phase == EventsReceivedDuringInitialScan { + if old_entry.id != new_entry.id { + changes.push(( + old_entry.path.clone(), + old_entry.id, + Removed, + )); + } // If the worktree was not fully initialized when this event was generated, // we can't know whether this entry was added during the scan or whether // it was merely updated. @@ -3702,6 +3716,39 @@ impl<'a> Default for TraversalProgress<'a> { } } +#[derive(Clone, Debug, Default, Copy)] +struct GitStatuses { + added: usize, + modified: usize, + conflict: usize, +} + +impl AddAssign for GitStatuses { + fn add_assign(&mut self, rhs: Self) { + self.added += rhs.added; + self.modified += rhs.modified; + self.conflict += rhs.conflict; + } +} + +impl Sub for GitStatuses { + type Output = GitStatuses; + + fn sub(self, rhs: Self) -> Self::Output { + GitStatuses { + added: self.added - rhs.added, + modified: self.modified - rhs.modified, + conflict: self.conflict - rhs.conflict, + } + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + *self += summary.statuses + } +} + pub struct Traversal<'a> { cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, include_ignored: bool, @@ -3803,6 +3850,14 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa } } +impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)> + for TraversalTarget<'b> +{ + fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { + self.cmp(&cursor_location.0, &()) + } +} + struct ChildEntriesIter<'a> { parent_path: &'a Path, traversal: Traversal<'a>, @@ -3851,6 +3906,7 @@ impl<'a> From<&'a Entry> for proto::Entry { mtime: Some(entry.mtime.into()), is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, + git_status: entry.git_status.map(|status| status.to_proto()), } } } @@ -3876,6 +3932,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { mtime: mtime.into(), is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, + git_status: GitFileStatus::from_proto(entry.git_status), }) } else { Err(anyhow!( @@ -4656,7 +4713,7 @@ mod tests { log::info!("mutating fs"); let mut files = Vec::new(); let mut dirs = Vec::new(); - for path in fs.as_fake().paths() { + for path in fs.as_fake().paths(false) { if path.starts_with(root_path) { if fs.is_file(&path).await { files.push(path); @@ -4931,14 +4988,14 @@ mod tests { cx.read(|cx| { let tree = tree.read(cx); - let (work_dir, repo) = tree.repositories().next().unwrap(); + let (work_dir, _) = tree.repositories().next().unwrap(); assert_eq!(work_dir.as_ref(), Path::new("projects/project1")); assert_eq!( - repo.status_for_file(tree, Path::new("projects/project1/a")), + tree.status_for_file(Path::new("projects/project1/a")), Some(GitFileStatus::Modified) ); assert_eq!( - repo.status_for_file(tree, Path::new("projects/project1/b")), + tree.status_for_file(Path::new("projects/project1/b")), Some(GitFileStatus::Added) ); }); @@ -4952,14 +5009,14 @@ mod tests { cx.read(|cx| { let tree = tree.read(cx); - let (work_dir, repo) = tree.repositories().next().unwrap(); + let (work_dir, _) = tree.repositories().next().unwrap(); assert_eq!(work_dir.as_ref(), Path::new("projects/project2")); assert_eq!( - repo.status_for_file(tree, Path::new("projects/project2/a")), + tree.status_for_file(Path::new("projects/project2/a")), Some(GitFileStatus::Modified) ); assert_eq!( - repo.status_for_file(tree, Path::new("projects/project2/b")), + tree.status_for_file(Path::new("projects/project2/b")), Some(GitFileStatus::Added) ); }); @@ -5088,7 +5145,7 @@ mod tests { } #[gpui::test] - async fn test_git_status(cx: &mut TestAppContext) { + async fn test_git_status(deterministic: Arc, cx: &mut TestAppContext) { const IGNORE_RULE: &'static str = "**/target"; let root = temp_tree(json!({ @@ -5131,6 +5188,7 @@ mod tests { const F_TXT: &'static str = "f.txt"; const DOTGITIGNORE: &'static str = ".gitignore"; const BUILD_FILE: &'static str = "target/build_file"; + let project_path: &Path = &Path::new("project"); let work_dir = root.path().join("project"); let mut repo = git_init(work_dir.as_path()); @@ -5140,29 +5198,37 @@ mod tests { git_add(Path::new(DOTGITIGNORE), &repo); git_commit("Initial commit", &repo); - std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); - tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); // Check that the right git state is observed on startup tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); assert_eq!(snapshot.repository_entries.iter().count(), 1); - let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); + let (dir, _) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); - assert_eq!(repo.statuses.iter().count(), 3); assert_eq!( - repo.statuses.get(&Path::new(A_TXT).into()), - Some(&GitFileStatus::Modified) + snapshot.status_for_file(project_path.join(B_TXT)), + Some(GitFileStatus::Added) ); assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file(project_path.join(F_TXT)), + Some(GitFileStatus::Added) ); + }); + + std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); + + tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + assert_eq!( - repo.statuses.get(&Path::new(F_TXT).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file(project_path.join(A_TXT)), + Some(GitFileStatus::Modified) ); }); @@ -5170,17 +5236,19 @@ mod tests { git_add(Path::new(B_TXT), &repo); git_commit("Committing modified and added", &repo); tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); // Check that repo only changes are tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.statuses.iter().count(), 1); assert_eq!( - repo.statuses.get(&Path::new(F_TXT).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file(project_path.join(F_TXT)), + Some(GitFileStatus::Added) ); + + assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None); + assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); }); git_reset(0, &repo); @@ -5189,25 +5257,20 @@ mod tests { std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap(); tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); // Check that more complex repo changes are tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.statuses.iter().count(), 3); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file(project_path.join(B_TXT)), + Some(GitFileStatus::Added) ); assert_eq!( - repo.statuses.get(&Path::new(E_TXT).into()), - Some(&GitFileStatus::Modified) - ); - assert_eq!( - repo.statuses.get(&Path::new(F_TXT).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file(project_path.join(E_TXT)), + Some(GitFileStatus::Modified) ); }); @@ -5223,14 +5286,7 @@ mod tests { git_commit("Committing modified git ignore", &repo); tree.flush_fs_events(cx).await; - - // Check that non-repo behavior is tracked - tree.read_with(cx, |tree, _cx| { - let snapshot = tree.snapshot(); - let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - - assert_eq!(repo.statuses.iter().count(), 0); - }); + deterministic.run_until_parked(); let mut renamed_dir_name = "first_directory/second_directory"; const RENAMED_FILE: &'static str = "rf.txt"; @@ -5243,16 +5299,14 @@ mod tests { .unwrap(); tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - - assert_eq!(repo.statuses.iter().count(), 1); assert_eq!( - repo.statuses - .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), - Some(&GitFileStatus::Added) + snapshot + .status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)), + Some(GitFileStatus::Added) ); }); @@ -5265,20 +5319,143 @@ mod tests { .unwrap(); tree.flush_fs_events(cx).await; + deterministic.run_until_parked(); tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.statuses.iter().count(), 1); assert_eq!( - repo.statuses - .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), - Some(&GitFileStatus::Added) + snapshot.status_for_file( + project_path + .join(Path::new(renamed_dir_name)) + .join(RENAMED_FILE) + ), + Some(GitFileStatus::Added) ); }); } + #[gpui::test] + async fn test_propagate_git_statuses(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + ".git": {}, + "a": { + "b": { + "c1.txt": "", + "c2.txt": "", + }, + "d": { + "e1.txt": "", + "e2.txt": "", + "e3.txt": "", + } + }, + "f": { + "no-status.txt": "" + }, + "g": { + "h1.txt": "", + "h2.txt": "" + }, + + }), + ) + .await; + + fs.set_status_for_repo_via_git_operation( + &Path::new("/root/.git"), + &[ + (Path::new("a/b/c1.txt"), GitFileStatus::Added), + (Path::new("a/d/e2.txt"), GitFileStatus::Modified), + (Path::new("g/h2.txt"), GitFileStatus::Conflict), + ], + ); + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + let tree = Worktree::local( + client, + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + cx.foreground().run_until_parked(); + let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), Some(GitFileStatus::Conflict)), + (Path::new("a"), Some(GitFileStatus::Modified)), + (Path::new("a/b"), Some(GitFileStatus::Added)), + (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), + (Path::new("a/b/c2.txt"), None), + (Path::new("a/d"), Some(GitFileStatus::Modified)), + (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), + (Path::new("f"), None), + (Path::new("f/no-status.txt"), None), + (Path::new("g"), Some(GitFileStatus::Conflict)), + (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new("a/b"), Some(GitFileStatus::Added)), + (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), + (Path::new("a/b/c2.txt"), None), + (Path::new("a/d"), Some(GitFileStatus::Modified)), + (Path::new("a/d/e1.txt"), None), + (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), + (Path::new("f"), None), + (Path::new("f/no-status.txt"), None), + (Path::new("g"), Some(GitFileStatus::Conflict)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), + (Path::new("a/b/c2.txt"), None), + (Path::new("a/d/e1.txt"), None), + (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), + (Path::new("f/no-status.txt"), None), + ], + ); + + #[track_caller] + fn check_propagated_statuses( + snapshot: &Snapshot, + expected_statuses: &[(&Path, Option)], + ) { + let mut entries = expected_statuses + .iter() + .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) + .collect::>(); + snapshot.propagate_git_statuses(&mut entries); + assert_eq!( + entries + .iter() + .map(|e| (e.path.as_ref(), e.git_status)) + .collect::>(), + expected_statuses + ); + } + } + #[track_caller] fn git_init(path: &Path) -> git2::Repository { git2::Repository::init(path).expect("Failed to initialize git repository") diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 60af95174f..9563d54be8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1002,6 +1002,7 @@ impl ProjectPanel { mtime: entry.mtime, is_symlink: false, is_ignored: false, + git_status: entry.git_status, }); } if expanded_dir_ids.binary_search(&entry.id).is_err() @@ -1011,6 +1012,9 @@ impl ProjectPanel { } entry_iter.advance(); } + + snapshot.propagate_git_statuses(&mut visible_worktree_entries); + visible_worktree_entries.sort_by(|entry_a, entry_b| { let mut components_a = entry_a.path.components().peekable(); let mut components_b = entry_b.path.components().peekable(); @@ -1108,14 +1112,8 @@ impl ProjectPanel { .unwrap_or(&[]); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - for (entry, repo) in - snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter()) - { - let status = (git_status_setting - && entry.path.parent().is_some() - && !entry.is_ignored) - .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path))) - .flatten(); + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then(|| entry.git_status).flatten(); let mut details = EntryDetails { filename: entry diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ca3ec7cafb..ce4dd7f7cf 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1005,13 +1005,12 @@ message Entry { Timestamp mtime = 5; bool is_symlink = 6; bool is_ignored = 7; + optional GitStatus git_status = 8; } message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; - repeated string removed_repo_paths = 3; - repeated StatusEntry updated_statuses = 4; } message StatusEntry { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 36f0f926cd..6c6150aa3a 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -480,6 +480,11 @@ impl SumTree { } => child_trees.last().unwrap().rightmost_leaf(), } } + + #[cfg(debug_assertions)] + pub fn _debug_entries(&self) -> Vec<&T> { + self.iter().collect::>() + } } impl PartialEq for SumTree { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 79f3c68514..71ffacebf3 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -57,7 +57,7 @@ pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { } /// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and -/// enforcing a maximum length. Sort the items according to the given callback. Before calling this, +/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this, /// both `vec` and `new_items` should already be sorted according to the `cmp` comparator. pub fn extend_sorted(vec: &mut Vec, new_items: I, limit: usize, mut cmp: F) where