Detailed changes
@@ -1031,6 +1031,7 @@ dependencies = [
"env_logger",
"envy",
"futures",
+ "git",
"gpui",
"hyper",
"language",
@@ -1061,6 +1062,7 @@ dependencies = [
"tracing",
"tracing-log",
"tracing-subscriber",
+ "unindent",
"util",
"workspace",
]
@@ -2232,6 +2234,7 @@ dependencies = [
"anyhow",
"async-trait",
"clock",
+ "collections",
"git2",
"lazy_static",
"log",
@@ -1,5 +1,5 @@
[package]
-authors = ["Nathan Sobo <nathan@warp.dev>"]
+authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
@@ -26,6 +26,7 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
envy = "0.4.2"
futures = "0.3"
+git = { path = "../git" }
hyper = "0.14"
lazy_static = "1.4"
lipsum = { version = "0.8", optional = true }
@@ -65,11 +66,13 @@ project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
+git = { path = "../git", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
util = { path = "../util" }
lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }
+unindent = "0.1"
[features]
seed-support = ["clap", "lipsum", "reqwest"]
@@ -51,6 +51,7 @@ use std::{
time::Duration,
};
use theme::ThemeRegistry;
+use unindent::Unindent as _;
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
#[ctor::ctor]
@@ -946,6 +947,143 @@ async fn test_propagate_saves_and_fs_changes(
.await;
}
+#[gpui::test(iterations = 10)]
+async fn test_git_head_text(
+ executor: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ executor.forbid_parking();
+ let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+
+ client_a
+ .fs
+ .insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ "a.txt": "
+ one
+ two
+ three
+ ".unindent(),
+ }),
+ )
+ .await;
+
+ let head_text = "
+ one
+ three
+ "
+ .unindent();
+
+ let new_head_text = "
+ 1
+ two
+ three
+ "
+ .unindent();
+
+ client_a
+ .fs
+ .as_fake()
+ .set_head_state_for_git_repository(
+ Path::new("/dir/.git"),
+ &[(Path::new("a.txt"), head_text.clone())],
+ )
+ .await;
+
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+ let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+ // Create the buffer
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx))
+ .await
+ .unwrap();
+
+ // Wait for it to catch up to the new diff
+ buffer_a
+ .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff())
+ .await;
+
+ // Smoke test diffing
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.head_text(), Some(head_text.as_ref()));
+ git::diff::assert_hunks(
+ buffer.snapshot().git_diff_hunks_in_range(0..4),
+ &buffer,
+ &head_text,
+ &[(1..2, "", "two\n")],
+ );
+ });
+
+ // Create remote buffer
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx))
+ .await
+ .unwrap();
+
+ //TODO: WAIT FOR REMOTE UPDATES TO FINISH
+
+ // Smoke test diffing
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(buffer.head_text(), Some(head_text.as_ref()));
+ git::diff::assert_hunks(
+ buffer.snapshot().git_diff_hunks_in_range(0..4),
+ &buffer,
+ &head_text,
+ &[(1..2, "", "two\n")],
+ );
+ });
+
+ // TODO: Create a dummy file event
+ client_a
+ .fs
+ .as_fake()
+ .set_head_state_for_git_repository(
+ Path::new("/dir/.git"),
+ &[(Path::new("a.txt"), new_head_text.clone())],
+ )
+ .await;
+
+ // TODO: Flush this file event
+
+ // Wait for buffer_a to receive it
+ buffer_a
+ .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff())
+ .await;
+
+ // Smoke test new diffing
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.head_text(), Some(new_head_text.as_ref()));
+ git::diff::assert_hunks(
+ buffer.snapshot().git_diff_hunks_in_range(0..4),
+ &buffer,
+ &head_text,
+ &[(0..1, "1", "one\n")],
+ );
+ });
+
+ //TODO: WAIT FOR REMOTE UPDATES TO FINISH on B
+
+ // Smoke test B
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(buffer.head_text(), Some(new_head_text.as_ref()));
+ git::diff::assert_hunks(
+ buffer.snapshot().git_diff_hunks_in_range(0..4),
+ &buffer,
+ &head_text,
+ &[(0..1, "1", "one\n")],
+ );
+ });
+}
+
#[gpui::test(iterations = 10)]
async fn test_fs_operations(
executor: Arc<Deterministic>,
@@ -13,12 +13,15 @@ git2 = { version = "0.15", default-features = false }
lazy_static = "1.4.0"
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
+collections = { path = "../collections" }
util = { path = "../util" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
smol = "1.2"
parking_lot = "0.11.1"
async-trait = "0.1"
-
[dev-dependencies]
unindent = "0.1.7"
+
+[features]
+test-support = []
@@ -222,6 +222,40 @@ impl BufferDiff {
}
}
+/// Range (crossing new lines), old, new
+#[cfg(any(test, feature = "test-support"))]
+#[track_caller]
+pub fn assert_hunks<Iter>(
+ diff_hunks: Iter,
+ buffer: &BufferSnapshot,
+ head_text: &str,
+ expected_hunks: &[(Range<u32>, &str, &str)],
+) where
+ Iter: Iterator<Item = DiffHunk<u32>>,
+{
+ let actual_hunks = diff_hunks
+ .map(|hunk| {
+ (
+ hunk.buffer_range.clone(),
+ &head_text[hunk.head_byte_range],
+ buffer
+ .text_for_range(
+ Point::new(hunk.buffer_range.start, 0)
+ ..Point::new(hunk.buffer_range.end, 0),
+ )
+ .collect::<String>(),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ let expected_hunks: Vec<_> = expected_hunks
+ .iter()
+ .map(|(r, s, h)| (r.clone(), *s, h.to_string()))
+ .collect();
+
+ assert_eq!(actual_hunks, expected_hunks);
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -248,21 +282,19 @@ mod tests {
let mut diff = BufferDiff::new();
smol::block_on(diff.update(&head_text, &buffer));
assert_hunks(
- &diff,
+ diff.hunks(&buffer),
&buffer,
&head_text,
&[(1..2, "two\n", "HELLO\n")],
- None,
);
buffer.edit([(0..0, "point five\n")]);
smol::block_on(diff.update(&head_text, &buffer));
assert_hunks(
- &diff,
+ diff.hunks(&buffer),
&buffer,
&head_text,
&[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
- None,
);
}
@@ -309,7 +341,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
- &diff,
+ diff.hunks_in_range(7..12, &buffer),
&buffer,
&head_text,
&[
@@ -317,39 +349,6 @@ mod tests {
(9..10, "six\n", "SIXTEEN\n"),
(12..13, "", "WORLD\n"),
],
- Some(7..12),
);
}
-
- #[track_caller]
- fn assert_hunks(
- diff: &BufferDiff,
- buffer: &BufferSnapshot,
- head_text: &str,
- expected_hunks: &[(Range<u32>, &str, &str)],
- range: Option<Range<u32>>,
- ) {
- let actual_hunks = diff
- .hunks_in_range(range.unwrap_or(0..u32::MAX), buffer)
- .map(|hunk| {
- (
- hunk.buffer_range.clone(),
- &head_text[hunk.head_byte_range],
- buffer
- .text_for_range(
- Point::new(hunk.buffer_range.start, 0)
- ..Point::new(hunk.buffer_range.end, 0),
- )
- .collect::<String>(),
- )
- })
- .collect::<Vec<_>>();
-
- let expected_hunks: Vec<_> = expected_hunks
- .iter()
- .map(|(r, s, h)| (r.clone(), *s, h.to_string()))
- .collect();
-
- assert_eq!(actual_hunks, expected_hunks);
- }
}
@@ -1,7 +1,11 @@
use anyhow::Result;
+use collections::HashMap;
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
-use std::{path::Path, sync::Arc};
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
use util::ResultExt;
#[async_trait::async_trait]
@@ -140,14 +144,25 @@ pub struct FakeGitRepository {
content_path: Arc<Path>,
git_dir_path: Arc<Path>,
scan_id: usize,
+ state: Arc<Mutex<FakeGitRepositoryState>>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FakeGitRepositoryState {
+ pub index_contents: HashMap<PathBuf, String>,
}
impl FakeGitRepository {
- pub fn open(dotgit_path: &Path, scan_id: usize) -> Box<dyn GitRepository> {
+ pub fn open(
+ dotgit_path: &Path,
+ scan_id: usize,
+ state: Arc<Mutex<FakeGitRepositoryState>>,
+ ) -> Box<dyn GitRepository> {
Box::new(FakeGitRepository {
content_path: dotgit_path.parent().unwrap().into(),
git_dir_path: dotgit_path.into(),
scan_id,
+ state,
})
}
}
@@ -174,12 +189,13 @@ impl GitRepository for FakeGitRepository {
self.scan_id
}
- async fn load_head_text(&self, _: &Path) -> Option<String> {
- None
+ async fn load_head_text(&self, path: &Path) -> Option<String> {
+ let state = self.state.lock();
+ state.index_contents.get(path).cloned()
}
fn reopen_git_repo(&mut self) -> bool {
- false
+ true
}
fn git_repo(&self) -> Arc<Mutex<LibGitRepository>> {
@@ -662,6 +662,11 @@ impl Buffer {
task
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn head_text(&self) -> Option<&str> {
+ self.head_text.as_deref()
+ }
+
pub fn update_head_text(&mut self, head_text: Option<String>, cx: &mut ModelContext<Self>) {
self.head_text = head_text;
self.git_diff_recalc(cx);
@@ -671,6 +676,10 @@ impl Buffer {
self.git_diff_status.diff.needs_update(self)
}
+ pub fn is_recalculating_git_diff(&self) -> bool {
+ self.git_diff_status.update_in_progress
+ }
+
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
if self.git_diff_status.update_in_progress {
self.git_diff_status.update_requested = true;
@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use fsevent::EventStream;
use futures::{future::BoxFuture, Stream, StreamExt};
-use git::repository::{GitRepository, RealGitRepository};
+use git::repository::{FakeGitRepositoryState, GitRepository, RealGitRepository};
use language::LineEnding;
use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::{
@@ -277,6 +277,7 @@ enum FakeFsEntry {
inode: u64,
mtime: SystemTime,
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
+ git_repo_state: Option<Arc<parking_lot::Mutex<git::repository::FakeGitRepositoryState>>>,
},
Symlink {
target: PathBuf,
@@ -391,6 +392,7 @@ impl FakeFs {
inode: 0,
mtime: SystemTime::now(),
entries: Default::default(),
+ git_repo_state: None,
})),
next_inode: 1,
event_txs: Default::default(),
@@ -480,6 +482,31 @@ impl FakeFs {
.boxed()
}
+ pub async fn set_head_state_for_git_repository(
+ &self,
+ dot_git: &Path,
+ head_state: &[(&Path, String)],
+ ) {
+ let content_path = dot_git.parent().unwrap();
+ let 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)| (content_path.join(path), content.clone())),
+ );
+ } else {
+ panic!("not a directory");
+ }
+ }
+
pub async fn files(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -569,6 +596,7 @@ impl Fs for FakeFs {
inode,
mtime: SystemTime::now(),
entries: Default::default(),
+ git_repo_state: None,
}))
});
Ok(())
@@ -854,10 +882,26 @@ impl Fs for FakeFs {
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Box<dyn GitRepository>> {
- Some(git::repository::FakeGitRepository::open(
- abs_dot_git.into(),
- 0,
- ))
+ let executor = self.executor.upgrade().unwrap();
+ executor.block(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(parking_lot::Mutex::new(FakeGitRepositoryState::default()))
+ })
+ .clone();
+ Some(git::repository::FakeGitRepository::open(
+ abs_dot_git.into(),
+ 0,
+ state,
+ ))
+ } else {
+ None
+ }
+ })
}
fn is_fake(&self) -> bool {
@@ -3288,15 +3288,15 @@ mod tests {
#[test]
fn test_changed_repos() {
let prev_repos: Vec<Box<dyn GitRepository>> = vec![
- FakeGitRepository::open(Path::new("/.git"), 0),
- FakeGitRepository::open(Path::new("/a/.git"), 0),
- FakeGitRepository::open(Path::new("/a/b/.git"), 0),
+ FakeGitRepository::open(Path::new("/.git"), 0, Default::default()),
+ FakeGitRepository::open(Path::new("/a/.git"), 0, Default::default()),
+ FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()),
];
let new_repos: Vec<Box<dyn GitRepository>> = vec![
- FakeGitRepository::open(Path::new("/a/.git"), 1),
- FakeGitRepository::open(Path::new("/a/b/.git"), 0),
- FakeGitRepository::open(Path::new("/a/c/.git"), 0),
+ FakeGitRepository::open(Path::new("/a/.git"), 1, Default::default()),
+ FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()),
+ FakeGitRepository::open(Path::new("/a/c/.git"), 0, Default::default()),
];
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
@@ -26,7 +26,7 @@ impl Anchor {
bias: Bias::Right,
buffer_id: None,
};
-
+
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
let fragment_id_comparison = if self.timestamp == other.timestamp {
Ordering::Equal