Merge remote-tracking branch 'origin/main' into room
This commit is contained in:
commit
afaacba41f
92 changed files with 10800 additions and 6586 deletions
|
@ -10,6 +10,7 @@ doctest = false
|
|||
[features]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"db/test-support",
|
||||
"language/test-support",
|
||||
"settings/test-support",
|
||||
"text/test-support",
|
||||
|
@ -20,8 +21,10 @@ text = { path = "../text" }
|
|||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
|
@ -54,6 +57,7 @@ rocksdb = "0.18"
|
|||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
db = { path = "../db", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Db(DbStore);
|
||||
|
||||
enum DbStore {
|
||||
Null,
|
||||
Real(rocksdb::DB),
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Fake {
|
||||
data: parking_lot::Mutex<collections::HashMap<Vec<u8>, Vec<u8>>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Db {
|
||||
/// Open or create a database at the given file path.
|
||||
pub fn open(path: &Path) -> Result<Arc<Self>> {
|
||||
let db = rocksdb::DB::open_default(path)?;
|
||||
Ok(Arc::new(Self(DbStore::Real(db))))
|
||||
}
|
||||
|
||||
/// Open a null database that stores no data, for use as a fallback
|
||||
/// when there is an error opening the real database.
|
||||
pub fn null() -> Arc<Self> {
|
||||
Arc::new(Self(DbStore::Null))
|
||||
}
|
||||
|
||||
/// Open a fake database for testing.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_fake() -> Arc<Self> {
|
||||
Arc::new(Self(DbStore::Fake {
|
||||
data: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn read<K, I>(&self, keys: I) -> Result<Vec<Option<Vec<u8>>>>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = K>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => db
|
||||
.multi_get(keys)
|
||||
.into_iter()
|
||||
.map(|e| e.map_err(Into::into))
|
||||
.collect(),
|
||||
|
||||
DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()),
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let db = db.lock();
|
||||
Ok(keys
|
||||
.into_iter()
|
||||
.map(|key| db.get(key.as_ref()).cloned())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete<K, I>(&self, keys: I) -> Result<()>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = K>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => {
|
||||
let mut batch = rocksdb::WriteBatch::default();
|
||||
for key in keys {
|
||||
batch.delete(key);
|
||||
}
|
||||
db.write(batch)?;
|
||||
}
|
||||
|
||||
DbStore::Null => {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let mut db = db.lock();
|
||||
for key in keys {
|
||||
db.remove(key.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write<K, V, I>(&self, entries: I) -> Result<()>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
V: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => {
|
||||
let mut batch = rocksdb::WriteBatch::default();
|
||||
for (key, value) in entries {
|
||||
batch.put(key, value);
|
||||
}
|
||||
db.write(batch)?;
|
||||
}
|
||||
|
||||
DbStore::Null => {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let mut db = db.lock();
|
||||
for (key, value) in entries {
|
||||
db.insert(key.as_ref().into(), value.as_ref().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_db() {
|
||||
let dir = TempDir::new("db-test").unwrap();
|
||||
let fake_db = Db::open_fake();
|
||||
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
|
||||
|
||||
for db in [&real_db, &fake_db] {
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[None, None, None]
|
||||
);
|
||||
|
||||
db.write([("key-1", "one"), ("key-3", "three")]).unwrap();
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[
|
||||
Some("one".as_bytes().to_vec()),
|
||||
None,
|
||||
Some("three".as_bytes().to_vec())
|
||||
]
|
||||
);
|
||||
|
||||
db.delete(["key-3", "key-4"]).unwrap();
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[Some("one".as_bytes().to_vec()), None, None,]
|
||||
);
|
||||
}
|
||||
|
||||
drop(real_db);
|
||||
|
||||
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
|
||||
assert_eq!(
|
||||
real_db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[Some("one".as_bytes().to_vec()), None, None,]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use fsevent::EventStream;
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use git::repository::{GitRepository, LibGitRepository};
|
||||
use language::LineEnding;
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io,
|
||||
os::unix::fs::MetadataExt,
|
||||
|
@ -11,13 +14,16 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
use text::Rope;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::lock::Mutex;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::{Arc, Weak};
|
||||
use git::repository::FakeGitRepositoryState;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Weak;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
|
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
|
|||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_fake(&self) -> &FakeFs;
|
||||
|
@ -235,6 +242,14 @@ impl Fs for RealFs {
|
|||
})))
|
||||
}
|
||||
|
||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||
LibGitRepository::open(&dotgit_path)
|
||||
.log_err()
|
||||
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
|
||||
Some(Arc::new(SyncMutex::new(libgit_repository)))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -270,6 +285,7 @@ enum FakeFsEntry {
|
|||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
|
||||
},
|
||||
Symlink {
|
||||
target: PathBuf,
|
||||
|
@ -384,6 +400,7 @@ impl FakeFs {
|
|||
inode: 0,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
})),
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
|
@ -473,6 +490,28 @@ impl FakeFs {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
let mut state = self.state.lock().await;
|
||||
let entry = state.read_path(dot_git).await.unwrap();
|
||||
let mut entry = entry.lock().await;
|
||||
|
||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||
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())),
|
||||
);
|
||||
|
||||
state.emit_event([dot_git]);
|
||||
} else {
|
||||
panic!("not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn files(&self) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
|
@ -562,6 +601,7 @@ impl Fs for FakeFs {
|
|||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
}))
|
||||
});
|
||||
Ok(())
|
||||
|
@ -846,6 +886,24 @@ impl Fs for FakeFs {
|
|||
}))
|
||||
}
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||
smol::block_on(async move {
|
||||
let state = self.state.lock().await;
|
||||
let entry = state.read_path(abs_dot_git).await.unwrap();
|
||||
let mut entry = entry.lock().await;
|
||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||
let state = git_repo_state
|
||||
.get_or_insert_with(|| {
|
||||
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
|
||||
})
|
||||
.clone();
|
||||
Some(git::repository::FakeGitRepository::open(state))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod db;
|
||||
pub mod fs;
|
||||
mod ignore;
|
||||
mod lsp_command;
|
||||
|
@ -13,6 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
|||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||
|
@ -62,7 +62,7 @@ use std::{
|
|||
time::Instant,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
||||
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
|
||||
|
||||
pub use db::Db;
|
||||
pub use fs::*;
|
||||
|
@ -123,6 +123,7 @@ pub struct Project {
|
|||
opened_buffers: HashMap<u64, OpenBuffer>,
|
||||
incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>,
|
||||
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
||||
buffers_being_formatted: HashSet<usize>,
|
||||
nonce: u128,
|
||||
_maintain_buffer_languages: Task<()>,
|
||||
}
|
||||
|
@ -407,6 +408,7 @@ impl Project {
|
|||
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||
client.add_model_request_handler(Self::handle_save_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diff_base);
|
||||
}
|
||||
|
||||
pub fn local(
|
||||
|
@ -466,6 +468,7 @@ impl Project {
|
|||
language_server_statuses: Default::default(),
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_settings: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
next_language_server_id: 0,
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
}
|
||||
|
@ -562,6 +565,7 @@ impl Project {
|
|||
last_workspace_edits_by_language_server: Default::default(),
|
||||
next_language_server_id: 0,
|
||||
opened_buffers: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
};
|
||||
|
@ -604,7 +608,7 @@ impl Project {
|
|||
|
||||
let languages = Arc::new(LanguageRegistry::test());
|
||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||
let client = client::Client::new(http_client.clone());
|
||||
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||
let project =
|
||||
|
@ -2804,7 +2808,26 @@ impl Project {
|
|||
.await?;
|
||||
}
|
||||
|
||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
||||
// Do not allow multiple concurrent formatting requests for the
|
||||
// same buffer.
|
||||
this.update(&mut cx, |this, _| {
|
||||
local_buffers
|
||||
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
|
||||
});
|
||||
let _cleanup = defer({
|
||||
let this = this.clone();
|
||||
let mut cx = cx.clone();
|
||||
let local_buffers = &local_buffers;
|
||||
move || {
|
||||
this.update(&mut cx, |this, _| {
|
||||
for (buffer, _, _) in local_buffers {
|
||||
this.buffers_being_formatted.remove(&buffer.id());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (buffer, buffer_abs_path, language_server) in &local_buffers {
|
||||
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
|
@ -2856,7 +2879,7 @@ impl Project {
|
|||
buffer.forget_transaction(transaction.id)
|
||||
});
|
||||
}
|
||||
project_transaction.0.insert(buffer, transaction);
|
||||
project_transaction.0.insert(buffer.clone(), transaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4229,8 +4252,11 @@ impl Project {
|
|||
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, _, cx| {
|
||||
this.update_local_worktree_buffers(worktree, cx);
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -4338,6 +4364,63 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_local_worktree_buffers_git_repos(
|
||||
&mut self,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
repos: &[GitRepositoryEntry],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
Some(file) => file,
|
||||
None => continue,
|
||||
};
|
||||
if file.worktree != worktree {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
|
||||
Some(repo) => repo.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let relative_repo = match path.strip_prefix(repo.content_path) {
|
||||
Ok(relative_repo) => relative_repo.to_owned(),
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = cx
|
||||
.background()
|
||||
.spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
|
||||
.await;
|
||||
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.update_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
|
@ -4861,6 +4944,27 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_update_diff_base(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateDiffBase>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer_id = envelope.payload.buffer_id;
|
||||
let diff_base = envelope.payload.diff_base;
|
||||
let buffer = this
|
||||
.opened_buffers
|
||||
.get_mut(&buffer_id)
|
||||
.and_then(|b| b.upgrade(cx))
|
||||
.ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?;
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.update_diff_base(diff_base, cx));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_buffer_file(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
||||
|
@ -5427,7 +5531,7 @@ impl Project {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let mut opened_buffer_rx = self.opened_buffer.1.clone();
|
||||
cx.spawn(|this, cx| async move {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let buffer = loop {
|
||||
let buffer = this.read_with(&cx, |this, cx| {
|
||||
this.opened_buffers
|
||||
|
@ -5445,6 +5549,7 @@ impl Project {
|
|||
.await
|
||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||
};
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||
Ok(buffer)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
|
||||
use super::{
|
||||
fs::{self, Fs},
|
||||
ignore::IgnoreStack,
|
||||
DiagnosticSummary,
|
||||
};
|
||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client};
|
||||
|
@ -18,6 +17,8 @@ use futures::{
|
|||
Stream, StreamExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::repository::GitRepository;
|
||||
use git::{DOT_GIT, GITIGNORE};
|
||||
use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task,
|
||||
|
@ -26,12 +27,12 @@ use language::{
|
|||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
any::Any,
|
||||
|
@ -40,6 +41,7 @@ use std::{
|
|||
ffi::{OsStr, OsString},
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||
path::{Path, PathBuf},
|
||||
|
@ -50,10 +52,6 @@ use std::{
|
|||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
lazy_static! {
|
||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
|
||||
|
@ -101,15 +99,51 @@ pub struct Snapshot {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitRepositoryEntry {
|
||||
pub(crate) repo: Arc<Mutex<dyn GitRepository>>,
|
||||
|
||||
pub(crate) scan_id: usize,
|
||||
// Path to folder containing the .git file or directory
|
||||
pub(crate) content_path: Arc<Path>,
|
||||
// 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 std::fmt::Debug for GitRepositoryEntry {
|
||||
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)
|
||||
.field("libgit_repository", &"LibGitRepository")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalSnapshot {
|
||||
abs_path: Arc<Path>,
|
||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
git_repositories: Vec<GitRepositoryEntry>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
extension_counts: HashMap<OsString, usize>,
|
||||
}
|
||||
|
||||
impl Clone for LocalSnapshot {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
abs_path: self.abs_path.clone(),
|
||||
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(),
|
||||
extension_counts: self.extension_counts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for LocalSnapshot {
|
||||
type Target = Snapshot;
|
||||
|
||||
|
@ -142,6 +176,7 @@ struct ShareState {
|
|||
|
||||
pub enum Event {
|
||||
UpdatedEntries,
|
||||
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
|
||||
}
|
||||
|
||||
impl Entity for Worktree {
|
||||
|
@ -372,6 +407,7 @@ impl LocalWorktree {
|
|||
let mut snapshot = LocalSnapshot {
|
||||
abs_path,
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
removed_entry_ids: Default::default(),
|
||||
next_entry_id,
|
||||
snapshot: Snapshot {
|
||||
|
@ -446,10 +482,14 @@ impl LocalWorktree {
|
|||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let path = Arc::from(path);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let (file, contents) = this
|
||||
let (file, contents, diff_base) = this
|
||||
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
|
||||
.await?;
|
||||
Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx)))
|
||||
Ok(cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx);
|
||||
buffer.git_diff_recalc(cx);
|
||||
buffer
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -499,17 +539,37 @@ impl LocalWorktree {
|
|||
|
||||
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
||||
self.poll_task.take();
|
||||
|
||||
match self.scan_state() {
|
||||
ScanState::Idle => {
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
let new_snapshot = self.background_snapshot.lock().clone();
|
||||
let updated_repos = Self::changed_repos(
|
||||
&self.snapshot.git_repositories,
|
||||
&new_snapshot.git_repositories,
|
||||
);
|
||||
self.snapshot = new_snapshot;
|
||||
|
||||
if let Some(share) = self.share.as_mut() {
|
||||
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||
}
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
ScanState::Initializing => {
|
||||
let is_fake_fs = self.fs.is_fake();
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
|
||||
let new_snapshot = self.background_snapshot.lock().clone();
|
||||
let updated_repos = Self::changed_repos(
|
||||
&self.snapshot.git_repositories,
|
||||
&new_snapshot.git_repositories,
|
||||
);
|
||||
self.snapshot = new_snapshot;
|
||||
|
||||
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
if is_fake_fs {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -521,17 +581,52 @@ impl LocalWorktree {
|
|||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||
}
|
||||
}));
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if force {
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn changed_repos(
|
||||
old_repos: &[GitRepositoryEntry],
|
||||
new_repos: &[GitRepositoryEntry],
|
||||
) -> Vec<GitRepositoryEntry> {
|
||||
fn diff<'a>(
|
||||
a: &'a [GitRepositoryEntry],
|
||||
b: &'a [GitRepositoryEntry],
|
||||
updated: &mut HashMap<&'a Path, GitRepositoryEntry>,
|
||||
) {
|
||||
for a_repo in a {
|
||||
let matched = b.iter().find(|b_repo| {
|
||||
a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id
|
||||
});
|
||||
|
||||
if matched.is_none() {
|
||||
updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut updated = HashMap::<&Path, GitRepositoryEntry>::default();
|
||||
|
||||
diff(old_repos, new_repos, &mut updated);
|
||||
diff(new_repos, old_repos, &mut updated);
|
||||
|
||||
updated.into_values().collect()
|
||||
}
|
||||
|
||||
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||
async move {
|
||||
|
@ -558,13 +653,33 @@ impl LocalWorktree {
|
|||
}
|
||||
}
|
||||
|
||||
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
|
||||
fn load(
|
||||
&self,
|
||||
path: &Path,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Task<Result<(File, String, Option<String>)>> {
|
||||
let handle = cx.handle();
|
||||
let path = Arc::from(path);
|
||||
let abs_path = self.absolutize(&path);
|
||||
let fs = self.fs.clone();
|
||||
let snapshot = self.snapshot();
|
||||
|
||||
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 repo_relative = repo_relative.to_owned();
|
||||
cx.background()
|
||||
.spawn(async move { repo.repo.lock().load_index_text(&repo_relative) })
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Eagerly populate the snapshot with an updated entry for the loaded file
|
||||
let entry = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
|
@ -573,6 +688,7 @@ impl LocalWorktree {
|
|||
.refresh_entry(path, abs_path, None, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
File {
|
||||
entry_id: Some(entry.id),
|
||||
|
@ -582,6 +698,7 @@ impl LocalWorktree {
|
|||
is_local: true,
|
||||
},
|
||||
text,
|
||||
diff_base,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
@ -1248,6 +1365,22 @@ impl LocalSnapshot {
|
|||
&self.extension_counts
|
||||
}
|
||||
|
||||
// Gives the most specific git repository for a given path
|
||||
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
|
||||
self.git_repositories
|
||||
.iter()
|
||||
.rev() //git_repository is ordered lexicographically
|
||||
.find(|repo| repo.manages(path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepositoryEntry> {
|
||||
// Git repositories cannot be nested, so we don't need to reverse the order
|
||||
self.git_repositories
|
||||
.iter_mut()
|
||||
.find(|repo| repo.in_dot_git(path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||
let root_name = self.root_name.clone();
|
||||
|
@ -1330,7 +1463,7 @@ impl LocalSnapshot {
|
|||
}
|
||||
|
||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
let abs_path = self.abs_path.join(&entry.path);
|
||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||
Ok(ignore) => {
|
||||
|
@ -1384,6 +1517,7 @@ impl LocalSnapshot {
|
|||
parent_path: Arc<Path>,
|
||||
entries: impl IntoIterator<Item = Entry>,
|
||||
ignore: Option<Arc<Gitignore>>,
|
||||
fs: &dyn Fs,
|
||||
) {
|
||||
let mut parent_entry = if let Some(parent_entry) =
|
||||
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
||||
|
@ -1409,6 +1543,27 @@ impl LocalSnapshot {
|
|||
unreachable!();
|
||||
}
|
||||
|
||||
if parent_path.file_name() == Some(&DOT_GIT) {
|
||||
let abs_path = self.abs_path.join(&parent_path);
|
||||
let content_path: Arc<Path> = parent_path.parent().unwrap().into();
|
||||
if let Err(ix) = self
|
||||
.git_repositories
|
||||
.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(
|
||||
ix,
|
||||
GitRepositoryEntry {
|
||||
repo,
|
||||
scan_id: 0,
|
||||
content_path,
|
||||
git_dir_path: parent_path,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
|
||||
let mut entries_by_id_edits = Vec::new();
|
||||
|
||||
|
@ -1493,6 +1648,14 @@ impl LocalSnapshot {
|
|||
{
|
||||
*scan_id = self.snapshot.scan_id;
|
||||
}
|
||||
} else if path.file_name() == Some(&DOT_GIT) {
|
||||
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())
|
||||
{
|
||||
self.git_repositories[ix].scan_id = self.snapshot.scan_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1532,6 +1695,22 @@ impl LocalSnapshot {
|
|||
|
||||
ignore_stack
|
||||
}
|
||||
|
||||
pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] {
|
||||
&self.git_repositories
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRepositoryEntry {
|
||||
// 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 theis 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<Gitignore> {
|
||||
|
@ -2244,9 +2423,12 @@ impl BackgroundScanner {
|
|||
new_entries.push(child_entry);
|
||||
}
|
||||
|
||||
self.snapshot
|
||||
.lock()
|
||||
.populate_dir(job.path.clone(), new_entries, new_ignore);
|
||||
self.snapshot.lock().populate_dir(
|
||||
job.path.clone(),
|
||||
new_entries,
|
||||
new_ignore,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
for new_job in new_jobs {
|
||||
job.scan_queue.send(new_job).await.unwrap();
|
||||
}
|
||||
|
@ -2321,6 +2503,12 @@ impl BackgroundScanner {
|
|||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
||||
|
||||
let scan_id = snapshot.scan_id;
|
||||
if let Some(repo) = snapshot.in_dot_git(&path) {
|
||||
repo.repo.lock().reload_index();
|
||||
repo.scan_id = scan_id;
|
||||
}
|
||||
|
||||
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||
ancestor_inodes.insert(metadata.inode);
|
||||
|
@ -2367,6 +2555,7 @@ impl BackgroundScanner {
|
|||
self.snapshot.lock().removed_entry_ids.clear();
|
||||
|
||||
self.update_ignore_statuses().await;
|
||||
self.update_git_repositories();
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -2432,6 +2621,13 @@ impl BackgroundScanner {
|
|||
.await;
|
||||
}
|
||||
|
||||
fn update_git_repositories(&self) {
|
||||
let mut snapshot = self.snapshot.lock();
|
||||
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
|
||||
git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
|
||||
snapshot.git_repositories = git_repositories;
|
||||
}
|
||||
|
||||
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||
let mut ignore_stack = job.ignore_stack;
|
||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||
|
@ -2778,6 +2974,7 @@ mod tests {
|
|||
use anyhow::Result;
|
||||
use client::test::FakeHttpClient;
|
||||
use fs::RealFs;
|
||||
use git::repository::FakeGitRepository;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
|
@ -2786,6 +2983,7 @@ mod tests {
|
|||
fmt::Write,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -2804,7 +3002,7 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client);
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
|
@ -2866,8 +3064,7 @@ mod tests {
|
|||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client);
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
Arc::from(Path::new("/root")),
|
||||
|
@ -2945,8 +3142,7 @@ mod tests {
|
|||
}));
|
||||
let dir = parent_dir.path().join("tree");
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
|
@ -3007,6 +3203,135 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
let root = temp_tree(json!({
|
||||
"dir1": {
|
||||
".git": {},
|
||||
"deps": {
|
||||
"dep1": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"a.txt": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"b.txt": ""
|
||||
}
|
||||
},
|
||||
"c.txt": "",
|
||||
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repo_for("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 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 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
|
||||
});
|
||||
|
||||
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 = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
|
||||
assert_ne!(
|
||||
original_scan_id, new_scan_id,
|
||||
"original {original_scan_id}, new {new_scan_id}"
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_changed_repos() {
|
||||
fn fake_entry(git_dir_path: impl AsRef<Path>, scan_id: usize) -> GitRepositoryEntry {
|
||||
GitRepositoryEntry {
|
||||
repo: Arc::new(Mutex::new(FakeGitRepository::default())),
|
||||
scan_id,
|
||||
content_path: git_dir_path.as_ref().parent().unwrap().into(),
|
||||
git_dir_path: git_dir_path.as_ref().into(),
|
||||
}
|
||||
}
|
||||
|
||||
let prev_repos: Vec<GitRepositoryEntry> = vec![
|
||||
fake_entry("/.git", 0),
|
||||
fake_entry("/a/.git", 0),
|
||||
fake_entry("/a/b/.git", 0),
|
||||
];
|
||||
|
||||
let new_repos: Vec<GitRepositoryEntry> = vec![
|
||||
fake_entry("/a/.git", 1),
|
||||
fake_entry("/a/b/.git", 0),
|
||||
fake_entry("/a/c/.git", 0),
|
||||
];
|
||||
|
||||
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
|
||||
|
||||
// Deletion retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && 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)
|
||||
.is_some());
|
||||
|
||||
// Addition retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && 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)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
|
@ -3016,8 +3341,7 @@ mod tests {
|
|||
"ignored-dir": {}
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
|
@ -3064,8 +3388,7 @@ mod tests {
|
|||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_create_directory(cx: &mut TestAppContext) {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
|
@ -3127,6 +3450,7 @@ mod tests {
|
|||
abs_path: root_dir.path().into(),
|
||||
removed_entry_ids: Default::default(),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
next_entry_id: next_entry_id.clone(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(0),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue