WIP: Add repository entries to collab and synchronize when rejoining the room
co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
2fe5bf419b
commit
8301ee43d6
8 changed files with 267 additions and 70 deletions
|
@ -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<Path>,
|
||||
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<Arc<str>>,
|
||||
}
|
||||
|
||||
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<Arc<str>> {
|
||||
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<Path> 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<Path>, (Arc<Gitignore>, usize)>,
|
||||
// The ProjectEntryId corresponds to the entry for the .git dir
|
||||
git_repositories: TreeMap<ProjectEntryId, Arc<Mutex<dyn GitRepository>>>,
|
||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalRepositoryEntry {
|
||||
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
|
||||
/// 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<Path>,
|
||||
}
|
||||
|
||||
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<Item = &'a RepositoryEntry>,
|
||||
mut b: impl Iterator<Item = &'a RepositoryEntry>,
|
||||
updated: &mut HashMap<&'a Path, RepositoryEntry>,
|
||||
updated: &mut HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
) {
|
||||
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::<ProjectEntryId, RepositoryEntry>::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<Worktree>,
|
||||
) -> Task<Option<String>> {
|
||||
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<Mutex<dyn GitRepository>>)> {
|
||||
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<proto::RepositoryEntry> = 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<Path>, 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::<RepositoryWorkDirectory, RepositoryEntry>::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::<RepositoryWorkDirectory, RepositoryEntry>::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());
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue