@@ -1569,8 +1569,8 @@ impl Database {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
- removed_worktree_repo_paths: Default::default(),
- updated_worktree_statuses: Default::default(),
+ removed_repo_paths: Default::default(),
+ updated_statuses: Default::default(),
});
}
}
@@ -1607,15 +1607,13 @@ impl Database {
let db_status_entry = db_status_entry?;
if db_status_entry.is_deleted {
repository
- .removed_worktree_repo_paths
+ .removed_repo_paths
.push(db_status_entry.repo_path);
} else {
- repository
- .updated_worktree_statuses
- .push(proto::StatusEntry {
- repo_path: db_status_entry.repo_path,
- status: db_status_entry.status as i32,
- });
+ repository.updated_statuses.push(proto::StatusEntry {
+ repo_path: db_status_entry.repo_path,
+ status: db_status_entry.status as i32,
+ });
}
}
}
@@ -2444,12 +2442,10 @@ impl Database {
.await?;
for repository in update.updated_repositories.iter() {
- if !repository.updated_worktree_statuses.is_empty() {
+ if !repository.updated_statuses.is_empty() {
worktree_repository_statuses::Entity::insert_many(
- repository
- .updated_worktree_statuses
- .iter()
- .map(|status_entry| worktree_repository_statuses::ActiveModel {
+ repository.updated_statuses.iter().map(|status_entry| {
+ worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
@@ -2459,7 +2455,8 @@ impl Database {
status: ActiveValue::set(status_entry.status as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
- }),
+ }
+ }),
)
.on_conflict(
OnConflict::columns([
@@ -2479,7 +2476,7 @@ impl Database {
.await?;
}
- if !repository.removed_worktree_repo_paths.is_empty() {
+ if !repository.removed_repo_paths.is_empty() {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
@@ -2492,14 +2489,9 @@ impl Database {
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id as i64),
)
- .and(
- worktree_repository_statuses::Column::RepoPath.is_in(
- repository
- .removed_worktree_repo_paths
- .iter()
- .map(String::as_str),
- ),
- ),
+ .and(worktree_repository_statuses::Column::RepoPath.is_in(
+ repository.removed_repo_paths.iter().map(String::as_str),
+ )),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
@@ -2765,8 +2757,8 @@ impl Database {
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
- removed_worktree_repo_paths: Default::default(),
- updated_worktree_statuses: Default::default(),
+ removed_repo_paths: Default::default(),
+ updated_statuses: Default::default(),
},
);
}
@@ -2791,12 +2783,10 @@ impl Database {
.repository_entries
.get_mut(&(db_status_entry.work_directory_id as u64))
{
- repository_entry
- .updated_worktree_statuses
- .push(proto::StatusEntry {
- repo_path: db_status_entry.repo_path,
- status: db_status_entry.status as i32,
- });
+ repository_entry.updated_statuses.push(proto::StatusEntry {
+ repo_path: db_status_entry.repo_path,
+ status: db_status_entry.status as i32,
+ });
}
}
}
@@ -143,7 +143,7 @@ impl Snapshot {
pub struct RepositoryEntry {
pub(crate) work_directory: WorkDirectoryEntry,
pub(crate) branch: Option<Arc<str>>,
- pub(crate) worktree_statuses: TreeMap<RepoPath, GitFileStatus>,
+ pub(crate) statuses: TreeMap<RepoPath, GitFileStatus>,
}
fn read_git_status(git_status: i32) -> Option<GitFileStatus> {
@@ -176,7 +176,7 @@ impl RepositoryEntry {
pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
self.work_directory
.relativize(snapshot, path)
- .and_then(|repo_path| self.worktree_statuses.get(&repo_path))
+ .and_then(|repo_path| self.statuses.get(&repo_path))
.cloned()
}
@@ -184,12 +184,12 @@ impl RepositoryEntry {
self.work_directory
.relativize(snapshot, path)
.and_then(|repo_path| {
- self.worktree_statuses
+ self.statuses
.iter_from(&repo_path)
.take_while(|(key, _)| key.starts_with(&repo_path))
- .map(|(_, status)| status)
// Short circut once we've found the highest level
- .take_until(|status| status == &&GitFileStatus::Conflict)
+ .take_until(|(_, status)| status == &&GitFileStatus::Conflict)
+ .map(|(_, status)| status)
.reduce(
|status_first, status_second| match (status_first, status_second) {
(GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => {
@@ -209,8 +209,8 @@ impl RepositoryEntry {
let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
let mut removed_statuses: Vec<String> = Vec::new();
- let mut self_statuses = self.worktree_statuses.iter().peekable();
- let mut other_statuses = other.worktree_statuses.iter().peekable();
+ let mut self_statuses = self.statuses.iter().peekable();
+ let mut other_statuses = other.statuses.iter().peekable();
loop {
match (self_statuses.peek(), other_statuses.peek()) {
(Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => {
@@ -249,8 +249,8 @@ impl RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: self.work_directory_id().to_proto(),
branch: self.branch.as_ref().map(|str| str.to_string()),
- removed_worktree_repo_paths: removed_statuses,
- updated_worktree_statuses: updated_statuses,
+ removed_repo_paths: removed_statuses,
+ updated_statuses: updated_statuses,
}
}
}
@@ -275,12 +275,12 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: value.work_directory.to_proto(),
branch: value.branch.as_ref().map(|str| str.to_string()),
- updated_worktree_statuses: value
- .worktree_statuses
+ updated_statuses: value
+ .statuses
.iter()
.map(|(repo_path, status)| make_status_entry(repo_path, status))
.collect(),
- removed_worktree_repo_paths: Default::default(),
+ removed_repo_paths: Default::default(),
}
}
}
@@ -1546,7 +1546,7 @@ impl Snapshot {
if let Some(entry) = self.entry_for_id(*work_directory_entry) {
let mut statuses = TreeMap::default();
- for status_entry in repository.updated_worktree_statuses {
+ for status_entry in repository.updated_statuses {
let Some(git_file_status) = read_git_status(status_entry.status) else {
continue;
};
@@ -1559,11 +1559,11 @@ impl Snapshot {
if self.repository_entries.get(&work_directory).is_some() {
self.repository_entries.update(&work_directory, |repo| {
repo.branch = repository.branch.map(Into::into);
- repo.worktree_statuses.insert_tree(statuses);
+ repo.statuses.insert_tree(statuses);
- for repo_path in repository.removed_worktree_repo_paths {
+ for repo_path in repository.removed_repo_paths {
let repo_path = RepoPath::new(repo_path.into());
- repo.worktree_statuses.remove(&repo_path);
+ repo.statuses.remove(&repo_path);
}
});
} else {
@@ -1572,7 +1572,7 @@ impl Snapshot {
RepositoryEntry {
work_directory: work_directory_entry,
branch: repository.branch.map(Into::into),
- worktree_statuses: statuses,
+ statuses,
},
)
}
@@ -1669,6 +1669,30 @@ impl Snapshot {
}
}
+ fn descendent_entries<'a>(
+ &'a self,
+ include_dirs: bool,
+ include_ignored: bool,
+ parent_path: &'a Path,
+ ) -> DescendentEntriesIter<'a> {
+ let mut cursor = self.entries_by_path.cursor();
+ cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
+ let mut traversal = Traversal {
+ cursor,
+ include_dirs,
+ include_ignored,
+ };
+
+ if traversal.end_offset() == traversal.start_offset() {
+ traversal.advance();
+ }
+
+ DescendentEntriesIter {
+ traversal,
+ parent_path,
+ }
+ }
+
pub fn root_entry(&self) -> Option<&Entry> {
self.entry_for_path("")
}
@@ -1988,7 +2012,7 @@ impl LocalSnapshot {
RepositoryEntry {
work_directory: work_dir_id.into(),
branch: repo_lock.branch_name().map(Into::into),
- worktree_statuses: repo_lock.worktree_statuses().unwrap_or_default(),
+ statuses: repo_lock.statuses().unwrap_or_default(),
},
);
drop(repo_lock);
@@ -2670,14 +2694,13 @@ impl BackgroundScanner {
async fn process_events(&mut self, paths: Vec<PathBuf>) {
let (scan_job_tx, scan_job_rx) = channel::unbounded();
- if let Some(mut paths) = self
+ let paths = self
.reload_entries_for_paths(paths, Some(scan_job_tx.clone()))
- .await
- {
- paths.sort_unstable();
+ .await;
+ if let Some(paths) = &paths {
util::extend_sorted(
&mut self.prev_state.lock().event_paths,
- paths,
+ paths.iter().cloned(),
usize::MAX,
Ord::cmp,
);
@@ -2689,6 +2712,12 @@ impl BackgroundScanner {
let mut snapshot = self.snapshot.lock();
+ if let Some(paths) = paths {
+ for path in paths {
+ self.reload_repo_for_file_path(&path, &mut *snapshot, self.fs.as_ref());
+ }
+ }
+
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
git_repositories.retain(|work_directory_id, _| {
snapshot
@@ -2999,8 +3028,6 @@ impl BackgroundScanner {
fs_entry.is_ignored = ignore_stack.is_all();
snapshot.insert_entry(fs_entry, self.fs.as_ref());
- self.reload_repo_for_path(&path, &mut snapshot, self.fs.as_ref());
-
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) {
@@ -3048,7 +3075,7 @@ impl BackgroundScanner {
snapshot.repository_entries.update(&work_dir, |entry| {
entry
- .worktree_statuses
+ .statuses
.remove_range(&repo_path, &RepoPathDescendants(&repo_path))
});
}
@@ -3056,7 +3083,7 @@ impl BackgroundScanner {
Some(())
}
- fn reload_repo_for_path(
+ fn reload_repo_for_file_path(
&self,
path: &Path,
snapshot: &mut LocalSnapshot,
@@ -3090,7 +3117,7 @@ impl BackgroundScanner {
let repo = repo_ptr.lock();
repo.reload_index();
let branch = repo.branch_name();
- let statuses = repo.worktree_statuses().unwrap_or_default();
+ let statuses = repo.statuses().unwrap_or_default();
snapshot.git_repositories.update(&entry_id, |entry| {
entry.scan_id = scan_id;
@@ -3099,7 +3126,7 @@ impl BackgroundScanner {
snapshot.repository_entries.update(&work_dir, |entry| {
entry.branch = branch.map(Into::into);
- entry.worktree_statuses = statuses;
+ entry.statuses = statuses;
});
} else {
if snapshot
@@ -3113,34 +3140,36 @@ impl BackgroundScanner {
let repo = snapshot.repo_for(&path)?;
- let repo_path = repo.work_directory.relativize(&snapshot, &path)?;
-
- let status = {
- let local_repo = snapshot.get_local_repo(&repo)?;
-
- // Short circuit if we've already scanned everything
- if local_repo.full_scan_id == scan_id {
- return None;
- }
-
- let git_ptr = local_repo.repo_ptr.lock();
- git_ptr.worktree_status(&repo_path)
- };
-
let work_dir = repo.work_directory(snapshot)?;
- let work_dir_id = repo.work_directory;
+ let work_dir_id = repo.work_directory.clone();
snapshot
.git_repositories
.update(&work_dir_id, |entry| entry.scan_id = scan_id);
- snapshot.repository_entries.update(&work_dir, |entry| {
+ let local_repo = snapshot.get_local_repo(&repo)?.to_owned();
+
+ // Short circuit if we've already scanned everything
+ if local_repo.full_scan_id == scan_id {
+ return None;
+ }
+
+ let mut repository = snapshot.repository_entries.remove(&work_dir)?;
+
+ for entry in snapshot.descendent_entries(false, false, path) {
+ let Some(repo_path) = repo.work_directory.relativize(snapshot, &entry.path) else {
+ continue;
+ };
+
+ let status = local_repo.repo_ptr.lock().status(&repo_path);
if let Some(status) = status {
- entry.worktree_statuses.insert(repo_path, status);
+ repository.statuses.insert(repo_path.clone(), status);
} else {
- entry.worktree_statuses.remove(&repo_path);
+ repository.statuses.remove(&repo_path);
}
- });
+ }
+
+ snapshot.repository_entries.insert(work_dir, repository)
}
Some(())
@@ -3475,17 +3504,13 @@ pub struct Traversal<'a> {
impl<'a> Traversal<'a> {
pub fn advance(&mut self) -> bool {
- self.advance_to_offset(self.offset() + 1)
- }
-
- pub fn advance_to_offset(&mut self, offset: usize) -> bool {
self.cursor.seek_forward(
&TraversalTarget::Count {
- count: offset,
+ count: self.end_offset() + 1,
include_dirs: self.include_dirs,
include_ignored: self.include_ignored,
},
- Bias::Right,
+ Bias::Left,
&(),
)
}
@@ -3512,11 +3537,17 @@ impl<'a> Traversal<'a> {
self.cursor.item()
}
- pub fn offset(&self) -> usize {
+ pub fn start_offset(&self) -> usize {
self.cursor
.start()
.count(self.include_dirs, self.include_ignored)
}
+
+ pub fn end_offset(&self) -> usize {
+ self.cursor
+ .end(&())
+ .count(self.include_dirs, self.include_ignored)
+ }
}
impl<'a> Iterator for Traversal<'a> {
@@ -3585,6 +3616,25 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
}
}
+struct DescendentEntriesIter<'a> {
+ parent_path: &'a Path,
+ traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for DescendentEntriesIter<'a> {
+ type Item = &'a Entry;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(item) = self.traversal.entry() {
+ if item.path.starts_with(&self.parent_path) {
+ self.traversal.advance();
+ return Some(item);
+ }
+ }
+ None
+ }
+}
+
impl<'a> From<&'a Entry> for proto::Entry {
fn from(entry: &'a Entry) -> Self {
Self {
@@ -3699,6 +3749,105 @@ mod tests {
})
}
+ #[gpui::test]
+ async fn test_descendent_entries(cx: &mut TestAppContext) {
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "a": "",
+ "b": {
+ "c": {
+ "d": ""
+ },
+ "e": {}
+ },
+ "f": "",
+ "g": {
+ "h": {}
+ },
+ "i": {
+ "j": {
+ "k": ""
+ },
+ "l": {
+
+ }
+ },
+ ".gitignore": "i/j\n",
+ }),
+ )
+ .await;
+
+ let http_client = FakeHttpClient::with_404_response();
+ let client = cx.read(|cx| Client::new(http_client, cx));
+
+ let tree = Worktree::local(
+ client,
+ Path::new("/root"),
+ true,
+ fs,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("b"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("b/c/d"),]
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("b"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new("b"),
+ Path::new("b/c"),
+ Path::new("b/c/d"),
+ Path::new("b/e"),
+ ]
+ );
+
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("g"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ Vec::<PathBuf>::new()
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("g"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("g"), Path::new("g/h"),]
+ );
+
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ Vec::<PathBuf>::new()
+ );
+ assert_eq!(
+ tree.descendent_entries(false, true, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("i/j/k")]
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("i"), Path::new("i/l"),]
+ );
+ })
+ }
+
#[gpui::test(iterations = 10)]
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.background());
@@ -4095,17 +4244,17 @@ mod tests {
let (dir, repo) = snapshot.repository_entries.iter().next().unwrap();
assert_eq!(dir.0.as_ref(), Path::new("project"));
- assert_eq!(repo.worktree_statuses.iter().count(), 3);
+ assert_eq!(repo.statuses.iter().count(), 3);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(A_TXT).into()),
+ repo.statuses.get(&Path::new(A_TXT).into()),
Some(&GitFileStatus::Modified)
);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(B_TXT).into()),
+ repo.statuses.get(&Path::new(B_TXT).into()),
Some(&GitFileStatus::Added)
);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(F_TXT).into()),
+ repo.statuses.get(&Path::new(F_TXT).into()),
Some(&GitFileStatus::Added)
);
});
@@ -4120,11 +4269,9 @@ mod tests {
let snapshot = tree.snapshot();
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
- assert_eq!(repo.worktree_statuses.iter().count(), 1);
- assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None);
- assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None);
+ assert_eq!(repo.statuses.iter().count(), 1);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(F_TXT).into()),
+ repo.statuses.get(&Path::new(F_TXT).into()),
Some(&GitFileStatus::Added)
);
});
@@ -4141,18 +4288,18 @@ mod tests {
let snapshot = tree.snapshot();
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
- assert_eq!(repo.worktree_statuses.iter().count(), 3);
- assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None);
+ assert_eq!(repo.statuses.iter().count(), 3);
+ assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(B_TXT).into()),
+ repo.statuses.get(&Path::new(B_TXT).into()),
Some(&GitFileStatus::Added)
);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(E_TXT).into()),
+ repo.statuses.get(&Path::new(E_TXT).into()),
Some(&GitFileStatus::Modified)
);
assert_eq!(
- repo.worktree_statuses.get(&Path::new(F_TXT).into()),
+ repo.statuses.get(&Path::new(F_TXT).into()),
Some(&GitFileStatus::Added)
);
});
@@ -4170,20 +4317,58 @@ mod tests {
tree.flush_fs_events(cx).await;
- dbg!(git_status(&repo));
-
// Check that non-repo behavior is tracked
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
- dbg!(&repo.worktree_statuses);
+ assert_eq!(repo.statuses.iter().count(), 0);
+ });
+
+ let mut renamed_dir_name = "first_directory/second_directory";
+ const RENAMED_FILE: &'static str = "rf.txt";
+
+ std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
+ std::fs::write(
+ work_dir.join(renamed_dir_name).join(RENAMED_FILE),
+ "new-contents",
+ )
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
+
+ assert_eq!(repo.statuses.iter().count(), 1);
+ assert_eq!(
+ repo.statuses
+ .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
+ Some(&GitFileStatus::Added)
+ );
+ });
+
+ renamed_dir_name = "new_first_directory/second_directory";
- assert_eq!(repo.worktree_statuses.iter().count(), 0);
- assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None);
- assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None);
- assert_eq!(repo.worktree_statuses.get(&Path::new(E_TXT).into()), None);
- assert_eq!(repo.worktree_statuses.get(&Path::new(F_TXT).into()), None);
+ std::fs::rename(
+ work_dir.join("first_directory"),
+ work_dir.join("new_first_directory"),
+ )
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
+
+ assert_eq!(repo.statuses.iter().count(), 1);
+ assert_eq!(
+ repo.statuses
+ .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
+ Some(&GitFileStatus::Added)
+ );
});
}