From a58d3d8128fb1b544d70f558a40cbc978933772f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 1 May 2023 15:35:22 -0700 Subject: [PATCH 01/34] Add a data driven representation of the current git repository state to the worktree snapshots WIP: Switch git repositories to use SumTrees Co-authored-by: Nathan --- crates/fs/src/repository.rs | 6 ++ crates/project/src/project.rs | 2 +- crates/project/src/worktree.rs | 156 ++++++++++++++++++++++---------- crates/sum_tree/src/tree_map.rs | 40 +++++++- 4 files changed, 154 insertions(+), 50 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 6ead6f36b5d1b903d02d54f78ef5349234462c76..75f9ca34dde98c445add2b84056c3eda583d6a88 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -15,6 +15,12 @@ pub trait GitRepository: Send { fn load_index_text(&self, relative_file_path: &Path) -> Option; } +impl std::fmt::Debug for dyn GitRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dyn GitRepository<...>").finish() + } +} + #[async_trait::async_trait] impl GitRepository for LibGitRepository { fn reload_index(&self) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 40687ce0a7fa518f61896ac5b42866086edb78a4..7eede73c75fd2f6e3905fd58abb9bde373b8541d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4696,7 +4696,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, worktree: ModelHandle, - repos: &[GitRepositoryEntry], + repos: &[LocalGitRepositoryEntry], cx: &mut ModelContext, ) { for (_, buffer) in &self.opened_buffers { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1281ddeff3f86607cbe5a25990a3171aaab0913e..2746bc95d225246ee5054f73b18f41fb4b967559 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -51,7 +51,7 @@ use std::{ }, time::{Duration, SystemTime}, }; -use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeSet}; +use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use util::{paths::HOME, ResultExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] @@ -102,6 +102,7 @@ pub struct Snapshot { root_char_bag: CharBag, entries_by_path: SumTree, entries_by_id: SumTree, + repository_entries: TreeMap, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused @@ -116,8 +117,35 @@ pub struct Snapshot { completed_scan_id: usize, } +#[derive(Clone, Debug)] +pub struct RepositoryEntry { + // Path to the actual .git folder. + // Note: if .git is a file, this points to the folder indicated by the .git file + pub(crate) git_dir_path: Arc, + pub(crate) git_dir_entry_id: ProjectEntryId, + pub(crate) scan_id: usize, + // TODO: pub(crate) head_ref: Arc, +} + +impl RepositoryEntry { + // Note that this path should be relative to the worktree root. + pub(crate) fn in_dot_git(&self, path: &Path) -> bool { + path.starts_with(self.git_dir_path.as_ref()) + } +} + +/// This path corresponds to the 'content path' (the folder that contains the .git) +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct RepositoryEntryKey(Arc); + +impl Default for RepositoryEntryKey { + fn default() -> Self { + RepositoryEntryKey(Arc::from(Path::new(""))) + } +} + #[derive(Clone)] -pub struct GitRepositoryEntry { +pub struct LocalGitRepositoryEntry { pub(crate) repo: Arc>, pub(crate) scan_id: usize, @@ -128,7 +156,7 @@ pub struct GitRepositoryEntry { pub(crate) git_dir_path: Arc, } -impl std::fmt::Debug for GitRepositoryEntry { +impl std::fmt::Debug for LocalGitRepositoryEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GitRepositoryEntry") .field("content_path", &self.content_path) @@ -137,27 +165,17 @@ impl std::fmt::Debug for GitRepositoryEntry { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories: Vec, + git_repositories_old: Vec, + // The ProjectEntryId corresponds to the entry for the .git dir + git_repositories: TreeMap>>, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, } -impl Clone for LocalSnapshot { - fn clone(&self) -> Self { - Self { - ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(), - git_repositories: self.git_repositories.iter().cloned().collect(), - removed_entry_ids: self.removed_entry_ids.clone(), - next_entry_id: self.next_entry_id.clone(), - snapshot: self.snapshot.clone(), - } - } -} - impl Deref for LocalSnapshot { type Target = Snapshot; @@ -191,7 +209,7 @@ struct ShareState { pub enum Event { UpdatedEntries(HashMap, PathChange>), - UpdatedGitRepositories(Vec), + UpdatedGitRepositories(Vec), } impl Entity for Worktree { @@ -222,8 +240,9 @@ impl Worktree { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - git_repositories: Default::default(), + git_repositories_old: Default::default(), removed_entry_ids: Default::default(), + git_repositories: Default::default(), next_entry_id, snapshot: Snapshot { id: WorktreeId::from_usize(cx.model_id()), @@ -232,6 +251,7 @@ impl Worktree { root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), + repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }, @@ -330,6 +350,7 @@ impl Worktree { .collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), + repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }; @@ -599,8 +620,8 @@ impl LocalWorktree { fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext) { let updated_repos = Self::changed_repos( - &self.snapshot.git_repositories, - &new_snapshot.git_repositories, + &self.snapshot.git_repositories_old, + &new_snapshot.git_repositories_old, ); self.snapshot = new_snapshot; @@ -614,13 +635,13 @@ impl LocalWorktree { } fn changed_repos( - old_repos: &[GitRepositoryEntry], - new_repos: &[GitRepositoryEntry], - ) -> Vec { + old_repos: &[LocalGitRepositoryEntry], + new_repos: &[LocalGitRepositoryEntry], + ) -> Vec { fn diff<'a>( - a: &'a [GitRepositoryEntry], - b: &'a [GitRepositoryEntry], - updated: &mut HashMap<&'a Path, GitRepositoryEntry>, + a: &'a [LocalGitRepositoryEntry], + b: &'a [LocalGitRepositoryEntry], + updated: &mut HashMap<&'a Path, LocalGitRepositoryEntry>, ) { for a_repo in a { let matched = b.iter().find(|b_repo| { @@ -633,7 +654,7 @@ impl LocalWorktree { } } - let mut updated = HashMap::<&Path, GitRepositoryEntry>::default(); + let mut updated = HashMap::<&Path, LocalGitRepositoryEntry>::default(); diff(old_repos, new_repos, &mut updated); diff(new_repos, old_repos, &mut updated); @@ -1002,9 +1023,10 @@ impl LocalWorktree { let mut share_tx = Some(share_tx); let mut prev_snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - git_repositories: Default::default(), + git_repositories_old: Default::default(), removed_entry_ids: Default::default(), next_entry_id: Default::default(), + git_repositories: Default::default(), snapshot: Snapshot { id: WorktreeId(worktree_id as usize), abs_path: Path::new("").into(), @@ -1012,6 +1034,7 @@ impl LocalWorktree { root_char_bag: Default::default(), entries_by_path: Default::default(), entries_by_id: Default::default(), + repository_entries: Default::default(), scan_id: 0, completed_scan_id: 0, }, @@ -1409,8 +1432,8 @@ impl Snapshot { impl LocalSnapshot { // Gives the most specific git repository for a given path - pub(crate) fn repo_for(&self, path: &Path) -> Option { - self.git_repositories + pub(crate) fn repo_for(&self, path: &Path) -> Option { + self.git_repositories_old .iter() .rev() //git_repository is ordered lexicographically .find(|repo| repo.manages(path)) @@ -1420,9 +1443,9 @@ impl LocalSnapshot { pub(crate) fn repo_with_dot_git_containing( &mut self, path: &Path, - ) -> Option<&mut GitRepositoryEntry> { + ) -> Option<&mut LocalGitRepositoryEntry> { // Git repositories cannot be nested, so we don't need to reverse the order - self.git_repositories + self.git_repositories_old .iter_mut() .find(|repo| repo.in_dot_git(path)) } @@ -1597,14 +1620,31 @@ impl LocalSnapshot { if parent_path.file_name() == Some(&DOT_GIT) { let abs_path = self.abs_path.join(&parent_path); let content_path: Arc = parent_path.parent().unwrap().into(); + + let key = RepositoryEntryKey(content_path.clone()); + if self.repository_entries.get(&key).is_none() { + if let Some(repo) = fs.open_repo(abs_path.as_path()) { + self.repository_entries.insert( + key, + RepositoryEntry { + git_dir_path: parent_path.clone(), + git_dir_entry_id: parent_entry.id, + scan_id: 0, + }, + ); + + self.git_repositories.insert(parent_entry.id, repo) + } + } + if let Err(ix) = self - .git_repositories + .git_repositories_old .binary_search_by_key(&&content_path, |repo| &repo.content_path) { if let Some(repo) = fs.open_repo(abs_path.as_path()) { - self.git_repositories.insert( + self.git_repositories_old.insert( ix, - GitRepositoryEntry { + LocalGitRepositoryEntry { repo, scan_id: 0, content_path, @@ -1672,12 +1712,17 @@ impl LocalSnapshot { *scan_id = self.snapshot.scan_id; } } else if path.file_name() == Some(&DOT_GIT) { + let repo_entry_key = RepositoryEntryKey(path.parent().unwrap().into()); + self.snapshot + .repository_entries + .update(&repo_entry_key, |repo| repo.scan_id = self.snapshot.scan_id); + let parent_path = path.parent().unwrap(); if let Ok(ix) = self - .git_repositories - .binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref()) + .git_repositories_old + .binary_search_by_key(&parent_path, |repo| repo.content_path.as_ref()) { - self.git_repositories[ix].scan_id = self.snapshot.scan_id; + self.git_repositories_old[ix].scan_id = self.snapshot.scan_id; } } } @@ -1719,12 +1764,12 @@ impl LocalSnapshot { ignore_stack } - pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] { - &self.git_repositories + pub fn git_repo_entries(&self) -> &[LocalGitRepositoryEntry] { + &self.git_repositories_old } } -impl GitRepositoryEntry { +impl LocalGitRepositoryEntry { // Note that these paths should be relative to the worktree root. pub(crate) fn manages(&self, path: &Path) -> bool { path.starts_with(self.content_path.as_ref()) @@ -2318,9 +2363,15 @@ impl BackgroundScanner { self.update_ignore_statuses().await; let mut snapshot = self.snapshot.lock(); - let mut git_repositories = mem::take(&mut snapshot.git_repositories); + + let mut git_repositories = mem::take(&mut snapshot.git_repositories_old); git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some()); + snapshot.git_repositories_old = git_repositories; + + let mut git_repositories = mem::take(&mut snapshot.git_repositories); + git_repositories.retain(|project_entry_id, _| snapshot.contains_entry(*project_entry_id)); snapshot.git_repositories = git_repositories; + snapshot.removed_entry_ids.clear(); snapshot.completed_scan_id = snapshot.scan_id; drop(snapshot); @@ -2607,11 +2658,22 @@ impl BackgroundScanner { snapshot.insert_entry(fs_entry, self.fs.as_ref()); let scan_id = snapshot.scan_id; + if let Some(repo) = snapshot.repo_with_dot_git_containing(&path) { repo.repo.lock().reload_index(); repo.scan_id = scan_id; } + let repo_with_path_in_dotgit = snapshot + .repository_entries + .iter() + .find_map(|(key, repo)| repo.in_dot_git(&path).then(|| key.clone())); + if let Some(key) = repo_with_path_in_dotgit { + snapshot + .repository_entries + .update(&key, |entry| entry.scan_id = scan_id); + } + if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { @@ -3427,8 +3489,8 @@ mod tests { #[test] fn test_changed_repos() { - fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> GitRepositoryEntry { - GitRepositoryEntry { + fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> LocalGitRepositoryEntry { + LocalGitRepositoryEntry { repo: Arc::new(Mutex::new(FakeGitRepository::default())), scan_id, content_path: git_dir_path.as_ref().parent().unwrap().into(), @@ -3436,13 +3498,13 @@ mod tests { } } - let prev_repos: Vec = vec![ + let prev_repos: Vec = vec![ fake_entry("/.git", 0), fake_entry("/a/.git", 0), fake_entry("/a/b/.git", 0), ]; - let new_repos: Vec = vec![ + let new_repos: Vec = vec![ fake_entry("/a/.git", 1), fake_entry("/a/b/.git", 0), fake_entry("/a/c/.git", 0), diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 0778cc5294ceeb3b226bee676e236ec42efc9986..ab44aa9f09c21cd730295660ff99bb082817a919 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,13 +2,13 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, V: Clone + Debug; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct MapEntry { key: K, value: V, @@ -73,6 +73,42 @@ impl TreeMap { removed } + pub fn update(&mut self, key: &K, f: F) -> Option + where + F: FnOnce(&mut V) -> T, + { + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left, &()); + let mut result = None; + if key.cmp(&cursor.end(&()), &()) == Ordering::Equal { + let mut updated = cursor.item().unwrap().clone(); + result = Some(f(&mut updated.value)); + new_tree.push(updated, &()); + cursor.next(&()); + } + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + result + } + + pub fn retain bool>(&mut self, mut predicate: F) { + let mut cursor = self.0.cursor::>(); + cursor.seek(&MapKeyRef(None), Bias::Left, &()); + + let mut new_map = SumTree::>::default(); + if let Some(item) = cursor.item() { + if predicate(&item.key, &item.value) { + new_map.push(item.clone(), &()); + } + cursor.next(&()); + } + drop(cursor); + + self.0 = new_map; + } + pub fn iter(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| (&entry.key, &entry.value)) } From 563f13925fc7fc488788719f2fdf812a2e56a5f3 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 1 May 2023 16:29:14 -0700 Subject: [PATCH 02/34] WIP: Convert old git repository vec to new treemap based approach. co-authored-by: Nathan --- crates/project/src/project.rs | 16 ++-- crates/project/src/worktree.rs | 155 ++++++++++++-------------------- crates/sum_tree/src/tree_map.rs | 19 ++++ 3 files changed, 84 insertions(+), 106 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7eede73c75fd2f6e3905fd58abb9bde373b8541d..3d59aefde30c33c294f846286d3e026be0affcd2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -64,6 +64,7 @@ use std::{ }, time::{Duration, Instant, SystemTime}, }; +use sum_tree::TreeMap; use terminals::Terminals; use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; @@ -4696,7 +4697,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, worktree: ModelHandle, - repos: &[LocalGitRepositoryEntry], + repos: &TreeMap, cx: &mut ModelContext, ) { for (_, buffer) in &self.opened_buffers { @@ -4711,14 +4712,17 @@ impl Project { let path = file.path().clone(); - let repo = match repos.iter().find(|repo| repo.manages(&path)) { - Some(repo) => repo.clone(), + let (work_directory, repo) = match repos + .iter() + .find(|(work_directory, _)| work_directory.contains(&path)) + { + Some((work_directory, repo)) => (work_directory, repo.clone()), None => return, }; - let relative_repo = match path.strip_prefix(repo.content_path) { - Ok(relative_repo) => relative_repo.to_owned(), - Err(_) => return, + let relative_repo = match work_directory.relativize(&path) { + Some(relative_repo) => relative_repo.to_owned(), + None => return, }; let remote_id = self.remote_id(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2746bc95d225246ee5054f73b18f41fb4b967559..d49b71135b9021453dd3d9c16bda12546a387db8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -102,7 +102,7 @@ pub struct Snapshot { root_char_bag: CharBag, entries_by_path: SumTree, entries_by_id: SumTree, - repository_entries: TreeMap, + repository_entries: TreeMap, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused @@ -136,39 +136,28 @@ impl RepositoryEntry { /// This path corresponds to the 'content path' (the folder that contains the .git) #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct RepositoryEntryKey(Arc); +pub struct RepositoryWorkDirectory(Arc); -impl Default for RepositoryEntryKey { - fn default() -> Self { - RepositoryEntryKey(Arc::from(Path::new(""))) +impl RepositoryWorkDirectory { + // Note that these paths should be relative to the worktree root. + pub(crate) fn contains(&self, path: &Path) -> bool { + path.starts_with(self.0.as_ref()) } -} -#[derive(Clone)] -pub struct LocalGitRepositoryEntry { - pub(crate) repo: Arc>, - - pub(crate) scan_id: usize, - // Path to folder containing the .git file or directory - pub(crate) content_path: Arc, - // Path to the actual .git folder. - // Note: if .git is a file, this points to the folder indicated by the .git file - pub(crate) git_dir_path: Arc, + pub(crate) fn relativize(&self, path: &Path) -> Option<&Path> { + path.strip_prefix(self.0.as_ref()).ok() + } } -impl std::fmt::Debug for LocalGitRepositoryEntry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GitRepositoryEntry") - .field("content_path", &self.content_path) - .field("git_dir_path", &self.git_dir_path) - .finish() +impl Default for RepositoryWorkDirectory { + fn default() -> Self { + RepositoryWorkDirectory(Arc::from(Path::new(""))) } } #[derive(Debug, Clone)] pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories_old: Vec, // The ProjectEntryId corresponds to the entry for the .git dir git_repositories: TreeMap>>, removed_entry_ids: HashMap, @@ -209,7 +198,7 @@ struct ShareState { pub enum Event { UpdatedEntries(HashMap, PathChange>), - UpdatedGitRepositories(Vec), + UpdatedGitRepositories(Vec), } impl Entity for Worktree { @@ -240,7 +229,6 @@ impl Worktree { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - git_repositories_old: Default::default(), removed_entry_ids: Default::default(), git_repositories: Default::default(), next_entry_id, @@ -620,8 +608,8 @@ impl LocalWorktree { fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext) { let updated_repos = Self::changed_repos( - &self.snapshot.git_repositories_old, - &new_snapshot.git_repositories_old, + &self.snapshot.repository_entries, + &new_snapshot.repository_entries, ); self.snapshot = new_snapshot; @@ -635,16 +623,16 @@ impl LocalWorktree { } fn changed_repos( - old_repos: &[LocalGitRepositoryEntry], - new_repos: &[LocalGitRepositoryEntry], - ) -> Vec { + old_repos: &TreeMap, + new_repos: &TreeMap, + ) -> Vec { fn diff<'a>( - a: &'a [LocalGitRepositoryEntry], - b: &'a [LocalGitRepositoryEntry], - updated: &mut HashMap<&'a Path, LocalGitRepositoryEntry>, + a: impl Iterator, + b: impl Iterator, + updated: &mut HashMap<&'a Path, RepositoryEntry>, ) { for a_repo in a { - let matched = b.iter().find(|b_repo| { + let matched = b.find(|b_repo| { a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id }); @@ -654,10 +642,10 @@ impl LocalWorktree { } } - let mut updated = HashMap::<&Path, LocalGitRepositoryEntry>::default(); + let mut updated = HashMap::<&Path, RepositoryEntry>::default(); - diff(old_repos, new_repos, &mut updated); - diff(new_repos, old_repos, &mut updated); + diff(old_repos.values(), new_repos.values(), &mut updated); + diff(new_repos.values(), old_repos.values(), &mut updated); updated.into_values().collect() } @@ -1023,7 +1011,6 @@ impl LocalWorktree { let mut share_tx = Some(share_tx); let mut prev_snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - git_repositories_old: Default::default(), removed_entry_ids: Default::default(), next_entry_id: Default::default(), git_repositories: Default::default(), @@ -1432,18 +1419,16 @@ impl Snapshot { impl LocalSnapshot { // Gives the most specific git repository for a given path - pub(crate) fn repo_for(&self, path: &Path) -> Option { - self.git_repositories_old - .iter() - .rev() //git_repository is ordered lexicographically - .find(|repo| repo.manages(path)) - .cloned() + pub(crate) fn repo_for(&self, path: &Path) -> Option { + self.repository_entries + .closest(&RepositoryWorkDirectory(path.into())) + .map(|(_, entry)| entry.to_owned()) } pub(crate) fn repo_with_dot_git_containing( &mut self, path: &Path, - ) -> Option<&mut LocalGitRepositoryEntry> { + ) -> Option<&mut RepositoryEntry> { // Git repositories cannot be nested, so we don't need to reverse the order self.git_repositories_old .iter_mut() @@ -1621,7 +1606,7 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&parent_path); let content_path: Arc = parent_path.parent().unwrap().into(); - let key = RepositoryEntryKey(content_path.clone()); + let key = RepositoryWorkDirectory(content_path.clone()); if self.repository_entries.get(&key).is_none() { if let Some(repo) = fs.open_repo(abs_path.as_path()) { self.repository_entries.insert( @@ -1636,23 +1621,6 @@ impl LocalSnapshot { self.git_repositories.insert(parent_entry.id, repo) } } - - if let Err(ix) = self - .git_repositories_old - .binary_search_by_key(&&content_path, |repo| &repo.content_path) - { - if let Some(repo) = fs.open_repo(abs_path.as_path()) { - self.git_repositories_old.insert( - ix, - LocalGitRepositoryEntry { - repo, - scan_id: 0, - content_path, - git_dir_path: parent_path, - }, - ); - } - } } let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; @@ -1712,18 +1680,10 @@ impl LocalSnapshot { *scan_id = self.snapshot.scan_id; } } else if path.file_name() == Some(&DOT_GIT) { - let repo_entry_key = RepositoryEntryKey(path.parent().unwrap().into()); + let repo_entry_key = RepositoryWorkDirectory(path.parent().unwrap().into()); self.snapshot .repository_entries .update(&repo_entry_key, |repo| repo.scan_id = self.snapshot.scan_id); - - let parent_path = path.parent().unwrap(); - if let Ok(ix) = self - .git_repositories_old - .binary_search_by_key(&parent_path, |repo| repo.content_path.as_ref()) - { - self.git_repositories_old[ix].scan_id = self.snapshot.scan_id; - } } } @@ -1763,22 +1723,6 @@ impl LocalSnapshot { ignore_stack } - - pub fn git_repo_entries(&self) -> &[LocalGitRepositoryEntry] { - &self.git_repositories_old - } -} - -impl LocalGitRepositoryEntry { - // Note that these paths should be relative to the worktree root. - pub(crate) fn manages(&self, path: &Path) -> bool { - path.starts_with(self.content_path.as_ref()) - } - - // Note that this path should be relative to the worktree root. - pub(crate) fn in_dot_git(&self, path: &Path) -> bool { - path.starts_with(self.git_dir_path.as_ref()) - } } async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { @@ -2364,10 +2308,6 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock(); - let mut git_repositories = mem::take(&mut snapshot.git_repositories_old); - git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some()); - snapshot.git_repositories_old = git_repositories; - let mut git_repositories = mem::take(&mut snapshot.git_repositories); git_repositories.retain(|project_entry_id, _| snapshot.contains_entry(*project_entry_id)); snapshot.git_repositories = git_repositories; @@ -3489,26 +3429,41 @@ mod tests { #[test] fn test_changed_repos() { - fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> LocalGitRepositoryEntry { - LocalGitRepositoryEntry { - repo: Arc::new(Mutex::new(FakeGitRepository::default())), + fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> RepositoryEntry { + RepositoryEntry { scan_id, - content_path: git_dir_path.as_ref().parent().unwrap().into(), git_dir_path: git_dir_path.as_ref().into(), + git_dir_entry_id: ProjectEntryId(0), } } - let prev_repos: Vec = vec![ + let mut prev_repos = TreeMap::::default(); + prev_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-1").into()), fake_entry("/.git", 0), + ); + prev_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-2").into()), fake_entry("/a/.git", 0), + ); + prev_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-3").into()), fake_entry("/a/b/.git", 0), - ]; + ); - let new_repos: Vec = vec![ + let mut new_repos = TreeMap::::default(); + new_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-4").into()), fake_entry("/a/.git", 1), + ); + new_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-5").into()), fake_entry("/a/b/.git", 0), + ); + new_repos.insert( + RepositoryWorkDirectory(Path::new("don't-care-6").into()), fake_entry("/a/c/.git", 0), - ]; + ); let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index ab44aa9f09c21cd730295660ff99bb082817a919..2580b08783b4e69d0228d0f0c2662a4259f347a0 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -73,6 +73,15 @@ impl TreeMap { removed } + /// Returns the key-value pair with the greatest key less than or equal to the given key. + pub fn closest(&self, key: &K) -> Option<(&K, &V)> { + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + cursor.seek(&key, Bias::Right, &()); + cursor.prev(&()); + cursor.item().map(|item| (&item.key, &item.value)) + } + pub fn update(&mut self, key: &K, f: F) -> Option where F: FnOnce(&mut V) -> T, @@ -112,6 +121,10 @@ impl TreeMap { pub fn iter(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| (&entry.key, &entry.value)) } + + pub fn values(&self) -> impl Iterator + '_ { + self.0.iter().map(|entry| &entry.value) + } } impl Default for TreeMap @@ -235,10 +248,16 @@ mod tests { vec![(&1, &"a"), (&2, &"b"), (&3, &"c")] ); + assert_eq!(map.closest(&0), None); + assert_eq!(map.closest(&1), Some((&1, &"a"))); + assert_eq!(map.closest(&10), Some((&3, &"c"))); + map.remove(&2); assert_eq!(map.get(&2), None); assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); + assert_eq!(map.closest(&2), Some((&1, &"a"))); + map.remove(&3); assert_eq!(map.get(&3), None); assert_eq!(map.iter().collect::>(), vec![(&1, &"a")]); From bcf608e9e9c9f527f9d10e00802ea699ad842975 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 2 May 2023 10:22:47 -0700 Subject: [PATCH 03/34] WIP: Refactor existing git code to use new representation. co-authored-by: petros --- crates/project/src/project.rs | 23 +++--- crates/project/src/worktree.rs | 145 +++++++++++++++++++++++---------- 2 files changed, 117 insertions(+), 51 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3d59aefde30c33c294f846286d3e026be0affcd2..e8ee80262ffe4d1f0a7b8db2736a6c64bc79a5c1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -64,7 +64,7 @@ use std::{ }, time::{Duration, Instant, SystemTime}, }; -use sum_tree::TreeMap; + use terminals::Terminals; use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; @@ -4697,7 +4697,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, worktree: ModelHandle, - repos: &TreeMap, + repos: &Vec, cx: &mut ModelContext, ) { for (_, buffer) in &self.opened_buffers { @@ -4712,27 +4712,30 @@ impl Project { let path = file.path().clone(); - let (work_directory, repo) = match repos + let repo = match repos .iter() - .find(|(work_directory, _)| work_directory.contains(&path)) + .find(|entry| entry.work_directory.contains(&path)) { - Some((work_directory, repo)) => (work_directory, repo.clone()), + Some(repo) => repo.clone(), None => return, }; - let relative_repo = match work_directory.relativize(&path) { + let relative_repo = match repo.work_directory.relativize(&path) { Some(relative_repo) => relative_repo.to_owned(), None => return, }; let remote_id = self.remote_id(); let client = self.client.clone(); + let diff_base_task = worktree.update(cx, move |worktree, cx| { + worktree + .as_local() + .unwrap() + .load_index_text(repo, relative_repo, cx) + }); cx.spawn(|_, mut cx| async move { - let diff_base = cx - .background() - .spawn(async move { repo.repo.lock().load_index_text(&relative_repo) }) - .await; + let diff_base = diff_base_task.await; let buffer_id = buffer.update(&mut cx, |buffer, cx| { buffer.set_diff_base(diff_base.clone(), cx); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d49b71135b9021453dd3d9c16bda12546a387db8..5f0ae5b4e06cc8f388251025467c4d6e189cb2a2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -123,6 +123,7 @@ pub struct RepositoryEntry { // Note: if .git is a file, this points to the folder indicated by the .git file pub(crate) git_dir_path: Arc, pub(crate) git_dir_entry_id: ProjectEntryId, + pub(crate) work_directory: RepositoryWorkDirectory, pub(crate) scan_id: usize, // TODO: pub(crate) head_ref: Arc, } @@ -144,8 +145,41 @@ impl RepositoryWorkDirectory { path.starts_with(self.0.as_ref()) } - pub(crate) fn relativize(&self, path: &Path) -> Option<&Path> { - path.strip_prefix(self.0.as_ref()).ok() + pub(crate) fn relativize(&self, path: &Path) -> Option { + path.strip_prefix(self.0.as_ref()) + .ok() + .map(move |path| RepoPath(path.to_owned())) + } +} + +impl Deref for RepositoryWorkDirectory { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct RepoPath(PathBuf); + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl Deref for RepoPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for RepositoryWorkDirectory { + fn as_ref(&self) -> &Path { + self.0.as_ref() } } @@ -628,7 +662,7 @@ impl LocalWorktree { ) -> Vec { fn diff<'a>( a: impl Iterator, - b: impl Iterator, + mut b: impl Iterator, updated: &mut HashMap<&'a Path, RepositoryEntry>, ) { for a_repo in a { @@ -691,11 +725,12 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let diff_base = if let Some(repo) = snapshot.repo_for(&path) { - if let Ok(repo_relative) = path.strip_prefix(repo.content_path) { + let diff_base = if let Some((work_directory, repo)) = snapshot.repo_for_metadata(&path) + { + if let Ok(repo_relative) = path.strip_prefix(&work_directory) { let repo_relative = repo_relative.to_owned(); cx.background() - .spawn(async move { repo.repo.lock().load_index_text(&repo_relative) }) + .spawn(async move { repo.lock().load_index_text(&repo_relative) }) .await } else { None @@ -1076,6 +1111,19 @@ impl LocalWorktree { pub fn is_shared(&self) -> bool { self.share.is_some() } + + pub fn load_index_text( + &self, + repo: RepositoryEntry, + repo_path: RepoPath, + cx: &mut ModelContext, + ) -> Task> { + let Some(git_ptr) = self.git_repositories.get(&repo.git_dir_entry_id).map(|git_ptr| git_ptr.to_owned()) else { + return Task::Ready(Some(None)) + }; + cx.background() + .spawn(async move { git_ptr.lock().load_index_text(&repo_path) }) + } } impl RemoteWorktree { @@ -1418,21 +1466,22 @@ impl Snapshot { } impl LocalSnapshot { - // Gives the most specific git repository for a given path - pub(crate) fn repo_for(&self, path: &Path) -> Option { - self.repository_entries - .closest(&RepositoryWorkDirectory(path.into())) - .map(|(_, entry)| entry.to_owned()) - } - - pub(crate) fn repo_with_dot_git_containing( - &mut self, + pub(crate) fn repo_for_metadata( + &self, path: &Path, - ) -> Option<&mut RepositoryEntry> { - // Git repositories cannot be nested, so we don't need to reverse the order - self.git_repositories_old - .iter_mut() - .find(|repo| repo.in_dot_git(path)) + ) -> Option<(RepositoryWorkDirectory, Arc>)> { + self.repository_entries + .iter() + .find(|(_, repo)| repo.in_dot_git(path)) + .map(|(work_directory, entry)| { + ( + work_directory.to_owned(), + self.git_repositories + .get(&entry.git_dir_entry_id) + .expect("These two data structures should be in sync") + .to_owned(), + ) + }) } #[cfg(test)] @@ -1610,10 +1659,11 @@ impl LocalSnapshot { if self.repository_entries.get(&key).is_none() { if let Some(repo) = fs.open_repo(abs_path.as_path()) { self.repository_entries.insert( - key, + key.clone(), RepositoryEntry { git_dir_path: parent_path.clone(), git_dir_entry_id: parent_entry.id, + work_directory: key, scan_id: 0, }, ); @@ -2599,16 +2649,10 @@ impl BackgroundScanner { let scan_id = snapshot.scan_id; - if let Some(repo) = snapshot.repo_with_dot_git_containing(&path) { - repo.repo.lock().reload_index(); - repo.scan_id = scan_id; - } + let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); + if let Some((key, repo)) = repo_with_path_in_dotgit { + repo.lock().reload_index(); - let repo_with_path_in_dotgit = snapshot - .repository_entries - .iter() - .find_map(|(key, repo)| repo.in_dot_git(&path).then(|| key.clone())); - if let Some(key) = repo_with_path_in_dotgit { snapshot .repository_entries .update(&key, |entry| entry.scan_id = scan_id); @@ -3123,7 +3167,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { #[cfg(test)] mod tests { use super::*; - use fs::repository::FakeGitRepository; use fs::{FakeFs, RealFs}; use gpui::{executor::Deterministic, TestAppContext}; use pretty_assertions::assert_eq; @@ -3386,23 +3429,37 @@ mod tests { .await; tree.flush_fs_events(cx).await; + fn entry(key: &RepositoryWorkDirectory, tree: &LocalWorktree) -> RepositoryEntry { + tree.repository_entries.get(key).unwrap().to_owned() + } + tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree.repo_for("c.txt".as_ref()).is_none()); + assert!(tree.repo_for_metadata("c.txt".as_ref()).is_none()); - let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!(repo.content_path.as_ref(), Path::new("dir1")); - assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git")); + let (work_directory, _repo_entry) = + tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); + assert_eq!(work_directory.0.as_ref(), Path::new("dir1")); + assert_eq!( + entry(&work_directory, &tree).git_dir_path.as_ref(), + Path::new("dir1/.git") + ); - let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); - assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); - assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),); + let _repo = tree + .repo_for_metadata("dir1/deps/dep1/src/a.txt".as_ref()) + .unwrap(); + assert_eq!(work_directory.deref(), Path::new("dir1/deps/dep1")); + assert_eq!( + entry(&work_directory, &tree).git_dir_path.as_ref(), + Path::new("dir1/deps/dep1/.git"), + ); }); let original_scan_id = tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id + let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); + entry(&key, &tree).scan_id }); std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); @@ -3410,7 +3467,10 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id; + let new_scan_id = { + let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); + entry(&key, &tree).scan_id + }; assert_ne!( original_scan_id, new_scan_id, "original {original_scan_id}, new {new_scan_id}" @@ -3423,7 +3483,7 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none()); + assert!(tree.repo_for_metadata("dir1/src/b.txt".as_ref()).is_none()); }); } @@ -3434,6 +3494,9 @@ mod tests { scan_id, git_dir_path: git_dir_path.as_ref().into(), git_dir_entry_id: ProjectEntryId(0), + work_directory: RepositoryWorkDirectory( + Path::new(&format!("don't-care-{}", scan_id)).into(), + ), } } From ae890212e39c104dca9fc1ddd891e902c7f8703d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 2 May 2023 16:12:51 -0700 Subject: [PATCH 04/34] Restored a lost API and got everything compiling --- crates/project/src/project.rs | 2 ++ crates/project/src/worktree.rs | 65 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e8ee80262ffe4d1f0a7b8db2736a6c64bc79a5c1..3971eafbddb36e0d5e2561420e36237cfa121364 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4700,6 +4700,8 @@ impl Project { repos: &Vec, cx: &mut ModelContext, ) { + debug_assert!(worktree.read(cx).is_local()); + for (_, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { let file = match File::from_dyn(buffer.read(cx).file()) { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5f0ae5b4e06cc8f388251025467c4d6e189cb2a2..dcece4f484ee4da785d888eefeebd27ca7d8b035 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -722,19 +722,24 @@ impl LocalWorktree { let fs = self.fs.clone(); let snapshot = self.snapshot(); + let mut index_task = None; + + if let Some(repo) = snapshot.repo_for(&path) { + let repo_path = repo.work_directory.relativize(&path).unwrap(); + if let Some(repo) = self.git_repositories.get(&repo.git_dir_entry_id) { + let repo = repo.to_owned(); + index_task = Some( + cx.background() + .spawn(async move { repo.lock().load_index_text(&repo_path) }), + ); + } + } + cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let diff_base = if let Some((work_directory, repo)) = snapshot.repo_for_metadata(&path) - { - if let Ok(repo_relative) = path.strip_prefix(&work_directory) { - let repo_relative = repo_relative.to_owned(); - cx.background() - .spawn(async move { repo.lock().load_index_text(&repo_relative) }) - .await - } else { - None - } + let diff_base = if let Some(index_task) = index_task { + index_task.await } else { None }; @@ -1466,6 +1471,12 @@ impl Snapshot { } impl LocalSnapshot { + pub(crate) fn repo_for(&self, path: &Path) -> Option { + dbg!(&self.repository_entries) + .closest(&RepositoryWorkDirectory(path.into())) + .map(|(_, entry)| entry.to_owned()) + } + pub(crate) fn repo_for_metadata( &self, path: &Path, @@ -3429,37 +3440,27 @@ mod tests { .await; tree.flush_fs_events(cx).await; - fn entry(key: &RepositoryWorkDirectory, tree: &LocalWorktree) -> RepositoryEntry { - tree.repository_entries.get(key).unwrap().to_owned() - } - tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree.repo_for_metadata("c.txt".as_ref()).is_none()); + assert!(tree.repo_for("c.txt".as_ref()).is_none()); - let (work_directory, _repo_entry) = - tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!(work_directory.0.as_ref(), Path::new("dir1")); - assert_eq!( - entry(&work_directory, &tree).git_dir_path.as_ref(), - Path::new("dir1/.git") - ); + let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); + assert_eq!(entry.work_directory.0.as_ref(), Path::new("dir1")); + assert_eq!(entry.git_dir_path.as_ref(), Path::new("dir1/.git")); - let _repo = tree - .repo_for_metadata("dir1/deps/dep1/src/a.txt".as_ref()) - .unwrap(); - assert_eq!(work_directory.deref(), Path::new("dir1/deps/dep1")); + let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); + assert_eq!(entry.work_directory.deref(), Path::new("dir1/deps/dep1")); assert_eq!( - entry(&work_directory, &tree).git_dir_path.as_ref(), + entry.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"), ); }); let original_scan_id = tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); - entry(&key, &tree).scan_id + let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); + entry.scan_id }); std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); @@ -3468,8 +3469,8 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); let new_scan_id = { - let (key, _) = tree.repo_for_metadata("dir1/src/b.txt".as_ref()).unwrap(); - entry(&key, &tree).scan_id + let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); + entry.scan_id }; assert_ne!( original_scan_id, new_scan_id, @@ -3483,7 +3484,7 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree.repo_for_metadata("dir1/src/b.txt".as_ref()).is_none()); + assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none()); }); } From 023d665fb386bdd69f052a8a1fe96b4d30fd918b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 08:39:25 -0700 Subject: [PATCH 05/34] Fix TreeMap retain --- crates/sum_tree/src/tree_map.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 2580b08783b4e69d0228d0f0c2662a4259f347a0..1b97cbec9fee02819fa926febb14531b4bbd611f 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -103,11 +103,11 @@ impl TreeMap { } pub fn retain bool>(&mut self, mut predicate: F) { - let mut cursor = self.0.cursor::>(); - cursor.seek(&MapKeyRef(None), Bias::Left, &()); - let mut new_map = SumTree::>::default(); - if let Some(item) = cursor.item() { + + let mut cursor = self.0.cursor::>(); + cursor.next(&()); + while let Some(item) = cursor.item() { if predicate(&item.key, &item.value) { new_map.push(item.clone(), &()); } @@ -265,5 +265,11 @@ mod tests { map.remove(&1); assert_eq!(map.get(&1), None); assert_eq!(map.iter().collect::>(), vec![]); + + map.insert(4, "d"); + map.insert(5, "e"); + map.insert(6, "f"); + map.retain(|key, _| *key % 2 == 0); + assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); } } From 5b4e58d1de35b92e55931f52283e0c05bac6bca1 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 08:40:22 -0700 Subject: [PATCH 06/34] Fix repo_for and clean up repository_entries --- crates/project/src/worktree.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index dcece4f484ee4da785d888eefeebd27ca7d8b035..e197e0ffad50577a4db3fbf96114a39af07735a6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1472,9 +1472,20 @@ impl Snapshot { impl LocalSnapshot { pub(crate) fn repo_for(&self, path: &Path) -> Option { - dbg!(&self.repository_entries) - .closest(&RepositoryWorkDirectory(path.into())) - .map(|(_, entry)| entry.to_owned()) + let mut max_len = 0; + let mut current_candidate = None; + for (work_directory, repo) in (&self.repository_entries).iter() { + if work_directory.contains(path) { + if work_directory.0.as_os_str().len() > max_len { + current_candidate = Some(repo); + max_len = work_directory.0.as_os_str().len(); + } else { + break; + } + } + } + + current_candidate.map(|entry| entry.to_owned()) } pub(crate) fn repo_for_metadata( @@ -2373,8 +2384,13 @@ impl BackgroundScanner { git_repositories.retain(|project_entry_id, _| snapshot.contains_entry(*project_entry_id)); snapshot.git_repositories = git_repositories; + let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries); + git_repository_entries.retain(|_, entry| snapshot.contains_entry(entry.git_dir_entry_id)); + snapshot.snapshot.repository_entries = git_repository_entries; + snapshot.removed_entry_ids.clear(); snapshot.completed_scan_id = snapshot.scan_id; + drop(snapshot); self.send_status_update(false, None); From 26afd592c5790de57582700f1c418977ba69c256 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 08:51:58 -0700 Subject: [PATCH 07/34] Wire in the branch name --- crates/fs/src/repository.rs | 13 +++++++++++++ crates/project/src/worktree.rs | 15 ++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 75f9ca34dde98c445add2b84056c3eda583d6a88..d22d670a8bd874f37d3a22bcb2208bc27bca4ee7 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -5,6 +5,7 @@ use std::{ path::{Component, Path, PathBuf}, sync::Arc, }; +use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -13,6 +14,8 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; + + fn branch_name(&self) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -52,6 +55,12 @@ impl GitRepository for LibGitRepository { } None } + + fn branch_name(&self) -> Option { + let head = self.head().log_err()?; + let branch = String::from_utf8_lossy(head.shorthand_bytes()); + Some(branch.to_string()) + } } #[derive(Debug, Clone, Default)] @@ -78,6 +87,10 @@ impl GitRepository for FakeGitRepository { let state = self.state.lock(); state.index_contents.get(path).cloned() } + + fn branch_name(&self) -> Option { + None + } } fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e197e0ffad50577a4db3fbf96114a39af07735a6..d663fc8b1145038cf05a6a50f4e3f2ef63cfc98f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -125,7 +125,7 @@ pub struct RepositoryEntry { pub(crate) git_dir_entry_id: ProjectEntryId, pub(crate) work_directory: RepositoryWorkDirectory, pub(crate) scan_id: usize, - // TODO: pub(crate) head_ref: Arc, + pub(crate) branch: Option>, } impl RepositoryEntry { @@ -1687,6 +1687,7 @@ impl LocalSnapshot { git_dir_entry_id: parent_entry.id, work_directory: key, scan_id: 0, + branch: None, }, ); @@ -2678,11 +2679,14 @@ impl BackgroundScanner { let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); if let Some((key, repo)) = repo_with_path_in_dotgit { - repo.lock().reload_index(); + let repo = repo.lock(); + repo.reload_index(); + let branch = repo.branch_name(); - snapshot - .repository_entries - .update(&key, |entry| entry.scan_id = scan_id); + snapshot.repository_entries.update(&key, |entry| { + entry.scan_id = scan_id; + entry.branch = branch.map(Into::into) + }); } if let Some(scan_queue_tx) = &scan_queue_tx { @@ -3514,6 +3518,7 @@ mod tests { work_directory: RepositoryWorkDirectory( Path::new(&format!("don't-care-{}", scan_id)).into(), ), + branch: None, } } From 35708105168bacadc8b94179b0b3356ed9548adb Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 09:09:09 -0700 Subject: [PATCH 08/34] Add API for accessing git branch --- crates/gpui/src/elements/flex.rs | 4 ++++ crates/project/src/worktree.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index e0e8dfc215069b892fbfc1cd25d640fbdb7f18e2..857f3f56fc08b0b24f39011d7f4323838b97dde2 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -66,6 +66,10 @@ impl Flex { self } + pub fn is_empty(&self) -> bool { + self.children.is_empty() + } + fn layout_flex_children( &mut self, layout_expanded: bool, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d663fc8b1145038cf05a6a50f4e3f2ef63cfc98f..fccb382ca52ca9f87729c428798e6c843b034ef2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -133,6 +133,10 @@ impl RepositoryEntry { pub(crate) fn in_dot_git(&self, path: &Path) -> bool { path.starts_with(self.git_dir_path.as_ref()) } + + pub fn branch(&self) -> Option> { + self.branch.clone() + } } /// This path corresponds to the 'content path' (the folder that contains the .git) @@ -160,6 +164,12 @@ impl Deref for RepositoryWorkDirectory { } } +impl<'a> From<&'a str> for RepositoryWorkDirectory { + fn from(value: &'a str) -> Self { + RepositoryWorkDirectory(Path::new(value).into()) + } +} + #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct RepoPath(PathBuf); @@ -1443,6 +1453,12 @@ impl Snapshot { &self.root_name } + pub fn root_git_entry(&self) -> Option { + self.repository_entries + .get(&"".into()) + .map(|entry| entry.to_owned()) + } + pub fn scan_id(&self) -> usize { self.scan_id } From ffd9d4eb59a7ca794145a3ec241e47ad4042f1fa Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 10:34:29 -0700 Subject: [PATCH 09/34] Fix bug in repo detection --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fccb382ca52ca9f87729c428798e6c843b034ef2..6a8de46f8f4ee22f64ad5379a14980888c6cb311 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1492,7 +1492,7 @@ impl LocalSnapshot { let mut current_candidate = None; for (work_directory, repo) in (&self.repository_entries).iter() { if work_directory.contains(path) { - if work_directory.0.as_os_str().len() > max_len { + if work_directory.0.as_os_str().len() >= max_len { current_candidate = Some(repo); max_len = work_directory.0.as_os_str().len(); } else { From d34ec462f85b536bf94f5674a97529af9de6fcc1 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Wed, 3 May 2023 20:41:44 +0300 Subject: [PATCH 10/34] Display branch information per worktree root Co-Authored-By: Mikayla Maki --- crates/collab_ui/src/collab_titlebar_item.rs | 69 ++++++++++++++------ crates/project/src/worktree.rs | 4 ++ 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 69ca64360cbe5363f6ea9811eb8f43f6f1c63a97..de9f9615d52f8acf5178e287dbe1cd62a9456ec6 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -17,7 +17,7 @@ use gpui::{ AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::Project; +use project::{Project, Worktree}; use settings::Settings; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; @@ -68,29 +68,17 @@ impl View for CollabTitlebarItem { }; let project = self.project.read(cx); - let mut project_title = String::new(); - for (i, name) in project.worktree_root_names(cx).enumerate() { - if i > 0 { - project_title.push_str(", "); - } - project_title.push_str(name); - } - if project_title.is_empty() { - project_title = "empty project".to_owned(); - } - + let project_title = self.prepare_title(&project, cx); let theme = cx.global::().theme.clone(); let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); - left_container.add_child( - Label::new(project_title, theme.workspace.titlebar.title.clone()) - .contained() - .with_margin_right(theme.workspace.titlebar.item_spacing) - .aligned() - .left(), - ); + left_container.add_child(self.render_title_with_information( + project, + &project_title, + theme.clone(), + )); let user = self.user_store.read(cx).current_user(); let peer_id = self.client.peer_id(); @@ -181,6 +169,49 @@ impl CollabTitlebarItem { } } + fn decorate_with_git_branch( + &self, + worktree: &ModelHandle, + cx: &ViewContext, + ) -> String { + let name = worktree.read(cx).root_name(); + let branch = worktree + .read(cx) + .snapshot() + .git_branch() + .unwrap_or_else(|| "".to_owned()); + format!("{} / {}", name, branch) + } + + fn prepare_title(&self, project: &Project, cx: &ViewContext) -> String { + let decorated_root_names: Vec = project + .visible_worktrees(cx) + .map(|worktree| self.decorate_with_git_branch(&worktree, cx)) + .collect(); + if decorated_root_names.is_empty() { + "empty project".to_owned() + } else { + decorated_root_names.join(", ") + } + } + + fn render_title_with_information( + &self, + _project: &Project, + title: &str, + theme: Arc, + ) -> AnyElement { + let text_style = theme.workspace.titlebar.title.clone(); + let item_spacing = theme.workspace.titlebar.item_spacing; + + Label::new(title.to_owned(), text_style) + .contained() + .with_margin_right(dbg!(item_spacing)) + .aligned() + .left() + .into_any_named("title-with-git-information") + } + fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { let project = if active { Some(self.project.clone()) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6a8de46f8f4ee22f64ad5379a14980888c6cb311..88886347bc1f045d94680b61e31b1a9b52a07216 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1484,6 +1484,10 @@ impl Snapshot { pub fn inode_for_path(&self, path: impl AsRef) -> Option { self.entry_for_path(path.as_ref()).map(|e| e.inode) } + + pub fn git_branch(&self) -> Option { + Some("test".to_owned()) + } } impl LocalSnapshot { From 8f0aa3c6d95ec807760d4e06b26055b000f8d388 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 May 2023 12:09:16 -0700 Subject: [PATCH 11/34] Add branch name into title --- crates/collab_ui/src/collab_titlebar_item.rs | 6 ++++-- crates/project/src/worktree.rs | 8 +++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index de9f9615d52f8acf5178e287dbe1cd62a9456ec6..41d0315ee32b635b99453a82974c30b3990815ef 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -178,7 +178,9 @@ impl CollabTitlebarItem { let branch = worktree .read(cx) .snapshot() - .git_branch() + .root_git_entry() + .and_then(|entry| entry.branch()) + .map(|branch| branch.to_string()) .unwrap_or_else(|| "".to_owned()); format!("{} / {}", name, branch) } @@ -206,7 +208,7 @@ impl CollabTitlebarItem { Label::new(title.to_owned(), text_style) .contained() - .with_margin_right(dbg!(item_spacing)) + .with_margin_right(item_spacing) .aligned() .left() .into_any_named("title-with-git-information") diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 88886347bc1f045d94680b61e31b1a9b52a07216..fed98d49edc8e55979617206c045ee46e37d2455 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1484,10 +1484,6 @@ impl Snapshot { pub fn inode_for_path(&self, path: impl AsRef) -> Option { self.entry_for_path(path.as_ref()).map(|e| e.inode) } - - pub fn git_branch(&self) -> Option { - Some("test".to_owned()) - } } impl LocalSnapshot { @@ -1700,6 +1696,7 @@ impl LocalSnapshot { let key = RepositoryWorkDirectory(content_path.clone()); if self.repository_entries.get(&key).is_none() { if let Some(repo) = fs.open_repo(abs_path.as_path()) { + let repo_lock = repo.lock(); self.repository_entries.insert( key.clone(), RepositoryEntry { @@ -1707,9 +1704,10 @@ impl LocalSnapshot { git_dir_entry_id: parent_entry.id, work_directory: key, scan_id: 0, - branch: None, + branch: repo_lock.branch_name().map(Into::into), }, ); + drop(repo_lock); self.git_repositories.insert(parent_entry.id, repo) } From 92a222aba87749edff84d8eb75d82f595f94e603 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 4 May 2023 12:54:21 +0300 Subject: [PATCH 12/34] Introduce a version control branch icon --- assets/icons/version_control_branch_12.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 assets/icons/version_control_branch_12.svg diff --git a/assets/icons/version_control_branch_12.svg b/assets/icons/version_control_branch_12.svg new file mode 100644 index 0000000000000000000000000000000000000000..3571874a898e6f1bc9dbfb162c81f8708610d5d9 --- /dev/null +++ b/assets/icons/version_control_branch_12.svg @@ -0,0 +1,3 @@ + + + From 797d47a08c00e4aefd871bd8e134285148240016 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 4 May 2023 20:27:22 +0300 Subject: [PATCH 13/34] Render title root names without branches --- crates/collab_ui/src/collab_titlebar_item.rs | 43 +++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 41d0315ee32b635b99453a82974c30b3990815ef..f8c68e640ab201fd08d6f1e26f594f063abb2d32 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,6 @@ use crate::{ contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, - toggle_screen_sharing, ToggleScreenSharing, + toggle_screen_sharing, BranchesButton, ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; @@ -48,6 +48,7 @@ pub struct CollabTitlebarItem { workspace: WeakViewHandle, contacts_popover: Option>, user_menu: ViewHandle, + branches: ViewHandle, _subscriptions: Vec, } @@ -68,17 +69,14 @@ impl View for CollabTitlebarItem { }; let project = self.project.read(cx); - let project_title = self.prepare_title(&project, cx); let theme = cx.global::().theme.clone(); - let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); - left_container.add_child(self.render_title_with_information( - project, - &project_title, - theme.clone(), - )); + let project_title = self.collect_title_root_names(&project, cx); + left_container.add_child(self.render_title_root_names(&project_title, theme.clone())); + + left_container.add_child(ChildView::new(&self.branches.clone().into_any(), cx)); let user = self.user_store.read(cx).current_user(); let peer_id = self.client.peer_id(); @@ -165,15 +163,16 @@ impl CollabTitlebarItem { menu.set_position_mode(OverlayPositionMode::Local); menu }), + branches: cx.add_view(|cx| BranchesButton::new(workspace_handle.to_owned(), cx)), _subscriptions: subscriptions, } } - fn decorate_with_git_branch( + fn root_name_with_branch( &self, worktree: &ModelHandle, cx: &ViewContext, - ) -> String { + ) -> (String, String) { let name = worktree.read(cx).root_name(); let branch = worktree .read(cx) @@ -182,14 +181,23 @@ impl CollabTitlebarItem { .and_then(|entry| entry.branch()) .map(|branch| branch.to_string()) .unwrap_or_else(|| "".to_owned()); - format!("{} / {}", name, branch) + (name.to_owned(), branch) } - fn prepare_title(&self, project: &Project, cx: &ViewContext) -> String { - let decorated_root_names: Vec = project + fn collect_root_names_with_branches( + &self, + project: &Project, + cx: &ViewContext, + ) -> Vec<(String, String)> { + let root_names_with_branches: Vec<(String, String)> = project .visible_worktrees(cx) - .map(|worktree| self.decorate_with_git_branch(&worktree, cx)) + .map(|worktree| self.root_name_with_branch(&worktree, cx)) .collect(); + root_names_with_branches + } + + fn collect_title_root_names(&self, project: &Project, cx: &ViewContext) -> String { + let decorated_root_names: Vec<&str> = project.worktree_root_names(cx).collect(); if decorated_root_names.is_empty() { "empty project".to_owned() } else { @@ -197,12 +205,7 @@ impl CollabTitlebarItem { } } - fn render_title_with_information( - &self, - _project: &Project, - title: &str, - theme: Arc, - ) -> AnyElement { + fn render_title_root_names(&self, title: &str, theme: Arc) -> AnyElement { let text_style = theme.workspace.titlebar.title.clone(); let item_spacing = theme.workspace.titlebar.item_spacing; From e057b0193f32c12621d2a0826e9a59dfce499a73 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 4 May 2023 20:27:54 +0300 Subject: [PATCH 14/34] Introduce BrancesButton in title bar Co-Authored-By: Mikayla Maki --- crates/collab_ui/src/branches_button.rs | 165 ++++++++++++++++++++++++ crates/collab_ui/src/collab_ui.rs | 2 + crates/project/src/worktree.rs | 8 ++ 3 files changed, 175 insertions(+) create mode 100644 crates/collab_ui/src/branches_button.rs diff --git a/crates/collab_ui/src/branches_button.rs b/crates/collab_ui/src/branches_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..7acc7a8ed3210fec9298c108645048edf86b20b0 --- /dev/null +++ b/crates/collab_ui/src/branches_button.rs @@ -0,0 +1,165 @@ +use context_menu::{ContextMenu, ContextMenuItem}; +use gpui::{ + elements::*, + platform::{CursorStyle, MouseButton}, + AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use settings::Settings; +use workspace::Workspace; + +pub struct BranchesButton { + workspace: WeakViewHandle, + popup_menu: ViewHandle, +} + +impl Entity for BranchesButton { + type Event = (); +} + +impl View for BranchesButton { + fn ui_name() -> &'static str { + "BranchesButton" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let Some(workspace) = self.workspace.upgrade(cx) else { + return Empty::new().into_any(); + }; + + let project = workspace.read(cx).project().read(cx); + let only_one_worktree = project.visible_worktrees(cx).count() == 1; + let branches_count: usize = project + .visible_worktrees(cx) + .map(|worktree_handle| worktree_handle.read(cx).snapshot().git_entries().count()) + .sum(); + let branch_caption: String = if only_one_worktree { + project + .visible_worktrees(cx) + .next() + .unwrap() + .read(cx) + .snapshot() + .root_git_entry() + .and_then(|entry| entry.branch()) + .map(|branch| branch.to_string()) + .unwrap_or_else(|| "".to_owned()) + } else { + branches_count.to_string() + }; + let is_popup_menu_visible = self.popup_menu.read(cx).visible(); + + let theme = cx.global::().theme.clone(); + + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + move |state, _cx| { + let style = theme + .workspace + .titlebar + .toggle_contacts_button + .style_for(state, is_popup_menu_visible); + + Flex::row() + .with_child( + Svg::new("icons/version_control_branch_12.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + // .constrained() + // .with_width(style.button_width) + // .with_height(style.button_width) + // .contained() + // .with_style(style.container) + .into_any_named("version-control-branch-icon"), + ) + .with_child( + Label::new(branch_caption, theme.workspace.titlebar.title.clone()) + .contained() + .with_style(style.container) + .aligned(), + ) + .constrained() + .with_height(style.button_width) + .contained() + .with_style(style.container) + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.deploy_branches_menu(cx); + }) + .with_tooltip::( + 0, + "Branches".into(), + None, + theme.tooltip.clone(), + cx, + ), + ) + .with_child( + ChildView::new(&self.popup_menu, cx) + .aligned() + .bottom() + .left(), + ) + .into_any_named("branches-button") + } +} + +impl BranchesButton { + pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { + cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); + Self { + workspace: workspace.downgrade(), + popup_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), + } + } + + pub fn deploy_branches_menu(&mut self, cx: &mut ViewContext) { + let mut menu_options = vec![]; + + if let Some(workspace) = self.workspace.upgrade(cx) { + let project = workspace.read(cx).project().read(cx); + + let worktrees_with_branches = project + .visible_worktrees(cx) + .map(|worktree_handle| { + worktree_handle + .read(cx) + .snapshot() + .git_entries() + .filter_map(|entry| { + entry.branch().map(|branch| { + let repo_name = entry.work_directory(); + if let Some(name) = repo_name.file_name() { + (name.to_string_lossy().to_string(), branch) + } else { + ("WORKTREE ROOT".into(), branch) + } + }) + }) + .collect::>() + }) + .flatten(); + + let context_menu_items = worktrees_with_branches.map(|(repo_name, branch_name)| { + let caption = format!("{} / {}", repo_name, branch_name); + ContextMenuItem::handler(caption.to_owned(), move |_| { + println!("{}", caption); + }) + }); + menu_options.extend(context_menu_items); + } + + self.popup_menu.update(cx, |menu, cx| { + menu.show(Default::default(), AnchorCorner::TopLeft, menu_options, cx); + }); + } +} diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index c0734388b1512b2e9cac5014c460bf1a8c09650b..21a864d4e8943f6b8e7c00d30e9f9dc5358a5ffb 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +mod branches_button; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -9,6 +10,7 @@ mod notifications; mod project_shared_notification; mod sharing_status_indicator; +pub use branches_button::BranchesButton; use call::ActiveCall; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; use gpui::{actions, AppContext, Task}; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fed98d49edc8e55979617206c045ee46e37d2455..b6de263d2a3d8b3e53c82d439b488c3faeeb20da 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -137,6 +137,10 @@ impl RepositoryEntry { pub fn branch(&self) -> Option> { self.branch.clone() } + + pub fn work_directory(&self) -> Arc { + self.work_directory.0.clone() + } } /// This path corresponds to the 'content path' (the folder that contains the .git) @@ -1459,6 +1463,10 @@ impl Snapshot { .map(|entry| entry.to_owned()) } + pub fn git_entries(&self) -> impl Iterator { + self.repository_entries.values() + } + pub fn scan_id(&self) -> usize { self.scan_id } From ca4da52e3970abf86d334e70b25e3d4fb4aa4167 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 4 May 2023 20:28:45 +0300 Subject: [PATCH 15/34] Remove unused functions --- crates/collab_ui/src/collab_titlebar_item.rs | 30 +------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f8c68e640ab201fd08d6f1e26f594f063abb2d32..a435a15130eef4db024e7a371fef56ffb4bebc44 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -17,7 +17,7 @@ use gpui::{ AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Project, Worktree}; +use project::Project; use settings::Settings; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; @@ -168,34 +168,6 @@ impl CollabTitlebarItem { } } - fn root_name_with_branch( - &self, - worktree: &ModelHandle, - cx: &ViewContext, - ) -> (String, String) { - let name = worktree.read(cx).root_name(); - let branch = worktree - .read(cx) - .snapshot() - .root_git_entry() - .and_then(|entry| entry.branch()) - .map(|branch| branch.to_string()) - .unwrap_or_else(|| "".to_owned()); - (name.to_owned(), branch) - } - - fn collect_root_names_with_branches( - &self, - project: &Project, - cx: &ViewContext, - ) -> Vec<(String, String)> { - let root_names_with_branches: Vec<(String, String)> = project - .visible_worktrees(cx) - .map(|worktree| self.root_name_with_branch(&worktree, cx)) - .collect(); - root_names_with_branches - } - fn collect_title_root_names(&self, project: &Project, cx: &ViewContext) -> String { let decorated_root_names: Vec<&str> = project.worktree_root_names(cx).collect(); if decorated_root_names.is_empty() { From c6d7ed33c2419a5b6982775d5d62594e140a0f0f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 4 May 2023 06:41:11 -0700 Subject: [PATCH 16/34] Add smoke test for collaboration --- crates/collab/src/tests/integration_tests.rs | 83 +++++++++++++++++++- crates/fs/src/fs.rs | 27 +++++-- crates/fs/src/repository.rs | 4 +- 3 files changed, 104 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 40f11735795f920cb68ba9dc640b1b11d0a2ad7f..4c1c2534710a64d46617229f8c700caeb3f269dd 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -13,8 +13,8 @@ use editor::{ use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; use gpui::{ - executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext, - ViewHandle, + executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, + TestAppContext, ViewHandle, }; use indoc::indoc; use language::{ @@ -2604,6 +2604,85 @@ async fn test_git_diff_base_change( }); } +#[gpui::test] +async fn test_git_branch_name( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".git": {}, + }), + ) + .await; + + let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| { + call.share_project(project_local.clone(), cx) + }) + .await + .unwrap(); + + let project_remote = client_b.build_remote_project(project_id, cx_b).await; + client_a + .fs + .as_fake() + .set_branch_name(Path::new("/dir/.git"), Some("branch-1")) + .await; + + // Wait for it to catch up to the new branch + deterministic.run_until_parked(); + + #[track_caller] + fn assert_branch(branch_name: Option>, project: &Project, cx: &AppContext) { + let branch_name = branch_name.map(Into::into); + let worktrees = project.visible_worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree = worktrees[0].clone(); + let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap(); + assert_eq!(root_entry.branch(), branch_name.map(Into::into)); + } + + // Smoke test branch reading + project_local.read_with(cx_a, |project, cx| { + assert_branch(Some("branch-1"), project, cx) + }); + project_remote.read_with(cx_b, |project, cx| { + assert_branch(Some("branch-1"), project, cx) + }); + + client_a + .fs + .as_fake() + .set_branch_name(Path::new("/dir/.git"), Some("branch-2")) + .await; + + // Wait for buffer_local_a to receive it + deterministic.run_until_parked(); + + // Smoke test branch reading + project_local.read_with(cx_a, |project, cx| { + assert_branch(Some("branch-2"), project, cx) + }); + project_remote.read_with(cx_b, |project, cx| { + assert_branch(Some("branch-2"), project, cx) + }); +} + #[gpui::test(iterations = 10)] async fn test_fs_operations( deterministic: Arc, diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index d856b71e398c58940623773adf79334215e71807..945ffaea16a66e754db72bfa8db23dc56f48c424 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -619,7 +619,10 @@ impl FakeFs { .boxed() } - pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) { + pub fn with_git_state(&self, dot_git: &Path, f: F) + where + F: FnOnce(&mut FakeGitRepositoryState), + { let mut state = self.state.lock(); let entry = state.read_path(dot_git).unwrap(); let mut entry = entry.lock(); @@ -628,12 +631,7 @@ impl FakeFs { let repo_state = git_repo_state.get_or_insert_with(Default::default); let mut repo_state = repo_state.lock(); - repo_state.index_contents.clear(); - repo_state.index_contents.extend( - head_state - .iter() - .map(|(path, content)| (path.to_path_buf(), content.clone())), - ); + f(&mut repo_state); state.emit_event([dot_git]); } else { @@ -641,6 +639,21 @@ impl FakeFs { } } + 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 async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) { + self.with_git_state(dot_git, |state| { + state.index_contents.clear(); + state.index_contents.extend( + head_state + .iter() + .map(|(path, content)| (path.to_path_buf(), content.clone())), + ); + }); + } + pub fn paths(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index d22d670a8bd874f37d3a22bcb2208bc27bca4ee7..5624ce42f1dc6e134f4602ff82342835bbc4a39f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -71,6 +71,7 @@ pub struct FakeGitRepository { #[derive(Debug, Clone, Default)] pub struct FakeGitRepositoryState { pub index_contents: HashMap, + pub branch_name: Option, } impl FakeGitRepository { @@ -89,7 +90,8 @@ impl GitRepository for FakeGitRepository { } fn branch_name(&self) -> Option { - None + let state = self.state.lock(); + state.branch_name.clone() } } From 2fe5bf419b98a352bc98af5ee7607f3aa954986d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 4 May 2023 13:26:53 -0700 Subject: [PATCH 17/34] Add proto fields for repository entry maintenance --- crates/collab/src/rpc.rs | 4 ++++ crates/project/src/worktree.rs | 16 ++++++++++++++++ crates/rpc/proto/zed.proto | 15 ++++++++++++--- crates/rpc/src/proto.rs | 8 ++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 16e7577d957e3ee993acceab9a52e90987ef7358..05c7d10982a3a705a08b7037cb31c6eb6a75ef55 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1063,6 +1063,8 @@ async fn rejoin_room( removed_entries: worktree.removed_entries, scan_id: worktree.scan_id, is_last_update: worktree.completed_scan_id == worktree.scan_id, + //TODO repo + updated_repositories: vec![], }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; @@ -1383,6 +1385,8 @@ async fn join_project( removed_entries: Default::default(), scan_id: worktree.scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id, + // TODO repo + updated_repositories: vec![], }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b6de263d2a3d8b3e53c82d439b488c3faeeb20da..3adb956be22484c439f4eb0b84349c3486f436fe 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -43,6 +43,7 @@ use std::{ future::Future, mem, ops::{Deref, DerefMut}, + os::unix::prelude::OsStrExt, path::{Path, PathBuf}, pin::Pin, sync::{ @@ -143,6 +144,18 @@ impl RepositoryEntry { } } +impl From<&RepositoryEntry> for proto::RepositoryEntry { + fn from(value: &RepositoryEntry) -> Self { + proto::RepositoryEntry { + git_dir_entry_id: value.git_dir_entry_id.to_proto(), + scan_id: value.scan_id as u64, + git_dir_path: value.git_dir_path.as_os_str().as_bytes().to_vec(), + work_directory: value.work_directory.0.as_os_str().as_bytes().to_vec(), + branch: value.branch.as_ref().map(|str| str.to_string()), + } + } +} + /// This path corresponds to the 'content path' (the folder that contains the .git) #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct RepositoryWorkDirectory(Arc); @@ -1542,6 +1555,7 @@ impl LocalSnapshot { removed_entries: Default::default(), scan_id: self.scan_id as u64, is_last_update: true, + updated_repositories: self.repository_entries.values().map(Into::into).collect(), } } @@ -1610,6 +1624,8 @@ impl LocalSnapshot { removed_entries, scan_id: self.scan_id as u64, is_last_update: self.completed_scan_id == self.scan_id, + // TODO repo + updated_repositories: vec![], } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ebb524e1a0fb8540c28ff2c7e020a41ddc162e36..179452296bd5f0b7686fed46adf5c43ef28668d0 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -329,9 +329,10 @@ message UpdateWorktree { string root_name = 3; repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; - uint64 scan_id = 6; - bool is_last_update = 7; - string abs_path = 8; + repeated RepositoryEntry updated_repositories = 6; + uint64 scan_id = 7; + bool is_last_update = 8; + string abs_path = 9; } message CreateProjectEntry { @@ -979,6 +980,14 @@ message Entry { bool is_ignored = 7; } +message RepositoryEntry { + uint64 git_dir_entry_id = 1; + uint64 scan_id = 2; + bytes git_dir_path = 3; + bytes work_directory = 4; + optional string branch = 5; +} + message BufferState { uint64 id = 1; optional File file = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index a27c6ac1bbc50a362a76f49439b9b2b9f04207ff..b410d0cb83892ec2f7c33b8155c5d2ec98f0a2b6 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -502,6 +502,13 @@ pub fn split_worktree_update( .drain(..removed_entries_chunk_size) .collect(); + let updated_repositories_chunk_size = + cmp::min(message.updated_repositories.len(), max_chunk_size); + let updated_repositories = message + .updated_repositories + .drain(..updated_repositories_chunk_size) + .collect(); + done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); Some(UpdateWorktree { project_id: message.project_id, @@ -512,6 +519,7 @@ pub fn split_worktree_update( removed_entries, scan_id: message.scan_id, is_last_update: done && message.is_last_update, + updated_repositories, }) }) } From 8301ee43d69732d24633eb7d32ad82c30c4f102e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 4 May 2023 16:26:37 -0700 Subject: [PATCH 18/34] WIP: Add repository entries to collab and synchronize when rejoining the room co-authored-by: Max --- .../20221109000000_test_schema.sql | 17 +- crates/collab/src/db.rs | 50 ++++ crates/collab/src/db/worktree_repository.rs | 22 ++ crates/collab/src/rpc.rs | 2 + crates/collab/src/tests/integration_tests.rs | 9 +- crates/project/src/worktree.rs | 217 +++++++++++++----- crates/rpc/proto/zed.proto | 12 +- crates/rpc/src/proto.rs | 8 + 8 files changed, 267 insertions(+), 70 deletions(-) create mode 100644 crates/collab/src/db/worktree_repository.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 89b924087ef987c89ec58e65f2b165a7d11b4afa..c77d13c898aa237987ecf6ac799a253b3c0da8b1 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -82,6 +82,21 @@ CREATE TABLE "worktree_entries" ( CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id"); CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id"); +CREATE TABLE "worktree_repositories" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "dot_git_entry_id" INTEGER NOT NULL, + "scan_id" INTEGER NOT NULL, + "branch" VARCHAR, + "work_directory_path" VARCHAR NOT NULL, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, dot_git_entry_id), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, + FOREIGN KEY(project_id, worktree_id, dot_git_entry_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE +); +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_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, @@ -153,7 +168,7 @@ CREATE TABLE "followers" ( "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, "follower_connection_id" INTEGER NOT NULL ); -CREATE UNIQUE INDEX +CREATE UNIQUE INDEX "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f441bbfb000504d959b9e54f333b5a33cc31273d..9ac7e72ffedb0de4061dbed6a98a5a6bd015648c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -14,6 +14,7 @@ mod user; mod worktree; mod worktree_diagnostic_summary; mod worktree_entry; +mod worktree_repository; use crate::executor::Executor; use crate::{Error, Result}; @@ -2330,6 +2331,55 @@ impl Database { .await?; } + if !update.updated_repositories.is_empty() { + worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( + |repository| worktree_repository::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + dot_git_entry_id: ActiveValue::set(repository.dot_git_entry_id as i64), + work_directory_path: ActiveValue::set(repository.work_directory.clone()), + scan_id: ActiveValue::set(update.scan_id as i64), + branch: ActiveValue::set(repository.branch.clone()), + is_deleted: ActiveValue::set(false), + }, + )) + .on_conflict( + OnConflict::columns([ + worktree_repository::Column::ProjectId, + worktree_repository::Column::WorktreeId, + worktree_repository::Column::DotGitEntryId, + ]) + .update_columns([ + worktree_repository::Column::ScanId, + worktree_repository::Column::WorkDirectoryPath, + worktree_repository::Column::Branch, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !update.removed_repositories.is_empty() { + worktree_repository::Entity::update_many() + .filter( + worktree_repository::Column::ProjectId + .eq(project_id) + .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_repository::Column::DotGitEntryId + .is_in(update.removed_repositories.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_repository::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; Ok(connection_ids) }) diff --git a/crates/collab/src/db/worktree_repository.rs b/crates/collab/src/db/worktree_repository.rs new file mode 100644 index 0000000000000000000000000000000000000000..b281f2047aafd15ef8b45d9cce9235851bf35d2e --- /dev/null +++ b/crates/collab/src/db/worktree_repository.rs @@ -0,0 +1,22 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_repositories")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub dot_git_entry_id: i64, + pub scan_id: i64, + pub branch: Option, + pub work_directory_path: String, + pub is_deleted: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 05c7d10982a3a705a08b7037cb31c6eb6a75ef55..0d57582d7e641579ba9c39d654ae7c1ae26141b1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1065,6 +1065,7 @@ async fn rejoin_room( is_last_update: worktree.completed_scan_id == worktree.scan_id, //TODO repo updated_repositories: vec![], + removed_repositories: vec![], }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; @@ -1387,6 +1388,7 @@ async fn join_project( is_last_update: worktree.scan_id == worktree.completed_scan_id, // TODO repo updated_repositories: vec![], + removed_repositories: vec![], }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 4c1c2534710a64d46617229f8c700caeb3f269dd..9f04642e30855a79aa6ac91e674c62e3c23c4adc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2609,13 +2609,15 @@ async fn test_git_branch_name( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; let active_call_a = cx_a.read(ActiveCall::global); @@ -2681,6 +2683,11 @@ async fn test_git_branch_name( project_remote.read_with(cx_b, |project, cx| { assert_branch(Some("branch-2"), project, cx) }); + + let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; + project_remote_c.read_with(cx_c, |project, cx| { + assert_branch(Some("branch-2"), project, cx) + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3adb956be22484c439f4eb0b84349c3486f436fe..3c550aa6bbf26ae252032c717c4d08fcdbb7edf2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -43,7 +43,6 @@ use std::{ future::Future, mem, ops::{Deref, DerefMut}, - os::unix::prelude::OsStrExt, path::{Path, PathBuf}, pin::Pin, sync::{ @@ -120,21 +119,15 @@ pub struct Snapshot { #[derive(Clone, Debug)] pub struct RepositoryEntry { - // Path to the actual .git folder. - // Note: if .git is a file, this points to the folder indicated by the .git file - pub(crate) git_dir_path: Arc, - pub(crate) git_dir_entry_id: ProjectEntryId, - pub(crate) work_directory: RepositoryWorkDirectory, pub(crate) scan_id: usize, + pub(crate) dot_git_entry_id: ProjectEntryId, + /// Relative to the worktree, the repository for the root will have + /// a work directory equal to: "" + pub(crate) work_directory: RepositoryWorkDirectory, pub(crate) branch: Option>, } impl RepositoryEntry { - // Note that this path should be relative to the worktree root. - pub(crate) fn in_dot_git(&self, path: &Path) -> bool { - path.starts_with(self.git_dir_path.as_ref()) - } - pub fn branch(&self) -> Option> { self.branch.clone() } @@ -147,10 +140,9 @@ impl RepositoryEntry { impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { - git_dir_entry_id: value.git_dir_entry_id.to_proto(), + dot_git_entry_id: value.dot_git_entry_id.to_proto(), scan_id: value.scan_id as u64, - git_dir_path: value.git_dir_path.as_os_str().as_bytes().to_vec(), - work_directory: value.work_directory.0.as_os_str().as_bytes().to_vec(), + work_directory: value.work_directory.to_string_lossy().to_string(), branch: value.branch.as_ref().map(|str| str.to_string()), } } @@ -187,6 +179,12 @@ impl<'a> From<&'a str> for RepositoryWorkDirectory { } } +impl Default for RepositoryWorkDirectory { + fn default() -> Self { + RepositoryWorkDirectory(Arc::from(Path::new(""))) + } +} + #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct RepoPath(PathBuf); @@ -210,22 +208,31 @@ impl AsRef for RepositoryWorkDirectory { } } -impl Default for RepositoryWorkDirectory { - fn default() -> Self { - RepositoryWorkDirectory(Arc::from(Path::new(""))) - } -} - #[derive(Debug, Clone)] pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, // The ProjectEntryId corresponds to the entry for the .git dir - git_repositories: TreeMap>>, + git_repositories: TreeMap, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, } +#[derive(Debug, Clone)] +pub struct LocalRepositoryEntry { + pub(crate) repo_ptr: Arc>, + /// Path to the actual .git folder. + /// Note: if .git is a file, this points to the folder indicated by the .git file + pub(crate) git_dir_path: Arc, +} + +impl LocalRepositoryEntry { + // Note that this path should be relative to the worktree root. + pub(crate) fn in_dot_git(&self, path: &Path) -> bool { + path.starts_with(self.git_dir_path.as_ref()) + } +} + impl Deref for LocalSnapshot { type Target = Snapshot; @@ -690,20 +697,21 @@ impl LocalWorktree { fn diff<'a>( a: impl Iterator, mut b: impl Iterator, - updated: &mut HashMap<&'a Path, RepositoryEntry>, + updated: &mut HashMap, ) { for a_repo in a { let matched = b.find(|b_repo| { - a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id + a_repo.dot_git_entry_id == b_repo.dot_git_entry_id + && a_repo.scan_id == b_repo.scan_id }); if matched.is_none() { - updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone()); + updated.insert(a_repo.dot_git_entry_id, a_repo.clone()); } } } - let mut updated = HashMap::<&Path, RepositoryEntry>::default(); + let mut updated = HashMap::::default(); diff(old_repos.values(), new_repos.values(), &mut updated); diff(new_repos.values(), old_repos.values(), &mut updated); @@ -753,8 +761,8 @@ impl LocalWorktree { if let Some(repo) = snapshot.repo_for(&path) { let repo_path = repo.work_directory.relativize(&path).unwrap(); - if let Some(repo) = self.git_repositories.get(&repo.git_dir_entry_id) { - let repo = repo.to_owned(); + if let Some(repo) = self.git_repositories.get(&repo.dot_git_entry_id) { + let repo = repo.repo_ptr.to_owned(); index_task = Some( cx.background() .spawn(async move { repo.lock().load_index_text(&repo_path) }), @@ -1150,9 +1158,11 @@ impl LocalWorktree { repo_path: RepoPath, cx: &mut ModelContext, ) -> Task> { - let Some(git_ptr) = self.git_repositories.get(&repo.git_dir_entry_id).map(|git_ptr| git_ptr.to_owned()) else { + let Some(git_ptr) = self.git_repositories.get(&repo.dot_git_entry_id).map(|git_ptr| git_ptr.to_owned()) else { return Task::Ready(Some(None)) }; + let git_ptr = git_ptr.repo_ptr; + cx.background() .spawn(async move { git_ptr.lock().load_index_text(&repo_path) }) } @@ -1352,7 +1362,7 @@ impl Snapshot { Some(removed_entry.path) } - pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { + 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(); for entry_id in update.removed_entries { @@ -1378,6 +1388,32 @@ impl Snapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); + + update.removed_repositories.sort_unstable(); + self.repository_entries.retain(|_, entry| { + if let Ok(_) = update + .removed_repositories + .binary_search(&entry.dot_git_entry_id.to_proto()) + { + false + } else { + true + } + }); + + for repository in update.updated_repositories { + let repository = RepositoryEntry { + dot_git_entry_id: ProjectEntryId::from_proto(repository.dot_git_entry_id), + work_directory: RepositoryWorkDirectory( + Path::new(&repository.work_directory).into(), + ), + scan_id: repository.scan_id as usize, + branch: repository.branch.map(Into::into), + }; + self.repository_entries + .insert(repository.work_directory.clone(), repository) + } + self.scan_id = update.scan_id as usize; if update.is_last_update { self.completed_scan_id = update.scan_id as usize; @@ -1529,18 +1565,19 @@ impl LocalSnapshot { &self, path: &Path, ) -> Option<(RepositoryWorkDirectory, Arc>)> { - self.repository_entries + let (entry_id, local_repo) = self + .git_repositories .iter() - .find(|(_, repo)| repo.in_dot_git(path)) - .map(|(work_directory, entry)| { - ( - work_directory.to_owned(), - self.git_repositories - .get(&entry.git_dir_entry_id) - .expect("These two data structures should be in sync") - .to_owned(), - ) - }) + .find(|(_, repo)| repo.in_dot_git(path))?; + + let work_dir = self + .snapshot + .repository_entries + .iter() + .find(|(_, entry)| entry.dot_git_entry_id == *entry_id) + .map(|(_, entry)| entry.work_directory.to_owned())?; + + Some((work_dir, local_repo.repo_ptr.to_owned())) } #[cfg(test)] @@ -1556,6 +1593,7 @@ impl LocalSnapshot { scan_id: self.scan_id as u64, is_last_update: true, updated_repositories: self.repository_entries.values().map(Into::into).collect(), + removed_repositories: Default::default(), } } @@ -1615,6 +1653,44 @@ impl LocalSnapshot { } } + let mut updated_repositories: Vec = Vec::new(); + let mut removed_repositories = Vec::new(); + let mut self_repos = self.snapshot.repository_entries.values().peekable(); + let mut other_repos = other.snapshot.repository_entries.values().peekable(); + loop { + match (self_repos.peek(), other_repos.peek()) { + (Some(self_repo), Some(other_repo)) => { + match Ord::cmp(&self_repo.work_directory, &other_repo.work_directory) { + Ordering::Less => { + updated_repositories.push((*self_repo).into()); + self_repos.next(); + } + Ordering::Equal => { + if self_repo.scan_id != other_repo.scan_id { + updated_repositories.push((*self_repo).into()); + } + + self_repos.next(); + other_repos.next(); + } + Ordering::Greater => { + removed_repositories.push(other_repo.dot_git_entry_id.to_proto()); + other_repos.next(); + } + } + } + (Some(self_repo), None) => { + updated_repositories.push((*self_repo).into()); + self_repos.next(); + } + (None, Some(other_repo)) => { + removed_repositories.push(other_repo.dot_git_entry_id.to_proto()); + other_repos.next(); + } + (None, None) => break, + } + } + proto::UpdateWorktree { project_id, worktree_id, @@ -1624,8 +1700,8 @@ impl LocalSnapshot { removed_entries, scan_id: self.scan_id as u64, is_last_update: self.completed_scan_id == self.scan_id, - // TODO repo - updated_repositories: vec![], + updated_repositories, + removed_repositories, } } @@ -1724,8 +1800,7 @@ impl LocalSnapshot { self.repository_entries.insert( key.clone(), RepositoryEntry { - git_dir_path: parent_path.clone(), - git_dir_entry_id: parent_entry.id, + dot_git_entry_id: parent_entry.id, work_directory: key, scan_id: 0, branch: repo_lock.branch_name().map(Into::into), @@ -1733,7 +1808,13 @@ impl LocalSnapshot { ); drop(repo_lock); - self.git_repositories.insert(parent_entry.id, repo) + self.git_repositories.insert( + parent_entry.id, + LocalRepositoryEntry { + repo_ptr: repo, + git_dir_path: parent_path.clone(), + }, + ) } } } @@ -2424,11 +2505,15 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock(); let mut git_repositories = mem::take(&mut snapshot.git_repositories); - git_repositories.retain(|project_entry_id, _| snapshot.contains_entry(*project_entry_id)); + git_repositories.retain(|project_entry_id, _| { + snapshot + .entry_for_id(*project_entry_id) + .map_or(false, |entry| entry.path.file_name() == Some(&DOT_GIT)) + }); snapshot.git_repositories = git_repositories; let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries); - git_repository_entries.retain(|_, entry| snapshot.contains_entry(entry.git_dir_entry_id)); + git_repository_entries.retain(|_, entry| snapshot.contains_entry(entry.dot_git_entry_id)); snapshot.snapshot.repository_entries = git_repository_entries; snapshot.removed_entry_ids.clear(); @@ -3509,12 +3594,21 @@ mod tests { let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); assert_eq!(entry.work_directory.0.as_ref(), Path::new("dir1")); - assert_eq!(entry.git_dir_path.as_ref(), Path::new("dir1/.git")); + assert_eq!( + tree.entry_for_id(entry.dot_git_entry_id) + .unwrap() + .path + .as_ref(), + Path::new("dir1/.git") + ); let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); assert_eq!(entry.work_directory.deref(), Path::new("dir1/deps/dep1")); assert_eq!( - entry.git_dir_path.as_ref(), + tree.entry_for_id(entry.dot_git_entry_id) + .unwrap() + .path + .as_ref(), Path::new("dir1/deps/dep1/.git"), ); }); @@ -3552,11 +3646,10 @@ mod tests { #[test] fn test_changed_repos() { - fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> RepositoryEntry { + fn fake_entry(dot_git_id: usize, scan_id: usize) -> RepositoryEntry { RepositoryEntry { scan_id, - git_dir_path: git_dir_path.as_ref().into(), - git_dir_entry_id: ProjectEntryId(0), + dot_git_entry_id: ProjectEntryId(dot_git_id), work_directory: RepositoryWorkDirectory( Path::new(&format!("don't-care-{}", scan_id)).into(), ), @@ -3567,29 +3660,29 @@ mod tests { let mut prev_repos = TreeMap::::default(); prev_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-1").into()), - fake_entry("/.git", 0), + fake_entry(1, 0), ); prev_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-2").into()), - fake_entry("/a/.git", 0), + fake_entry(2, 0), ); prev_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-3").into()), - fake_entry("/a/b/.git", 0), + fake_entry(3, 0), ); let mut new_repos = TreeMap::::default(); new_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-4").into()), - fake_entry("/a/.git", 1), + fake_entry(2, 1), ); new_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-5").into()), - fake_entry("/a/b/.git", 0), + fake_entry(3, 0), ); new_repos.insert( RepositoryWorkDirectory(Path::new("don't-care-6").into()), - fake_entry("/a/c/.git", 0), + fake_entry(4, 0), ); let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); @@ -3597,25 +3690,25 @@ mod tests { // Deletion retained assert!(res .iter() - .find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0) + .find(|repo| repo.dot_git_entry_id.0 == 1 && repo.scan_id == 0) .is_some()); // Update retained assert!(res .iter() - .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1) + .find(|repo| repo.dot_git_entry_id.0 == 2 && repo.scan_id == 1) .is_some()); // Addition retained assert!(res .iter() - .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0) + .find(|repo| repo.dot_git_entry_id.0 == 4 && repo.scan_id == 0) .is_some()); // Nochange, not retained assert!(res .iter() - .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0) + .find(|repo| repo.dot_git_entry_id.0 == 3 && repo.scan_id == 0) .is_none()); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 179452296bd5f0b7686fed46adf5c43ef28668d0..37092dea4d20e7b7e10bc18b6f88974e139532de 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -330,9 +330,10 @@ message UpdateWorktree { repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; repeated RepositoryEntry updated_repositories = 6; - uint64 scan_id = 7; - bool is_last_update = 8; - string abs_path = 9; + repeated uint64 removed_repositories = 7; + uint64 scan_id = 8; + bool is_last_update = 9; + string abs_path = 10; } message CreateProjectEntry { @@ -981,10 +982,9 @@ message Entry { } message RepositoryEntry { - uint64 git_dir_entry_id = 1; + uint64 dot_git_entry_id = 1; uint64 scan_id = 2; - bytes git_dir_path = 3; - bytes work_directory = 4; + string work_directory = 4; optional string branch = 5; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index b410d0cb83892ec2f7c33b8155c5d2ec98f0a2b6..14ab916f6527dfe49296d9b2e72b888b060796e7 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -509,6 +509,13 @@ pub fn split_worktree_update( .drain(..updated_repositories_chunk_size) .collect(); + let removed_repositories_chunk_size = + cmp::min(message.removed_repositories.len(), max_chunk_size); + let removed_repositories = message + .removed_repositories + .drain(..removed_repositories_chunk_size) + .collect(); + done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); Some(UpdateWorktree { project_id: message.project_id, @@ -520,6 +527,7 @@ pub fn split_worktree_update( scan_id: message.scan_id, is_last_update: done && message.is_last_update, updated_repositories, + removed_repositories, }) }) } From 5302c256a4aaffefa031525984f60c79f1addf3c Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Fri, 5 May 2023 13:01:57 +0300 Subject: [PATCH 19/34] Rebase main and fix error --- crates/collab_ui/src/branches_button.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/branches_button.rs b/crates/collab_ui/src/branches_button.rs index 7acc7a8ed3210fec9298c108645048edf86b20b0..6b9fb9fb72bbc3312ca47453184d14ec69ed04e6 100644 --- a/crates/collab_ui/src/branches_button.rs +++ b/crates/collab_ui/src/branches_button.rs @@ -111,11 +111,12 @@ impl View for BranchesButton { impl BranchesButton { pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { + let parent_id = cx.view_id(); cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); Self { workspace: workspace.downgrade(), popup_menu: cx.add_view(|cx| { - let mut menu = ContextMenu::new(cx); + let mut menu = ContextMenu::new(parent_id, cx); menu.set_position_mode(OverlayPositionMode::Local); menu }), From 8bde496e74f8c661d282027de1d278d4f158f9e5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 07:57:46 -0700 Subject: [PATCH 20/34] Add branch name in title UI co-authored-by: Petros --- crates/collab_ui/src/branches_button.rs | 2 + crates/collab_ui/src/collab_titlebar_item.rs | 72 +++++++++++++++----- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 1 + 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/crates/collab_ui/src/branches_button.rs b/crates/collab_ui/src/branches_button.rs index 6b9fb9fb72bbc3312ca47453184d14ec69ed04e6..7146e7f86b12f45675e7e0ed08b747d3918288b8 100644 --- a/crates/collab_ui/src/branches_button.rs +++ b/crates/collab_ui/src/branches_button.rs @@ -7,6 +7,8 @@ use gpui::{ use settings::Settings; use workspace::Workspace; +///! TODO: This file will hold the branch switching UI once we build it. + pub struct BranchesButton { workspace: WeakViewHandle, popup_menu: ViewHandle, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a435a15130eef4db024e7a371fef56ffb4bebc44..d74e2452e0f84ba01c2f07def0e986b8af32406c 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -24,6 +24,8 @@ use theme::{AvatarStyle, Theme}; use util::ResultExt; use workspace::{FollowNextCollaborator, Workspace}; +const MAX_TITLE_LENGTH: usize = 75; + actions!( collab, [ @@ -48,7 +50,6 @@ pub struct CollabTitlebarItem { workspace: WeakViewHandle, contacts_popover: Option>, user_menu: ViewHandle, - branches: ViewHandle, _subscriptions: Vec, } @@ -73,10 +74,7 @@ impl View for CollabTitlebarItem { let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); - let project_title = self.collect_title_root_names(&project, cx); - left_container.add_child(self.render_title_root_names(&project_title, theme.clone())); - - left_container.add_child(ChildView::new(&self.branches.clone().into_any(), cx)); + left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx)); let user = self.user_store.read(cx).current_user(); let peer_id = self.client.peer_id(); @@ -106,7 +104,14 @@ impl View for CollabTitlebarItem { Stack::new() .with_child(left_container) - .with_child(right_container.aligned().right()) + .with_child( + Flex::row() + .with_child(right_container.contained().with_background_color( + theme.workspace.titlebar.container.background_color.unwrap(), + )) + .aligned() + .right(), + ) .into_any() } } @@ -163,25 +168,60 @@ impl CollabTitlebarItem { menu.set_position_mode(OverlayPositionMode::Local); menu }), - branches: cx.add_view(|cx| BranchesButton::new(workspace_handle.to_owned(), cx)), _subscriptions: subscriptions, } } - fn collect_title_root_names(&self, project: &Project, cx: &ViewContext) -> String { - let decorated_root_names: Vec<&str> = project.worktree_root_names(cx).collect(); - if decorated_root_names.is_empty() { - "empty project".to_owned() - } else { - decorated_root_names.join(", ") + fn collect_title_root_names( + &self, + project: &Project, + theme: Arc, + cx: &ViewContext, + ) -> AnyElement { + let names_and_branches = project.visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + (worktree.root_name(), worktree.root_git_entry()) + }); + + fn push_str(buffer: &mut String, index: &mut usize, str: &str) { + buffer.push_str(str); + *index += str.chars().count(); + } + + let mut indices = Vec::new(); + let mut index = 0; + let mut title = String::new(); + let mut names_and_branches = names_and_branches.peekable(); + while let Some((name, entry)) = names_and_branches.next() { + push_str(&mut title, &mut index, name); + if let Some(branch) = entry.and_then(|entry| entry.branch()) { + push_str(&mut title, &mut index, "/"); + let pre_index = index; + push_str(&mut title, &mut index, &branch); + indices.extend((pre_index..index).into_iter()) + } + if names_and_branches.peek().is_some() { + push_str(&mut title, &mut index, ", "); + if index >= MAX_TITLE_LENGTH { + title.push_str(" …"); + break; + } + } } - } - fn render_title_root_names(&self, title: &str, theme: Arc) -> AnyElement { let text_style = theme.workspace.titlebar.title.clone(); let item_spacing = theme.workspace.titlebar.item_spacing; - Label::new(title.to_owned(), text_style) + let mut highlight = text_style.clone(); + highlight.color = theme.workspace.titlebar.highlight_color; + + let style = LabelStyle { + text: text_style, + highlight_text: Some(highlight), + }; + + Label::new(title, style) + .with_highlights(indices) .contained() .with_margin_right(item_spacing) .aligned() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index fb6bd85f02b1f7f43d98d819613543c0a95d3ab0..0bd23a0b87598c02d41a2d5ae79aaaefa5ba2a14 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -93,6 +93,7 @@ pub struct Titlebar { pub container: ContainerStyle, pub height: f32, pub title: TextStyle, + pub highlight_color: Color, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 9b53ecc5d2fd8702bbf7ade760160cfdbe83067b..9d0c4de9f78eef989a71ee33c1b9da3fcc8f8a50 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -140,6 +140,7 @@ export default function workspace(colorScheme: ColorScheme) { // Project title: text(layer, "sans", "variant"), + highlight_color: text(layer, "sans", "active").color, // Collaborators leaderAvatar: { From b6d6f5c6504f6444bddca77ebccf832d65f1d104 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 11:10:06 -0700 Subject: [PATCH 21/34] WIP: re-arranging the RepositoryEntry representation Added branches to the randomized test to check the git branch Added the remaining database integrations in collab Co-authored-by: Max Co-authored-by: Petros --- crates/collab/src/db.rs | 130 ++++++++++++++---- crates/collab/src/rpc.rs | 5 +- .../src/tests/randomized_integration_tests.rs | 52 ++++++- crates/project/src/worktree.rs | 25 ++-- crates/rpc/proto/zed.proto | 7 +- 5 files changed, 169 insertions(+), 50 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9ac7e72ffedb0de4061dbed6a98a5a6bd015648c..63fd59628c49983b42937dd7597bb44fdb28b131 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1490,6 +1490,8 @@ impl Database { visible: db_worktree.visible, updated_entries: Default::default(), removed_entries: Default::default(), + updated_repositories: Default::default(), + removed_repositories: Default::default(), diagnostic_summaries: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, @@ -1499,38 +1501,77 @@ impl Database { .worktrees .iter() .find(|worktree| worktree.id == db_worktree.id as u64); - let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { - worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_entry::Column::IsDeleted.eq(false) - }; - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) - .add(entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if db_entry.is_deleted { - worktree.removed_entries.push(db_entry.id as u64); + // File entries + { + let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { + worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) } else { - worktree.updated_entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - }); + worktree_entry::Column::IsDeleted.eq(false) + }; + + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) + .add(entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if db_entry.is_deleted { + worktree.removed_entries.push(db_entry.id as u64); + } else { + worktree.updated_entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + }); + } + } + } + + // Repository Entries + { + let repository_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_repository::Column::IsDeleted.eq(false) + }; + + let mut db_repositories = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) + .add(repository_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_repository) = db_repositories.next().await { + let db_repository = db_repository?; + if db_repository.is_deleted { + worktree + .removed_repositories + .push(db_repository.dot_git_entry_id as u64); + } else { + worktree.updated_repositories.push(proto::RepositoryEntry { + dot_git_entry_id: db_repository.dot_git_entry_id as u64, + scan_id: db_repository.scan_id as u64, + work_directory: db_repository.work_directory_path, + branch: db_repository.branch, + }); + } } } @@ -2555,6 +2596,7 @@ impl Database { root_name: db_worktree.root_name, visible: db_worktree.visible, entries: Default::default(), + repository_entries: Default::default(), diagnostic_summaries: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, @@ -2592,6 +2634,31 @@ impl Database { } } + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project_id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = + worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.push(proto::RepositoryEntry { + dot_git_entry_id: db_repository_entry.dot_git_entry_id as u64, + scan_id: db_repository_entry.scan_id as u64, + work_directory: db_repository_entry.work_directory_path, + branch: db_repository_entry.branch, + }); + } + } + } + // Populate worktree diagnostic summaries. { let mut db_summaries = worktree_diagnostic_summary::Entity::find() @@ -3273,6 +3340,8 @@ pub struct RejoinedWorktree { pub visible: bool, pub updated_entries: Vec, pub removed_entries: Vec, + pub updated_repositories: Vec, + pub removed_repositories: Vec, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, @@ -3327,6 +3396,7 @@ pub struct Worktree { pub root_name: String, pub visible: bool, pub entries: Vec, + pub repository_entries: Vec, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0d57582d7e641579ba9c39d654ae7c1ae26141b1..39f8266978c2f0a212ec8bb57cb412a50387b01d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1386,9 +1386,8 @@ async fn join_project( removed_entries: Default::default(), scan_id: worktree.scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id, - // TODO repo - updated_repositories: vec![], - removed_repositories: vec![], + updated_repositories: worktree.repository_entries, + removed_repositories: Default::default(), }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 00273722c4312217b533c9f3a25fc12b0387acc7..c90354f214c55611793c87ea11ae644fc5a748ec 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -785,6 +785,28 @@ async fn apply_client_operation( } client.fs.set_index_for_repo(&dot_git_dir, &contents).await; } + + ClientOperation::WriteGitBranch { + repo_path, + new_branch, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + log::info!( + "{}: writing git branch for repo {:?}: {:?}", + client.username, + repo_path, + new_branch + ); + + let dot_git_dir = repo_path.join(".git"); + 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; + } } Ok(()) } @@ -859,6 +881,12 @@ fn check_consistency_between_clients(clients: &[(Rc, TestAppContext) host_snapshot.abs_path(), guest_project.remote_id(), ); + assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), + "{} has different repositories than the host for worktree {:?} and project {:?}", + client.username, + host_snapshot.abs_path(), + guest_project.remote_id(), + ); assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(), "{} has different scan id than the host for worktree {:?} and project {:?}", client.username, @@ -1151,6 +1179,10 @@ enum ClientOperation { repo_path: PathBuf, contents: Vec<(PathBuf, String)>, }, + WriteGitBranch { + repo_path: PathBuf, + new_branch: Option, + }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1664,7 +1696,7 @@ impl TestPlan { } // Update a git index - 91..=95 => { + 91..=93 => { let repo_path = client .fs .directories() @@ -1698,6 +1730,24 @@ impl TestPlan { }; } + // Update a git branch + 94..=95 => { + let repo_path = client + .fs + .directories() + .choose(&mut self.rng) + .unwrap() + .clone(); + + let new_branch = (self.rng.gen_range(0..10) > 3) + .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); + + break ClientOperation::WriteGitBranch { + repo_path, + new_branch, + }; + } + // Create or update a file or directory 96.. => { let is_dir = self.rng.gen::(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3c550aa6bbf26ae252032c717c4d08fcdbb7edf2..ea26a969d2a301ea69b849d0a652a9a56b3d8a89 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -117,13 +117,10 @@ pub struct Snapshot { completed_scan_id: usize, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct RepositoryEntry { pub(crate) scan_id: usize, - pub(crate) dot_git_entry_id: ProjectEntryId, - /// Relative to the worktree, the repository for the root will have - /// a work directory equal to: "" - pub(crate) work_directory: RepositoryWorkDirectory, + pub(crate) work_directory_id: ProjectEntryId, pub(crate) branch: Option>, } @@ -132,17 +129,16 @@ impl RepositoryEntry { self.branch.clone() } - pub fn work_directory(&self) -> Arc { - self.work_directory.0.clone() + pub fn work_directory_id(&self) -> ProjectEntryId { + self.work_directory_id } } impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { - dot_git_entry_id: value.dot_git_entry_id.to_proto(), scan_id: value.scan_id as u64, - work_directory: value.work_directory.to_string_lossy().to_string(), + work_directory_id: value.work_directory_id.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } } @@ -212,6 +208,7 @@ impl AsRef for RepositoryWorkDirectory { pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, // The ProjectEntryId corresponds to the entry for the .git dir + // work_directory_id git_repositories: TreeMap, removed_entry_ids: HashMap, next_entry_id: Arc, @@ -701,12 +698,12 @@ impl LocalWorktree { ) { for a_repo in a { let matched = b.find(|b_repo| { - a_repo.dot_git_entry_id == b_repo.dot_git_entry_id + a_repo.work_directory_id == b_repo.work_directory_id && a_repo.scan_id == b_repo.scan_id }); if matched.is_none() { - updated.insert(a_repo.dot_git_entry_id, a_repo.clone()); + updated.insert(a_repo.work_directory_id, a_repo.clone()); } } } @@ -1158,7 +1155,7 @@ impl LocalWorktree { repo_path: RepoPath, cx: &mut ModelContext, ) -> Task> { - let Some(git_ptr) = self.git_repositories.get(&repo.dot_git_entry_id).map(|git_ptr| git_ptr.to_owned()) else { + let Some(git_ptr) = self.git_repositories.get(&repo.work_directory_id).map(|git_ptr| git_ptr.to_owned()) else { return Task::Ready(Some(None)) }; let git_ptr = git_ptr.repo_ptr; @@ -1476,6 +1473,10 @@ impl Snapshot { self.traverse_from_offset(true, include_ignored, 0) } + pub fn repositories(&self) -> impl Iterator { + self.repository_entries.values() + } + pub fn paths(&self) -> impl Iterator> { let empty_path = Path::new(""); self.entries_by_path diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 37092dea4d20e7b7e10bc18b6f88974e139532de..315352b01e1f24a8967255ebb29398e39912ce87 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -982,10 +982,9 @@ message Entry { } message RepositoryEntry { - uint64 dot_git_entry_id = 1; - uint64 scan_id = 2; - string work_directory = 4; - optional string branch = 5; + uint64 scan_id = 1; + uint64 work_directory_id = 2; + optional string branch = 3; } message BufferState { From 53569ece035b28f28b4a56427883f296ae5db8af Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 14:23:17 -0700 Subject: [PATCH 22/34] WIP: Change RepositoryEntry representation to be keyed off of the work directory Removes branches button scaffolding --- crates/collab/src/db.rs | 16 +- crates/collab/src/db/worktree_repository.rs | 3 +- crates/collab_ui/src/branches_button.rs | 168 ---------------- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 2 - crates/project/src/project.rs | 15 +- crates/project/src/worktree.rs | 194 +++++++++++-------- 7 files changed, 126 insertions(+), 274 deletions(-) delete mode 100644 crates/collab_ui/src/branches_button.rs diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 63fd59628c49983b42937dd7597bb44fdb28b131..8d5ad280ef6fe6db199ac7d789e67932edf191b1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1563,12 +1563,11 @@ impl Database { if db_repository.is_deleted { worktree .removed_repositories - .push(db_repository.dot_git_entry_id as u64); + .push(db_repository.work_directory_id as u64); } else { worktree.updated_repositories.push(proto::RepositoryEntry { - dot_git_entry_id: db_repository.dot_git_entry_id as u64, scan_id: db_repository.scan_id as u64, - work_directory: db_repository.work_directory_path, + work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, }); } @@ -2377,8 +2376,7 @@ impl Database { |repository| worktree_repository::ActiveModel { project_id: ActiveValue::set(project_id), worktree_id: ActiveValue::set(worktree_id), - dot_git_entry_id: ActiveValue::set(repository.dot_git_entry_id as i64), - work_directory_path: ActiveValue::set(repository.work_directory.clone()), + work_directory_id: ActiveValue::set(repository.work_directory_id as i64), scan_id: ActiveValue::set(update.scan_id as i64), branch: ActiveValue::set(repository.branch.clone()), is_deleted: ActiveValue::set(false), @@ -2388,11 +2386,10 @@ impl Database { OnConflict::columns([ worktree_repository::Column::ProjectId, worktree_repository::Column::WorktreeId, - worktree_repository::Column::DotGitEntryId, + worktree_repository::Column::WorkDirectoryId, ]) .update_columns([ worktree_repository::Column::ScanId, - worktree_repository::Column::WorkDirectoryPath, worktree_repository::Column::Branch, ]) .to_owned(), @@ -2408,7 +2405,7 @@ impl Database { .eq(project_id) .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) .and( - worktree_repository::Column::DotGitEntryId + worktree_repository::Column::WorkDirectoryId .is_in(update.removed_repositories.iter().map(|id| *id as i64)), ), ) @@ -2650,9 +2647,8 @@ impl Database { worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) { worktree.repository_entries.push(proto::RepositoryEntry { - dot_git_entry_id: db_repository_entry.dot_git_entry_id as u64, scan_id: db_repository_entry.scan_id as u64, - work_directory: db_repository_entry.work_directory_path, + work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, }); } diff --git a/crates/collab/src/db/worktree_repository.rs b/crates/collab/src/db/worktree_repository.rs index b281f2047aafd15ef8b45d9cce9235851bf35d2e..116d7b3ed9ed214ef8989171bfa610c3fcb08c37 100644 --- a/crates/collab/src/db/worktree_repository.rs +++ b/crates/collab/src/db/worktree_repository.rs @@ -9,10 +9,9 @@ pub struct Model { #[sea_orm(primary_key)] pub worktree_id: i64, #[sea_orm(primary_key)] - pub dot_git_entry_id: i64, + pub work_directory_id: i64, pub scan_id: i64, pub branch: Option, - pub work_directory_path: String, pub is_deleted: bool, } diff --git a/crates/collab_ui/src/branches_button.rs b/crates/collab_ui/src/branches_button.rs deleted file mode 100644 index 7146e7f86b12f45675e7e0ed08b747d3918288b8..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/branches_button.rs +++ /dev/null @@ -1,168 +0,0 @@ -use context_menu::{ContextMenu, ContextMenuItem}; -use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use settings::Settings; -use workspace::Workspace; - -///! TODO: This file will hold the branch switching UI once we build it. - -pub struct BranchesButton { - workspace: WeakViewHandle, - popup_menu: ViewHandle, -} - -impl Entity for BranchesButton { - type Event = (); -} - -impl View for BranchesButton { - fn ui_name() -> &'static str { - "BranchesButton" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let Some(workspace) = self.workspace.upgrade(cx) else { - return Empty::new().into_any(); - }; - - let project = workspace.read(cx).project().read(cx); - let only_one_worktree = project.visible_worktrees(cx).count() == 1; - let branches_count: usize = project - .visible_worktrees(cx) - .map(|worktree_handle| worktree_handle.read(cx).snapshot().git_entries().count()) - .sum(); - let branch_caption: String = if only_one_worktree { - project - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .snapshot() - .root_git_entry() - .and_then(|entry| entry.branch()) - .map(|branch| branch.to_string()) - .unwrap_or_else(|| "".to_owned()) - } else { - branches_count.to_string() - }; - let is_popup_menu_visible = self.popup_menu.read(cx).visible(); - - let theme = cx.global::().theme.clone(); - - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _cx| { - let style = theme - .workspace - .titlebar - .toggle_contacts_button - .style_for(state, is_popup_menu_visible); - - Flex::row() - .with_child( - Svg::new("icons/version_control_branch_12.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // .contained() - // .with_style(style.container) - .into_any_named("version-control-branch-icon"), - ) - .with_child( - Label::new(branch_caption, theme.workspace.titlebar.title.clone()) - .contained() - .with_style(style.container) - .aligned(), - ) - .constrained() - .with_height(style.button_width) - .contained() - .with_style(style.container) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.deploy_branches_menu(cx); - }) - .with_tooltip::( - 0, - "Branches".into(), - None, - theme.tooltip.clone(), - cx, - ), - ) - .with_child( - ChildView::new(&self.popup_menu, cx) - .aligned() - .bottom() - .left(), - ) - .into_any_named("branches-button") - } -} - -impl BranchesButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { - let parent_id = cx.view_id(); - cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); - Self { - workspace: workspace.downgrade(), - popup_menu: cx.add_view(|cx| { - let mut menu = ContextMenu::new(parent_id, cx); - menu.set_position_mode(OverlayPositionMode::Local); - menu - }), - } - } - - pub fn deploy_branches_menu(&mut self, cx: &mut ViewContext) { - let mut menu_options = vec![]; - - if let Some(workspace) = self.workspace.upgrade(cx) { - let project = workspace.read(cx).project().read(cx); - - let worktrees_with_branches = project - .visible_worktrees(cx) - .map(|worktree_handle| { - worktree_handle - .read(cx) - .snapshot() - .git_entries() - .filter_map(|entry| { - entry.branch().map(|branch| { - let repo_name = entry.work_directory(); - if let Some(name) = repo_name.file_name() { - (name.to_string_lossy().to_string(), branch) - } else { - ("WORKTREE ROOT".into(), branch) - } - }) - }) - .collect::>() - }) - .flatten(); - - let context_menu_items = worktrees_with_branches.map(|(repo_name, branch_name)| { - let caption = format!("{} / {}", repo_name, branch_name); - ContextMenuItem::handler(caption.to_owned(), move |_| { - println!("{}", caption); - }) - }); - menu_options.extend(context_menu_items); - } - - self.popup_menu.update(cx, |menu, cx| { - menu.show(Default::default(), AnchorCorner::TopLeft, menu_options, cx); - }); - } -} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d74e2452e0f84ba01c2f07def0e986b8af32406c..ec5eaab36c6fd03587a943ec67c431f84ee6f993 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,6 @@ use crate::{ contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, - toggle_screen_sharing, BranchesButton, ToggleScreenSharing, + toggle_screen_sharing, ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 21a864d4e8943f6b8e7c00d30e9f9dc5358a5ffb..c0734388b1512b2e9cac5014c460bf1a8c09650b 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,4 +1,3 @@ -mod branches_button; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -10,7 +9,6 @@ mod notifications; mod project_shared_notification; mod sharing_status_indicator; -pub use branches_button::BranchesButton; use call::ActiveCall; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; use gpui::{actions, AppContext, Task}; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3971eafbddb36e0d5e2561420e36237cfa121364..3f4c81afd14822d3cf212298e3452ca247215d72 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4696,11 +4696,11 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, - worktree: ModelHandle, + worktree_handle: ModelHandle, repos: &Vec, cx: &mut ModelContext, ) { - debug_assert!(worktree.read(cx).is_local()); + debug_assert!(worktree_handle.read(cx).is_local()); for (_, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { @@ -4708,28 +4708,31 @@ impl Project { Some(file) => file, None => continue, }; - if file.worktree != worktree { + if file.worktree != worktree_handle { continue; } let path = file.path().clone(); + let worktree = worktree_handle.read(cx); let repo = match repos .iter() - .find(|entry| entry.work_directory.contains(&path)) + .find(|repository| repository.work_directory.contains(worktree, &path)) { Some(repo) => repo.clone(), None => return, }; - let relative_repo = match repo.work_directory.relativize(&path) { + let relative_repo = match repo.work_directory.relativize(worktree, &path) { Some(relative_repo) => relative_repo.to_owned(), None => return, }; + drop(worktree); + let remote_id = self.remote_id(); let client = self.client.clone(); - let diff_base_task = worktree.update(cx, move |worktree, cx| { + let diff_base_task = worktree_handle.update(cx, move |worktree, cx| { worktree .as_local() .unwrap() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ea26a969d2a301ea69b849d0a652a9a56b3d8a89..5b42257f0b728a1455ed2e110855faddc8834ac4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -120,7 +120,7 @@ pub struct Snapshot { #[derive(Clone, Debug, Eq, PartialEq)] pub struct RepositoryEntry { pub(crate) scan_id: usize, - pub(crate) work_directory_id: ProjectEntryId, + pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, } @@ -130,7 +130,17 @@ impl RepositoryEntry { } pub fn work_directory_id(&self) -> ProjectEntryId { - self.work_directory_id + *self.work_directory + } + + pub fn work_directory(&self, snapshot: &Snapshot) -> Option { + snapshot + .entry_for_id(self.work_directory_id()) + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + } + + pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool { + self.work_directory.contains(snapshot, path) } } @@ -138,7 +148,7 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { scan_id: value.scan_id as u64, - work_directory_id: value.work_directory_id.to_proto(), + work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } } @@ -148,36 +158,44 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct RepositoryWorkDirectory(Arc); -impl RepositoryWorkDirectory { - // Note that these paths should be relative to the worktree root. - pub(crate) fn contains(&self, path: &Path) -> bool { - path.starts_with(self.0.as_ref()) +impl Default for RepositoryWorkDirectory { + fn default() -> Self { + RepositoryWorkDirectory(Arc::from(Path::new(""))) } +} - pub(crate) fn relativize(&self, path: &Path) -> Option { - path.strip_prefix(self.0.as_ref()) - .ok() - .map(move |path| RepoPath(path.to_owned())) +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct WorkDirectoryEntry(ProjectEntryId); + +impl WorkDirectoryEntry { + // Note that these paths should be relative to the worktree root. + pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool { + snapshot + .entry_for_id(self.0) + .map(|entry| path.starts_with(&entry.path)) + .unwrap_or(false) + } + + pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option { + worktree.entry_for_id(self.0).and_then(|entry| { + path.strip_prefix(&entry.path) + .ok() + .map(move |path| RepoPath(path.to_owned())) + }) } } -impl Deref for RepositoryWorkDirectory { - type Target = Path; +impl Deref for WorkDirectoryEntry { + type Target = ProjectEntryId; fn deref(&self) -> &Self::Target { - self.0.as_ref() + &self.0 } } -impl<'a> From<&'a str> for RepositoryWorkDirectory { - fn from(value: &'a str) -> Self { - RepositoryWorkDirectory(Path::new(value).into()) - } -} - -impl Default for RepositoryWorkDirectory { - fn default() -> Self { - RepositoryWorkDirectory(Arc::from(Path::new(""))) +impl<'a> From for WorkDirectoryEntry { + fn from(value: ProjectEntryId) -> Self { + WorkDirectoryEntry(value) } } @@ -698,12 +716,12 @@ impl LocalWorktree { ) { for a_repo in a { let matched = b.find(|b_repo| { - a_repo.work_directory_id == b_repo.work_directory_id + a_repo.work_directory == b_repo.work_directory && a_repo.scan_id == b_repo.scan_id }); if matched.is_none() { - updated.insert(a_repo.work_directory_id, a_repo.clone()); + updated.insert(*a_repo.work_directory, a_repo.clone()); } } } @@ -757,8 +775,8 @@ impl LocalWorktree { let mut index_task = None; if let Some(repo) = snapshot.repo_for(&path) { - let repo_path = repo.work_directory.relativize(&path).unwrap(); - if let Some(repo) = self.git_repositories.get(&repo.dot_git_entry_id) { + let repo_path = repo.work_directory.relativize(self, &path).unwrap(); + if let Some(repo) = self.git_repositories.get(&*repo.work_directory) { let repo = repo.repo_ptr.to_owned(); index_task = Some( cx.background() @@ -1155,7 +1173,7 @@ impl LocalWorktree { repo_path: RepoPath, cx: &mut ModelContext, ) -> Task> { - let Some(git_ptr) = self.git_repositories.get(&repo.work_directory_id).map(|git_ptr| git_ptr.to_owned()) else { + let Some(git_ptr) = self.git_repositories.get(&repo.work_directory).map(|git_ptr| git_ptr.to_owned()) else { return Task::Ready(Some(None)) }; let git_ptr = git_ptr.repo_ptr; @@ -1390,7 +1408,7 @@ impl Snapshot { self.repository_entries.retain(|_, entry| { if let Ok(_) = update .removed_repositories - .binary_search(&entry.dot_git_entry_id.to_proto()) + .binary_search(&entry.work_directory.to_proto()) { false } else { @@ -1400,15 +1418,15 @@ impl Snapshot { for repository in update.updated_repositories { let repository = RepositoryEntry { - dot_git_entry_id: ProjectEntryId::from_proto(repository.dot_git_entry_id), - work_directory: RepositoryWorkDirectory( - Path::new(&repository.work_directory).into(), - ), + work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), scan_id: repository.scan_id as usize, branch: repository.branch.map(Into::into), }; - self.repository_entries - .insert(repository.work_directory.clone(), repository) + // TODO: Double check this logic + if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { + self.repository_entries + .insert(RepositoryWorkDirectory(entry.path.clone()), repository) + } } self.scan_id = update.scan_id as usize; @@ -1509,7 +1527,7 @@ impl Snapshot { pub fn root_git_entry(&self) -> Option { self.repository_entries - .get(&"".into()) + .get(&RepositoryWorkDirectory(Path::new("").into())) .map(|entry| entry.to_owned()) } @@ -1549,7 +1567,7 @@ impl LocalSnapshot { let mut max_len = 0; let mut current_candidate = None; for (work_directory, repo) in (&self.repository_entries).iter() { - if work_directory.contains(path) { + if repo.contains(self, path) { if work_directory.0.as_os_str().len() >= max_len { current_candidate = Some(repo); max_len = work_directory.0.as_os_str().len(); @@ -1575,8 +1593,8 @@ impl LocalSnapshot { .snapshot .repository_entries .iter() - .find(|(_, entry)| entry.dot_git_entry_id == *entry_id) - .map(|(_, entry)| entry.work_directory.to_owned())?; + .find(|(_, entry)| *entry.work_directory == *entry_id) + .and_then(|(_, entry)| entry.work_directory(self))?; Some((work_dir, local_repo.repo_ptr.to_owned())) } @@ -1675,7 +1693,7 @@ impl LocalSnapshot { other_repos.next(); } Ordering::Greater => { - removed_repositories.push(other_repo.dot_git_entry_id.to_proto()); + removed_repositories.push(other_repo.work_directory.to_proto()); other_repos.next(); } } @@ -1685,7 +1703,7 @@ impl LocalSnapshot { self_repos.next(); } (None, Some(other_repo)) => { - removed_repositories.push(other_repo.dot_git_entry_id.to_proto()); + removed_repositories.push(other_repo.work_directory.to_proto()); other_repos.next(); } (None, None) => break, @@ -1794,28 +1812,32 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&parent_path); let content_path: Arc = parent_path.parent().unwrap().into(); - let key = RepositoryWorkDirectory(content_path.clone()); - if self.repository_entries.get(&key).is_none() { - if let Some(repo) = fs.open_repo(abs_path.as_path()) { - let repo_lock = repo.lock(); - self.repository_entries.insert( - key.clone(), - RepositoryEntry { - dot_git_entry_id: parent_entry.id, - work_directory: key, - scan_id: 0, - branch: repo_lock.branch_name().map(Into::into), - }, - ); - drop(repo_lock); - - self.git_repositories.insert( - parent_entry.id, - LocalRepositoryEntry { - repo_ptr: repo, - git_dir_path: parent_path.clone(), - }, - ) + if let Some(work_dir_id) = self + .entry_for_path(content_path.clone()) + .map(|entry| entry.id) + { + let key = RepositoryWorkDirectory(content_path.clone()); + if self.repository_entries.get(&key).is_none() { + if let Some(repo) = fs.open_repo(abs_path.as_path()) { + let repo_lock = repo.lock(); + self.repository_entries.insert( + key.clone(), + RepositoryEntry { + work_directory: work_dir_id.into(), + scan_id: 0, + branch: repo_lock.branch_name().map(Into::into), + }, + ); + drop(repo_lock); + + self.git_repositories.insert( + work_dir_id, + LocalRepositoryEntry { + repo_ptr: repo, + git_dir_path: parent_path.clone(), + }, + ) + } } } } @@ -2514,7 +2536,16 @@ impl BackgroundScanner { snapshot.git_repositories = git_repositories; let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries); - git_repository_entries.retain(|_, entry| snapshot.contains_entry(entry.dot_git_entry_id)); + git_repository_entries.retain(|_, entry| { + entry + .work_directory(&snapshot) + .map(|directory| { + snapshot + .entry_for_path((directory.as_ref()).join(".git")) + .is_some() + }) + .unwrap_or(false) + }); snapshot.snapshot.repository_entries = git_repository_entries; snapshot.removed_entry_ids.clear(); @@ -3594,23 +3625,19 @@ mod tests { assert!(tree.repo_for("c.txt".as_ref()).is_none()); let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!(entry.work_directory.0.as_ref(), Path::new("dir1")); assert_eq!( - tree.entry_for_id(entry.dot_git_entry_id) - .unwrap() - .path - .as_ref(), - Path::new("dir1/.git") + entry + .work_directory(tree) + .map(|directory| directory.as_ref().to_owned()), + Some(Path::new("dir1").to_owned()) ); let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); - assert_eq!(entry.work_directory.deref(), Path::new("dir1/deps/dep1")); assert_eq!( - tree.entry_for_id(entry.dot_git_entry_id) - .unwrap() - .path - .as_ref(), - Path::new("dir1/deps/dep1/.git"), + entry + .work_directory(tree) + .map(|directory| directory.as_ref().to_owned()), + Some(Path::new("dir1/deps/dep1").to_owned()) ); }); @@ -3647,13 +3674,10 @@ mod tests { #[test] fn test_changed_repos() { - fn fake_entry(dot_git_id: usize, scan_id: usize) -> RepositoryEntry { + fn fake_entry(work_dir_id: usize, scan_id: usize) -> RepositoryEntry { RepositoryEntry { scan_id, - dot_git_entry_id: ProjectEntryId(dot_git_id), - work_directory: RepositoryWorkDirectory( - Path::new(&format!("don't-care-{}", scan_id)).into(), - ), + work_directory: ProjectEntryId(work_dir_id).into(), branch: None, } } @@ -3691,25 +3715,25 @@ mod tests { // Deletion retained assert!(res .iter() - .find(|repo| repo.dot_git_entry_id.0 == 1 && repo.scan_id == 0) + .find(|repo| repo.work_directory.0 .0 == 1 && repo.scan_id == 0) .is_some()); // Update retained assert!(res .iter() - .find(|repo| repo.dot_git_entry_id.0 == 2 && repo.scan_id == 1) + .find(|repo| repo.work_directory.0 .0 == 2 && repo.scan_id == 1) .is_some()); // Addition retained assert!(res .iter() - .find(|repo| repo.dot_git_entry_id.0 == 4 && repo.scan_id == 0) + .find(|repo| repo.work_directory.0 .0 == 4 && repo.scan_id == 0) .is_some()); // Nochange, not retained assert!(res .iter() - .find(|repo| repo.dot_git_entry_id.0 == 3 && repo.scan_id == 0) + .find(|repo| repo.work_directory.0 .0 == 3 && repo.scan_id == 0) .is_none()); } From 270147d20c8c77b3367bfd3c138347e963ec5dbc Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 14:43:46 -0700 Subject: [PATCH 23/34] Finished RepositoryEntry refactoring, smoke tests passing co-authored-by: Max --- .../20221109000000_test_schema.sql | 7 +++--- crates/collab/src/rpc.rs | 5 ++-- crates/project/src/worktree.rs | 23 +++++++++---------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index c77d13c898aa237987ecf6ac799a253b3c0da8b1..684b6bffe0b938358bbb9f1803da162c5b19cda6 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -85,14 +85,13 @@ CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree CREATE TABLE "worktree_repositories" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, - "dot_git_entry_id" INTEGER NOT NULL, + "work_directory_id" INTEGER NOT NULL, "scan_id" INTEGER NOT NULL, "branch" VARCHAR, - "work_directory_path" VARCHAR NOT NULL, "is_deleted" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, dot_git_entry_id), + PRIMARY KEY(project_id, worktree_id, work_directory_id), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, - FOREIGN KEY(project_id, worktree_id, dot_git_entry_id) REFERENCES worktree_entries (project_id, worktree_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_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"); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 39f8266978c2f0a212ec8bb57cb412a50387b01d..23935904d3a85221e1e3ac95df29b64dfd40cbb4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1063,9 +1063,8 @@ async fn rejoin_room( removed_entries: worktree.removed_entries, scan_id: worktree.scan_id, is_last_update: worktree.completed_scan_id == worktree.scan_id, - //TODO repo - updated_repositories: vec![], - removed_repositories: vec![], + updated_repositories: worktree.updated_repositories, + removed_repositories: worktree.removed_repositories, }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5b42257f0b728a1455ed2e110855faddc8834ac4..12388dfb957f50a53f2b11790b7c528bceef402a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1422,10 +1422,11 @@ impl Snapshot { scan_id: repository.scan_id as usize, branch: repository.branch.map(Into::into), }; - // TODO: Double check this logic if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { self.repository_entries .insert(RepositoryWorkDirectory(entry.path.clone()), repository) + } else { + log::error!("no work directory entry for repository {:?}", repository) } } @@ -2528,23 +2529,21 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock(); let mut git_repositories = mem::take(&mut snapshot.git_repositories); - git_repositories.retain(|project_entry_id, _| { + git_repositories.retain(|work_directory_id, _| { snapshot - .entry_for_id(*project_entry_id) - .map_or(false, |entry| entry.path.file_name() == Some(&DOT_GIT)) + .entry_for_id(*work_directory_id) + .map_or(false, |entry| { + snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() + }) }); snapshot.git_repositories = git_repositories; let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries); git_repository_entries.retain(|_, entry| { - entry - .work_directory(&snapshot) - .map(|directory| { - snapshot - .entry_for_path((directory.as_ref()).join(".git")) - .is_some() - }) - .unwrap_or(false) + snapshot + .git_repositories + .get(&entry.work_directory.0) + .is_some() }); snapshot.snapshot.repository_entries = git_repository_entries; From d8dac07408095546b754ffbd6f8d9b65a04abd80 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 16:33:21 -0700 Subject: [PATCH 24/34] Removed scan ID from repository interfaces co-authored-by: Max --- crates/collab/src/db.rs | 2 - crates/project/src/project.rs | 19 +-- crates/project/src/worktree.rs | 304 ++++++++++++++++----------------- crates/rpc/proto/zed.proto | 5 +- crates/rpc/src/proto.rs | 29 ++-- 5 files changed, 178 insertions(+), 181 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8d5ad280ef6fe6db199ac7d789e67932edf191b1..bc5b816abf2126f0880ac2f23932b020a86a2ee8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1566,7 +1566,6 @@ impl Database { .push(db_repository.work_directory_id as u64); } else { worktree.updated_repositories.push(proto::RepositoryEntry { - scan_id: db_repository.scan_id as u64, work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, }); @@ -2647,7 +2646,6 @@ impl Database { worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) { worktree.repository_entries.push(proto::RepositoryEntry { - scan_id: db_repository_entry.scan_id as u64, work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3f4c81afd14822d3cf212298e3452ca247215d72..b3d432763e52f52dfaef111f26cbb8e1cf1a6b48 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4697,7 +4697,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, worktree_handle: ModelHandle, - repos: &Vec, + repos: &HashMap, LocalRepositoryEntry>, cx: &mut ModelContext, ) { debug_assert!(worktree_handle.read(cx).is_local()); @@ -4715,15 +4715,16 @@ impl Project { let path = file.path().clone(); let worktree = worktree_handle.read(cx); - let repo = match repos + + let (work_directory, repo) = match repos .iter() - .find(|repository| repository.work_directory.contains(worktree, &path)) + .find(|(work_directory, _)| path.starts_with(work_directory)) { Some(repo) => repo.clone(), None => return, }; - let relative_repo = match repo.work_directory.relativize(worktree, &path) { + let relative_repo = match path.strip_prefix(work_directory).log_err() { Some(relative_repo) => relative_repo.to_owned(), None => return, }; @@ -4732,12 +4733,10 @@ impl Project { let remote_id = self.remote_id(); let client = self.client.clone(); - let diff_base_task = worktree_handle.update(cx, move |worktree, cx| { - worktree - .as_local() - .unwrap() - .load_index_text(repo, relative_repo, cx) - }); + let git_ptr = repo.repo_ptr.clone(); + let diff_base_task = cx + .background() + .spawn(async move { git_ptr.lock().load_index_text(&relative_repo) }); cx.spawn(|_, mut cx| async move { let diff_base = diff_base_task.await; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 12388dfb957f50a53f2b11790b7c528bceef402a..af6576375a6fb5c282a6db3814d7326abca34a79 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -119,7 +119,6 @@ pub struct Snapshot { #[derive(Clone, Debug, Eq, PartialEq)] pub struct RepositoryEntry { - pub(crate) scan_id: usize, pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, } @@ -147,7 +146,6 @@ impl RepositoryEntry { impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { - scan_id: value.scan_id as u64, work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } @@ -235,6 +233,7 @@ pub struct LocalSnapshot { #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { + pub(crate) scan_id: usize, pub(crate) repo_ptr: Arc>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file @@ -281,7 +280,7 @@ struct ShareState { pub enum Event { UpdatedEntries(HashMap, PathChange>), - UpdatedGitRepositories(Vec), + UpdatedGitRepositories(HashMap, LocalRepositoryEntry>), } impl Entity for Worktree { @@ -690,10 +689,8 @@ impl LocalWorktree { } fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext) { - let updated_repos = Self::changed_repos( - &self.snapshot.repository_entries, - &new_snapshot.repository_entries, - ); + let updated_repos = + self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories); self.snapshot = new_snapshot; if let Some(share) = self.share.as_mut() { @@ -706,32 +703,57 @@ impl LocalWorktree { } fn changed_repos( - old_repos: &TreeMap, - new_repos: &TreeMap, - ) -> Vec { - fn diff<'a>( - a: impl Iterator, - mut b: impl Iterator, - updated: &mut HashMap, - ) { - for a_repo in a { - let matched = b.find(|b_repo| { - a_repo.work_directory == b_repo.work_directory - && a_repo.scan_id == b_repo.scan_id - }); + &self, + old_repos: &TreeMap, + new_repos: &TreeMap, + ) -> HashMap, LocalRepositoryEntry> { + let mut diff = HashMap::default(); + let mut old_repos = old_repos.iter().peekable(); + let mut new_repos = new_repos.iter().peekable(); + loop { + match (old_repos.peek(), new_repos.peek()) { + (Some((old_entry_id, old_repo)), Some((new_entry_id, new_repo))) => { + match Ord::cmp(old_entry_id, new_entry_id) { + Ordering::Less => { + if let Some(entry) = self.entry_for_id(**old_entry_id) { + diff.insert(entry.path.clone(), (*old_repo).clone()); + } + old_repos.next(); + } + Ordering::Equal => { + if old_repo.scan_id != new_repo.scan_id { + if let Some(entry) = self.entry_for_id(**new_entry_id) { + diff.insert(entry.path.clone(), (*new_repo).clone()); + } + } - if matched.is_none() { - updated.insert(*a_repo.work_directory, a_repo.clone()); + old_repos.next(); + new_repos.next(); + } + Ordering::Greater => { + if let Some(entry) = self.entry_for_id(**new_entry_id) { + diff.insert(entry.path.clone(), (*new_repo).clone()); + } + new_repos.next(); + } + } + } + (Some((old_entry_id, old_repo)), None) => { + if let Some(entry) = self.entry_for_id(**old_entry_id) { + diff.insert(entry.path.clone(), (*old_repo).clone()); + } + old_repos.next(); } + (None, Some((new_entry_id, new_repo))) => { + if let Some(entry) = self.entry_for_id(**new_entry_id) { + diff.insert(entry.path.clone(), (*new_repo).clone()); + } + new_repos.next(); + } + (None, None) => break, } } - - let mut updated = HashMap::::default(); - - diff(old_repos.values(), new_repos.values(), &mut updated); - diff(new_repos.values(), old_repos.values(), &mut updated); - - updated.into_values().collect() + diff } pub fn scan_complete(&self) -> impl Future { @@ -1166,21 +1188,6 @@ impl LocalWorktree { pub fn is_shared(&self) -> bool { self.share.is_some() } - - pub fn load_index_text( - &self, - repo: RepositoryEntry, - repo_path: RepoPath, - cx: &mut ModelContext, - ) -> Task> { - let Some(git_ptr) = self.git_repositories.get(&repo.work_directory).map(|git_ptr| git_ptr.to_owned()) else { - return Task::Ready(Some(None)) - }; - let git_ptr = git_ptr.repo_ptr; - - cx.background() - .spawn(async move { git_ptr.lock().load_index_text(&repo_path) }) - } } impl RemoteWorktree { @@ -1419,7 +1426,6 @@ impl Snapshot { for repository in update.updated_repositories { let repository = RepositoryEntry { work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), - scan_id: repository.scan_id as usize, branch: repository.branch.map(Into::into), }; if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { @@ -1584,20 +1590,12 @@ impl LocalSnapshot { pub(crate) fn repo_for_metadata( &self, path: &Path, - ) -> Option<(RepositoryWorkDirectory, Arc>)> { + ) -> Option<(ProjectEntryId, Arc>)> { let (entry_id, local_repo) = self .git_repositories .iter() .find(|(_, repo)| repo.in_dot_git(path))?; - - let work_dir = self - .snapshot - .repository_entries - .iter() - .find(|(_, entry)| *entry.work_directory == *entry_id) - .and_then(|(_, entry)| entry.work_directory(self))?; - - Some((work_dir, local_repo.repo_ptr.to_owned())) + Some((*entry_id, local_repo.repo_ptr.to_owned())) } #[cfg(test)] @@ -1686,7 +1684,7 @@ impl LocalSnapshot { self_repos.next(); } Ordering::Equal => { - if self_repo.scan_id != other_repo.scan_id { + if self_repo != other_repo { updated_repositories.push((*self_repo).into()); } @@ -1811,21 +1809,19 @@ impl LocalSnapshot { if parent_path.file_name() == Some(&DOT_GIT) { let abs_path = self.abs_path.join(&parent_path); - let content_path: Arc = parent_path.parent().unwrap().into(); + let work_dir: Arc = parent_path.parent().unwrap().into(); - if let Some(work_dir_id) = self - .entry_for_path(content_path.clone()) - .map(|entry| entry.id) - { - let key = RepositoryWorkDirectory(content_path.clone()); - if self.repository_entries.get(&key).is_none() { + if let Some(work_dir_id) = self.entry_for_path(work_dir.clone()).map(|entry| entry.id) { + if self.git_repositories.get(&work_dir_id).is_none() { if let Some(repo) = fs.open_repo(abs_path.as_path()) { + let work_directory = RepositoryWorkDirectory(work_dir.clone()); + let repo_lock = repo.lock(); + let scan_id = self.scan_id; self.repository_entries.insert( - key.clone(), + work_directory, RepositoryEntry { work_directory: work_dir_id.into(), - scan_id: 0, branch: repo_lock.branch_name().map(Into::into), }, ); @@ -1834,6 +1830,7 @@ impl LocalSnapshot { self.git_repositories.insert( work_dir_id, LocalRepositoryEntry { + scan_id, repo_ptr: repo, git_dir_path: parent_path.clone(), }, @@ -1899,11 +1896,6 @@ impl LocalSnapshot { { *scan_id = self.snapshot.scan_id; } - } else if path.file_name() == Some(&DOT_GIT) { - let repo_entry_key = RepositoryWorkDirectory(path.parent().unwrap().into()); - self.snapshot - .repository_entries - .update(&repo_entry_key, |repo| repo.scan_id = self.snapshot.scan_id); } } @@ -2836,15 +2828,22 @@ impl BackgroundScanner { let scan_id = snapshot.scan_id; let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); - if let Some((key, repo)) = repo_with_path_in_dotgit { + if let Some((entry_id, repo)) = repo_with_path_in_dotgit { + let work_dir = snapshot + .entry_for_id(entry_id) + .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; + let repo = repo.lock(); repo.reload_index(); let branch = repo.branch_name(); - snapshot.repository_entries.update(&key, |entry| { + snapshot.git_repositories.update(&entry_id, |entry| { entry.scan_id = scan_id; - entry.branch = branch.map(Into::into) }); + + snapshot + .repository_entries + .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); } if let Some(scan_queue_tx) = &scan_queue_tx { @@ -3640,26 +3639,27 @@ mod tests { ); }); - let original_scan_id = tree.read_with(cx, |tree, _cx| { - let tree = tree.as_local().unwrap(); - let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - entry.scan_id + let repo_update_events = Arc::new(Mutex::new(vec![])); + tree.update(cx, |_, cx| { + let repo_update_events = repo_update_events.clone(); + cx.subscribe(&tree, move |_, _, event, _| { + if let Event::UpdatedGitRepositories(update) = event { + repo_update_events.lock().push(update.clone()); + } + }) + .detach(); }); std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _cx| { - let tree = tree.as_local().unwrap(); - let new_scan_id = { - let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - entry.scan_id - }; - assert_ne!( - original_scan_id, new_scan_id, - "original {original_scan_id}, new {new_scan_id}" - ); - }); + assert_eq!( + repo_update_events.lock()[0] + .keys() + .cloned() + .collect::>>(), + vec![Path::new("dir1").into()] + ); std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); tree.flush_fs_events(cx).await; @@ -3671,70 +3671,70 @@ mod tests { }); } - #[test] - fn test_changed_repos() { - fn fake_entry(work_dir_id: usize, scan_id: usize) -> RepositoryEntry { - RepositoryEntry { - scan_id, - work_directory: ProjectEntryId(work_dir_id).into(), - branch: None, - } - } - - let mut prev_repos = TreeMap::::default(); - prev_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-1").into()), - fake_entry(1, 0), - ); - prev_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-2").into()), - fake_entry(2, 0), - ); - prev_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-3").into()), - fake_entry(3, 0), - ); - - let mut new_repos = TreeMap::::default(); - new_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-4").into()), - fake_entry(2, 1), - ); - new_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-5").into()), - fake_entry(3, 0), - ); - new_repos.insert( - RepositoryWorkDirectory(Path::new("don't-care-6").into()), - fake_entry(4, 0), - ); - - let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); - - // Deletion retained - assert!(res - .iter() - .find(|repo| repo.work_directory.0 .0 == 1 && repo.scan_id == 0) - .is_some()); - - // Update retained - assert!(res - .iter() - .find(|repo| repo.work_directory.0 .0 == 2 && repo.scan_id == 1) - .is_some()); - - // Addition retained - assert!(res - .iter() - .find(|repo| repo.work_directory.0 .0 == 4 && repo.scan_id == 0) - .is_some()); - - // Nochange, not retained - assert!(res - .iter() - .find(|repo| repo.work_directory.0 .0 == 3 && repo.scan_id == 0) - .is_none()); - } + // #[test] + // fn test_changed_repos() { + // fn fake_entry(work_dir_id: usize, scan_id: usize) -> RepositoryEntry { + // RepositoryEntry { + // scan_id, + // work_directory: ProjectEntryId(work_dir_id).into(), + // branch: None, + // } + // } + + // let mut prev_repos = TreeMap::::default(); + // prev_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-1").into()), + // fake_entry(1, 0), + // ); + // prev_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-2").into()), + // fake_entry(2, 0), + // ); + // prev_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-3").into()), + // fake_entry(3, 0), + // ); + + // let mut new_repos = TreeMap::::default(); + // new_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-4").into()), + // fake_entry(2, 1), + // ); + // new_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-5").into()), + // fake_entry(3, 0), + // ); + // new_repos.insert( + // RepositoryWorkDirectory(Path::new("don't-care-6").into()), + // fake_entry(4, 0), + // ); + + // let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); + + // // Deletion retained + // assert!(res + // .iter() + // .find(|repo| repo.work_directory.0 .0 == 1 && repo.scan_id == 0) + // .is_some()); + + // // Update retained + // assert!(res + // .iter() + // .find(|repo| repo.work_directory.0 .0 == 2 && repo.scan_id == 1) + // .is_some()); + + // // Addition retained + // assert!(res + // .iter() + // .find(|repo| repo.work_directory.0 .0 == 4 && repo.scan_id == 0) + // .is_some()); + + // // Nochange, not retained + // assert!(res + // .iter() + // .find(|repo| repo.work_directory.0 .0 == 3 && repo.scan_id == 0) + // .is_none()); + // } #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 315352b01e1f24a8967255ebb29398e39912ce87..d3b381bc5c499bdf7d8c0f2dede7cced6bf55af8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -982,9 +982,8 @@ message Entry { } message RepositoryEntry { - uint64 scan_id = 1; - uint64 work_directory_id = 2; - optional string branch = 3; + uint64 work_directory_id = 1; + optional string branch = 2; } message BufferState { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 14ab916f6527dfe49296d9b2e72b888b060796e7..564b97335e6046d639894db930da870b40dffa14 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -5,13 +5,13 @@ use futures::{SinkExt as _, StreamExt as _}; use prost::Message as _; use serde::Serialize; use std::any::{Any, TypeId}; -use std::fmt; use std::{ cmp, fmt::Debug, io, iter, time::{Duration, SystemTime, UNIX_EPOCH}, }; +use std::{fmt, mem}; include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); @@ -502,21 +502,22 @@ pub fn split_worktree_update( .drain(..removed_entries_chunk_size) .collect(); - let updated_repositories_chunk_size = - cmp::min(message.updated_repositories.len(), max_chunk_size); - let updated_repositories = message - .updated_repositories - .drain(..updated_repositories_chunk_size) - .collect(); + done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); - let removed_repositories_chunk_size = - cmp::min(message.removed_repositories.len(), max_chunk_size); - let removed_repositories = message - .removed_repositories - .drain(..removed_repositories_chunk_size) - .collect(); + // Wait to send repositories until after we've guarnteed that their associated entries + // will be read + let updated_repositories = if done { + mem::take(&mut message.updated_repositories) + } else { + Default::default() + }; + + let removed_repositories = if done { + mem::take(&mut message.removed_repositories) + } else { + Default::default() + }; - done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); Some(UpdateWorktree { project_id: message.project_id, worktree_id: message.worktree_id, From ab952f1b310ca763e85728fc2a6c5c906b8a0357 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 17:30:54 -0700 Subject: [PATCH 25/34] Fixed randomized test failures co-authored-by: Max --- crates/project/src/worktree.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index af6576375a6fb5c282a6db3814d7326abca34a79..136b0664e3e7a32a9d40651408dbf7a80672d143 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1673,12 +1673,12 @@ impl LocalSnapshot { let mut updated_repositories: Vec = Vec::new(); let mut removed_repositories = Vec::new(); - let mut self_repos = self.snapshot.repository_entries.values().peekable(); - let mut other_repos = other.snapshot.repository_entries.values().peekable(); + let mut self_repos = self.snapshot.repository_entries.iter().peekable(); + let mut other_repos = other.snapshot.repository_entries.iter().peekable(); loop { match (self_repos.peek(), other_repos.peek()) { - (Some(self_repo), Some(other_repo)) => { - match Ord::cmp(&self_repo.work_directory, &other_repo.work_directory) { + (Some((self_work_dir, self_repo)), Some((other_work_dir, other_repo))) => { + match Ord::cmp(self_work_dir, other_work_dir) { Ordering::Less => { updated_repositories.push((*self_repo).into()); self_repos.next(); @@ -1697,11 +1697,11 @@ impl LocalSnapshot { } } } - (Some(self_repo), None) => { + (Some((_, self_repo)), None) => { updated_repositories.push((*self_repo).into()); self_repos.next(); } - (None, Some(other_repo)) => { + (None, Some((_, other_repo))) => { removed_repositories.push(other_repo.work_directory.to_proto()); other_repos.next(); } From 2c2076bd7794af15aa67a661153d5db1bc03eb1a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 5 May 2023 17:49:46 -0700 Subject: [PATCH 26/34] Adjust tests to not create repositories inside repositories --- .../src/tests/randomized_integration_tests.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index c90354f214c55611793c87ea11ae644fc5a748ec..c535bc8dce8cf1c88b41abd6fc3008a80dbaa2d4 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -29,7 +29,7 @@ use std::{ sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, - }, + }, ffi::OsStr, }; use util::ResultExt; @@ -765,7 +765,7 @@ async fn apply_client_operation( contents, } => { if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); + client.fs.create_dir(&repo_path).await?; } log::info!( @@ -791,7 +791,7 @@ async fn apply_client_operation( new_branch, } => { if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); + client.fs.create_dir(&repo_path).await?; } log::info!( @@ -1704,6 +1704,12 @@ impl TestPlan { .unwrap() .clone(); + let repo_path = if repo_path.file_name() == Some(&OsStr::new(".git")) { + repo_path.parent().unwrap().join(Alphanumeric.sample_string(&mut self.rng, 16)) + } else { + repo_path + }; + let mut file_paths = client .fs .files() @@ -1739,6 +1745,12 @@ impl TestPlan { .unwrap() .clone(); + let repo_path = if repo_path.file_name() == Some(&OsStr::new(".git")) { + repo_path.parent().unwrap().join(Alphanumeric.sample_string(&mut self.rng, 16)) + } else { + repo_path + }; + let new_branch = (self.rng.gen_range(0..10) > 3) .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); From f9e446465857a09c78a189c398e4f573efcddab8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 07:59:58 -0700 Subject: [PATCH 27/34] Refresh titlebar on project notifications --- crates/collab_ui/src/collab_titlebar_item.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index ec5eaab36c6fd03587a943ec67c431f84ee6f993..a5c41a669f024b531305b2a8ff535c400c9fa11d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -128,6 +128,7 @@ impl CollabTitlebarItem { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify())); subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); subscriptions.push(cx.observe_window_activation(|this, active, cx| { this.window_activation_changed(active, cx) From 62e763d0d329213db76aed8bd2408a3ec80a2cac Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 08:44:41 -0700 Subject: [PATCH 28/34] Removed test modifications, added special case to git initialization for when the repository is inside a .git folder --- .../src/tests/randomized_integration_tests.rs | 24 ++- crates/project/src/worktree.rs | 140 ++++++------------ 2 files changed, 54 insertions(+), 110 deletions(-) diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index c535bc8dce8cf1c88b41abd6fc3008a80dbaa2d4..ed50059e4aa83d699f1703da11abdc563699c6f8 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -29,7 +29,7 @@ use std::{ sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, - }, ffi::OsStr, + }, }; use util::ResultExt; @@ -765,7 +765,7 @@ async fn apply_client_operation( contents, } => { if !client.fs.directories().contains(&repo_path) { - client.fs.create_dir(&repo_path).await?; + return Err(TestError::Inapplicable); } log::info!( @@ -791,7 +791,7 @@ async fn apply_client_operation( new_branch, } => { if !client.fs.directories().contains(&repo_path) { - client.fs.create_dir(&repo_path).await?; + return Err(TestError::Inapplicable); } log::info!( @@ -1700,16 +1700,13 @@ impl TestPlan { let repo_path = client .fs .directories() + .into_iter() + .filter(|dir| dir.ends_with(".git")) + .collect::>() .choose(&mut self.rng) .unwrap() .clone(); - let repo_path = if repo_path.file_name() == Some(&OsStr::new(".git")) { - repo_path.parent().unwrap().join(Alphanumeric.sample_string(&mut self.rng, 16)) - } else { - repo_path - }; - let mut file_paths = client .fs .files() @@ -1741,16 +1738,13 @@ impl TestPlan { let repo_path = client .fs .directories() + .into_iter() + .filter(|dir| dir.ends_with(".git")) + .collect::>() .choose(&mut self.rng) .unwrap() .clone(); - let repo_path = if repo_path.file_name() == Some(&OsStr::new(".git")) { - repo_path.parent().unwrap().join(Alphanumeric.sample_string(&mut self.rng, 16)) - } else { - repo_path - }; - let new_branch = (self.rng.gen_range(0..10) > 3) .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 136b0664e3e7a32a9d40651408dbf7a80672d143..554304f3d32dd4c223807a3ffbc9028bf93478ee 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1808,36 +1808,7 @@ impl LocalSnapshot { } if parent_path.file_name() == Some(&DOT_GIT) { - let abs_path = self.abs_path.join(&parent_path); - let work_dir: Arc = parent_path.parent().unwrap().into(); - - if let Some(work_dir_id) = self.entry_for_path(work_dir.clone()).map(|entry| entry.id) { - if self.git_repositories.get(&work_dir_id).is_none() { - if let Some(repo) = fs.open_repo(abs_path.as_path()) { - let work_directory = RepositoryWorkDirectory(work_dir.clone()); - - let repo_lock = repo.lock(); - let scan_id = self.scan_id; - self.repository_entries.insert( - work_directory, - RepositoryEntry { - work_directory: work_dir_id.into(), - branch: repo_lock.branch_name().map(Into::into), - }, - ); - drop(repo_lock); - - self.git_repositories.insert( - work_dir_id, - LocalRepositoryEntry { - scan_id, - repo_ptr: repo, - git_dir_path: parent_path.clone(), - }, - ) - } - } - } + self.build_repo(parent_path, fs); } let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; @@ -1858,6 +1829,50 @@ impl LocalSnapshot { self.entries_by_id.edit(entries_by_id_edits, &()); } + 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(); + + // Guard against repositories inside the repository metadata + if work_dir + .components() + .find(|component| component.as_os_str() == *DOT_GIT) + .is_some() + { + return None; + }; + + let work_dir_id = self + .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), + }, + ); + drop(repo_lock); + + self.git_repositories.insert( + work_dir_id, + LocalRepositoryEntry { + scan_id, + repo_ptr: repo, + git_dir_path: parent_path.clone(), + }, + ) + } + + Some(()) + } fn reuse_entry_id(&mut self, entry: &mut Entry) { if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { entry.id = removed_entry_id; @@ -3671,71 +3686,6 @@ mod tests { }); } - // #[test] - // fn test_changed_repos() { - // fn fake_entry(work_dir_id: usize, scan_id: usize) -> RepositoryEntry { - // RepositoryEntry { - // scan_id, - // work_directory: ProjectEntryId(work_dir_id).into(), - // branch: None, - // } - // } - - // let mut prev_repos = TreeMap::::default(); - // prev_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-1").into()), - // fake_entry(1, 0), - // ); - // prev_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-2").into()), - // fake_entry(2, 0), - // ); - // prev_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-3").into()), - // fake_entry(3, 0), - // ); - - // let mut new_repos = TreeMap::::default(); - // new_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-4").into()), - // fake_entry(2, 1), - // ); - // new_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-5").into()), - // fake_entry(3, 0), - // ); - // new_repos.insert( - // RepositoryWorkDirectory(Path::new("don't-care-6").into()), - // fake_entry(4, 0), - // ); - - // let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); - - // // Deletion retained - // assert!(res - // .iter() - // .find(|repo| repo.work_directory.0 .0 == 1 && repo.scan_id == 0) - // .is_some()); - - // // Update retained - // assert!(res - // .iter() - // .find(|repo| repo.work_directory.0 .0 == 2 && repo.scan_id == 1) - // .is_some()); - - // // Addition retained - // assert!(res - // .iter() - // .find(|repo| repo.work_directory.0 .0 == 4 && repo.scan_id == 0) - // .is_some()); - - // // Nochange, not retained - // assert!(res - // .iter() - // .find(|repo| repo.work_directory.0 .0 == 3 && repo.scan_id == 0) - // .is_none()); - // } - #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { let dir = temp_tree(json!({ From d2279674a7dd6e69a01a39a5530ed0859ea4c100 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 08:54:06 -0700 Subject: [PATCH 29/34] Fix panic in tests --- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a5c41a669f024b531305b2a8ff535c400c9fa11d..71a62b68ee1bada71be8d1002b2cc91654feba0d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -107,7 +107,7 @@ impl View for CollabTitlebarItem { .with_child( Flex::row() .with_child(right_container.contained().with_background_color( - theme.workspace.titlebar.container.background_color.unwrap(), + theme.workspace.titlebar.container.background_color.unwrap_or_else(|| Color::transparent_black()), )) .aligned() .right(), From 15d2f19b4a18a1534219152d453127491883dd28 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 08:55:20 -0700 Subject: [PATCH 30/34] fix format --- crates/collab_ui/src/collab_titlebar_item.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 71a62b68ee1bada71be8d1002b2cc91654feba0d..117411b8d443b7022ee80d8f2459562c08ca0135 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -106,9 +106,16 @@ impl View for CollabTitlebarItem { .with_child(left_container) .with_child( Flex::row() - .with_child(right_container.contained().with_background_color( - theme.workspace.titlebar.container.background_color.unwrap_or_else(|| Color::transparent_black()), - )) + .with_child( + right_container.contained().with_background_color( + theme + .workspace + .titlebar + .container + .background_color + .unwrap_or_else(|| Color::transparent_black()), + ), + ) .aligned() .right(), ) From 1a9afd186b452a4bdf08cd938d3b0e75218f5fd2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 09:07:25 -0700 Subject: [PATCH 31/34] Restore randomized integration tests --- crates/collab/src/tests/randomized_integration_tests.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index ed50059e4aa83d699f1703da11abdc563699c6f8..d5bd0033f7d53e6b18766db6d58642b5472201c4 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -1701,8 +1701,6 @@ impl TestPlan { .fs .directories() .into_iter() - .filter(|dir| dir.ends_with(".git")) - .collect::>() .choose(&mut self.rng) .unwrap() .clone(); @@ -1738,9 +1736,6 @@ impl TestPlan { let repo_path = client .fs .directories() - .into_iter() - .filter(|dir| dir.ends_with(".git")) - .collect::>() .choose(&mut self.rng) .unwrap() .clone(); From 712fb5ad7f00ffc4a5ca355a948083fc6ca73ac5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 14:27:02 -0700 Subject: [PATCH 32/34] Add postgres migration --- .../20230508211523_add-repository-entries.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 crates/collab/migrations/20230508211523_add-repository-entries.sql diff --git a/crates/collab/migrations/20230508211523_add-repository-entries.sql b/crates/collab/migrations/20230508211523_add-repository-entries.sql new file mode 100644 index 0000000000000000000000000000000000000000..1e593479394c8434f56f3519b41ce2fa2a9fc2a3 --- /dev/null +++ b/crates/collab/migrations/20230508211523_add-repository-entries.sql @@ -0,0 +1,13 @@ +CREATE TABLE "worktree_repositories" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "work_directory_id" INT8 NOT NULL, + "scan_id" INT8 NOT NULL, + "branch" VARCHAR, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, work_directory_id), + 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_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"); From f28419cfd15f78950c394eb3bf5bc6d7e5cf6c05 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 14:32:31 -0700 Subject: [PATCH 33/34] Fix styling of titlebar highlights --- crates/collab_ui/src/collab_titlebar_item.rs | 4 ++-- crates/rpc/src/proto.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 117411b8d443b7022ee80d8f2459562c08ca0135..7374b166ca6e4eb5b2b7aae5304c556fea5ff526 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -201,12 +201,12 @@ impl CollabTitlebarItem { let mut title = String::new(); let mut names_and_branches = names_and_branches.peekable(); while let Some((name, entry)) = names_and_branches.next() { + let pre_index = index; push_str(&mut title, &mut index, name); + indices.extend((pre_index..index).into_iter()); if let Some(branch) = entry.and_then(|entry| entry.branch()) { push_str(&mut title, &mut index, "/"); - let pre_index = index; push_str(&mut title, &mut index, &branch); - indices.extend((pre_index..index).into_iter()) } if names_and_branches.peek().is_some() { push_str(&mut title, &mut index, ", "); diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 564b97335e6046d639894db930da870b40dffa14..20a457cc4b993dc5a8738094fac7db50aa99e438 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -504,7 +504,7 @@ pub fn split_worktree_update( done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); - // Wait to send repositories until after we've guarnteed that their associated entries + // Wait to send repositories until after we've guaranteed that their associated entries // will be read let updated_repositories = if done { mem::take(&mut message.updated_repositories) From 9366a0dbee63ebfe6ee0552b5ef878aeb9a624c2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 May 2023 14:34:14 -0700 Subject: [PATCH 34/34] Bump protocol version number --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b7cb59266c73ccf16a69ca0dce8051c4218e0c8d..e51ded5969c513f8d970c486d3ec3f3104d20d0b 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 53; +pub const PROTOCOL_VERSION: u32 = 54;