diff --git a/Cargo.lock b/Cargo.lock index 3c87f336de..75dd5530c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9b3603e6e4..47c86e0fe7 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Nathan Sobo "] +authors = ["Nathan Sobo "] 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"] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3c9886dc16..586d988ef1 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -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, + 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, diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 7ef9a953ba..744fdc8b99 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -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 = [] diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 4d12ca90d1..6c904d44d1 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -222,6 +222,40 @@ impl BufferDiff { } } +/// Range (crossing new lines), old, new +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn assert_hunks( + diff_hunks: Iter, + buffer: &BufferSnapshot, + head_text: &str, + expected_hunks: &[(Range, &str, &str)], +) where + Iter: Iterator>, +{ + 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::(), + ) + }) + .collect::>(); + + 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, &str, &str)], - range: Option>, - ) { - 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::(), - ) - }) - .collect::>(); - - let expected_hunks: Vec<_> = expected_hunks - .iter() - .map(|(r, s, h)| (r.clone(), *s, h.to_string())) - .collect(); - - assert_eq!(actual_hunks, expected_hunks); - } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index f834ebc219..fb43e44561 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -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, git_dir_path: Arc, scan_id: usize, + state: Arc>, +} + +#[derive(Debug, Clone, Default)] +pub struct FakeGitRepositoryState { + pub index_contents: HashMap, } impl FakeGitRepository { - pub fn open(dotgit_path: &Path, scan_id: usize) -> Box { + pub fn open( + dotgit_path: &Path, + scan_id: usize, + state: Arc>, + ) -> Box { 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 { - None + async fn load_head_text(&self, path: &Path) -> Option { + 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> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0268f1cc68..831236ad5d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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, cx: &mut ModelContext) { 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) { if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index c14edcd5e4..2b914ae373 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -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>>, + git_repo_state: Option>>, }, 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 { 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> { - 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 { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0d2594475c..d3a5f710e0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3288,15 +3288,15 @@ mod tests { #[test] fn test_changed_repos() { let prev_repos: Vec> = 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> = 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); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index ab0e1eeabc..9f70ae1cc7 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -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