diff --git a/Cargo.lock b/Cargo.lock index 2859c3907e..c6280c7fb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5238,8 +5238,8 @@ dependencies = [ "fsevent", "futures 0.3.31", "git", - "git2", "gpui", + "ignore", "libc", "log", "notify 6.1.1", diff --git a/crates/collab/src/tests/git_tests.rs b/crates/collab/src/tests/git_tests.rs index 4248ea8712..ed74b9f7cf 100644 --- a/crates/collab/src/tests/git_tests.rs +++ b/crates/collab/src/tests/git_tests.rs @@ -103,7 +103,6 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) }), ) .await; - client_a.fs().recalculate_git_status(Path::new("/a/.git")); cx_b.run_until_parked(); project_b.update(cx_b, |project, cx| { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5574d52d31..81134502ea 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2958,15 +2958,38 @@ async fn test_git_status_sync( .insert_tree( "/dir", json!({ - ".git": {}, - "a.txt": "a", - "b.txt": "b", + ".git": {}, + "a.txt": "a", + "b.txt": "b", + "c.txt": "c", }), ) .await; - const A_TXT: &str = "a.txt"; - const B_TXT: &str = "b.txt"; + // Initially, a.txt is uncommitted, but present in the index, + // and b.txt is unmerged. + client_a.fs().set_head_for_repo( + "/dir/.git".as_ref(), + &[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())], + ); + client_a.fs().set_index_for_repo( + "/dir/.git".as_ref(), + &[ + ("a.txt".into(), "".into()), + ("b.txt".into(), "B".into()), + ("c.txt".into(), "c".into()), + ], + ); + client_a.fs().set_unmerged_paths_for_repo( + "/dir/.git".as_ref(), + &[( + "b.txt".into(), + UnmergedStatus { + first_head: UnmergedStatusCode::Updated, + second_head: UnmergedStatusCode::Deleted, + }, + )], + ); const A_STATUS_START: FileStatus = FileStatus::Tracked(TrackedStatus { index_status: StatusCode::Added, @@ -2977,14 +3000,6 @@ async fn test_git_status_sync( second_head: UnmergedStatusCode::Deleted, }); - client_a.fs().set_status_for_repo_via_git_operation( - Path::new("/dir/.git"), - &[ - (Path::new(A_TXT), A_STATUS_START), - (Path::new(B_TXT), B_STATUS_START), - ], - ); - let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| { @@ -3000,7 +3015,7 @@ async fn test_git_status_sync( #[track_caller] fn assert_status( - file: &impl AsRef, + file: impl AsRef, status: Option, project: &Project, cx: &App, @@ -3014,13 +3029,15 @@ async fn test_git_status_sync( } project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx); - assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx); + assert_status("a.txt", Some(A_STATUS_START), project, cx); + assert_status("b.txt", Some(B_STATUS_START), project, cx); + assert_status("c.txt", None, project, cx); }); project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx); - assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx); + assert_status("a.txt", Some(A_STATUS_START), project, cx); + assert_status("b.txt", Some(B_STATUS_START), project, cx); + assert_status("c.txt", None, project, cx); }); const A_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus { @@ -3029,30 +3046,42 @@ async fn test_git_status_sync( }); const B_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus { index_status: StatusCode::Deleted, - worktree_status: StatusCode::Unmodified, + worktree_status: StatusCode::Added, + }); + const C_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Unmodified, + worktree_status: StatusCode::Modified, }); - client_a.fs().set_status_for_repo_via_working_copy_change( - Path::new("/dir/.git"), - &[ - (Path::new(A_TXT), A_STATUS_END), - (Path::new(B_TXT), B_STATUS_END), - ], + // Delete b.txt from the index, mark conflict as resolved, + // and modify c.txt in the working copy. + client_a.fs().set_index_for_repo( + "/dir/.git".as_ref(), + &[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())], ); + client_a + .fs() + .set_unmerged_paths_for_repo("/dir/.git".as_ref(), &[]); + client_a + .fs() + .atomic_write("/dir/c.txt".into(), "CC".into()) + .await + .unwrap(); // Wait for buffer_local_a to receive it executor.run_until_parked(); // Smoke test status reading - project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx); - assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx); + assert_status("a.txt", Some(A_STATUS_END), project, cx); + assert_status("b.txt", Some(B_STATUS_END), project, cx); + assert_status("c.txt", Some(C_STATUS_END), project, cx); }); project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx); - assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx); + assert_status("a.txt", Some(A_STATUS_END), project, cx); + assert_status("b.txt", Some(B_STATUS_END), project, cx); + assert_status("c.txt", Some(C_STATUS_END), project, cx); }); // And synchronization while joining @@ -3060,8 +3089,9 @@ async fn test_git_status_sync( executor.run_until_parked(); project_remote_c.read_with(cx_c, |project, cx| { - assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx); - assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx); + assert_status("a.txt", Some(A_STATUS_END), project, cx); + assert_status("b.txt", Some(B_STATUS_END), project, cx); + assert_status("c.txt", Some(C_STATUS_END), project, cx); }); } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index aacacf9cca..37d5cfd1fc 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -128,7 +128,6 @@ enum GitOperation { WriteGitStatuses { repo_path: PathBuf, statuses: Vec<(PathBuf, FileStatus)>, - git_operation: bool, }, } @@ -987,7 +986,6 @@ impl RandomizedTest for ProjectCollaborationTest { GitOperation::WriteGitStatuses { repo_path, statuses, - git_operation, } => { if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); @@ -1016,17 +1014,9 @@ impl RandomizedTest for ProjectCollaborationTest { client.fs().create_dir(&dot_git_dir).await?; } - if git_operation { - client.fs().set_status_for_repo_via_git_operation( - &dot_git_dir, - statuses.as_slice(), - ); - } else { - client.fs().set_status_for_repo_via_working_copy_change( - &dot_git_dir, - statuses.as_slice(), - ); - } + client + .fs() + .set_status_for_repo(&dot_git_dir, statuses.as_slice()); } }, } @@ -1455,18 +1445,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation } 64..=100 => { let file_paths = generate_file_paths(&repo_path, rng, client); - let statuses = file_paths .into_iter() .map(|path| (path, gen_status(rng))) .collect::>(); - - let git_operation = rng.gen::(); - GitOperation::WriteGitStatuses { repo_path, statuses, - git_operation, } } _ => unreachable!(), @@ -1605,15 +1590,24 @@ fn gen_file_name(rng: &mut StdRng) -> String { } fn gen_status(rng: &mut StdRng) -> FileStatus { - fn gen_status_code(rng: &mut StdRng) -> StatusCode { - match rng.gen_range(0..7) { - 0 => StatusCode::Modified, - 1 => StatusCode::TypeChanged, - 2 => StatusCode::Added, - 3 => StatusCode::Deleted, - 4 => StatusCode::Renamed, - 5 => StatusCode::Copied, - 6 => StatusCode::Unmodified, + fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus { + match rng.gen_range(0..3) { + 0 => TrackedStatus { + index_status: StatusCode::Unmodified, + worktree_status: StatusCode::Unmodified, + }, + 1 => TrackedStatus { + index_status: StatusCode::Modified, + worktree_status: StatusCode::Modified, + }, + 2 => TrackedStatus { + index_status: StatusCode::Added, + worktree_status: StatusCode::Modified, + }, + 3 => TrackedStatus { + index_status: StatusCode::Added, + worktree_status: StatusCode::Unmodified, + }, _ => unreachable!(), } } @@ -1627,17 +1621,12 @@ fn gen_status(rng: &mut StdRng) -> FileStatus { } } - match rng.gen_range(0..4) { - 0 => FileStatus::Untracked, - 1 => FileStatus::Ignored, - 2 => FileStatus::Unmerged(UnmergedStatus { + match rng.gen_range(0..2) { + 0 => FileStatus::Unmerged(UnmergedStatus { first_head: gen_unmerged_status_code(rng), second_head: gen_unmerged_status_code(rng), }), - 3 => FileStatus::Tracked(TrackedStatus { - index_status: gen_status_code(rng), - worktree_status: gen_status_code(rng), - }), + 1 => FileStatus::Tracked(gen_tracked_status(rng)), _ => unreachable!(), } } diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 35754822e8..eb86c4067c 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -18,8 +18,8 @@ async-trait.workspace = true collections.workspace = true futures.workspace = true git.workspace = true -git2.workspace = true gpui.workspace = true +ignore.workspace = true libc.workspace = true log.workspace = true parking_lot.workspace = true diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs new file mode 100644 index 0000000000..6c2c230b5b --- /dev/null +++ b/crates/fs/src/fake_git_repo.rs @@ -0,0 +1,411 @@ +use crate::FakeFs; +use anyhow::{anyhow, Context as _, Result}; +use collections::{HashMap, HashSet}; +use futures::future::{self, BoxFuture}; +use git::{ + blame::Blame, + repository::{ + AskPassSession, Branch, CommitDetails, GitRepository, PushOptions, Remote, RepoPath, + ResetMode, + }, + status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, +}; +use gpui::{AsyncApp, BackgroundExecutor}; +use ignore::gitignore::GitignoreBuilder; +use rope::Rope; +use smol::future::FutureExt as _; +use std::{path::PathBuf, sync::Arc}; + +#[derive(Clone)] +pub struct FakeGitRepository { + pub(crate) fs: Arc, + pub(crate) executor: BackgroundExecutor, + pub(crate) dot_git_path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct FakeGitRepositoryState { + pub path: PathBuf, + pub event_emitter: smol::channel::Sender, + pub unmerged_paths: HashMap, + pub head_contents: HashMap, + pub index_contents: HashMap, + pub blames: HashMap, + pub current_branch_name: Option, + pub branches: HashSet, + pub simulated_index_write_error_message: Option, +} + +impl FakeGitRepositoryState { + pub fn new(path: PathBuf, event_emitter: smol::channel::Sender) -> Self { + FakeGitRepositoryState { + path, + event_emitter, + head_contents: Default::default(), + index_contents: Default::default(), + unmerged_paths: Default::default(), + blames: Default::default(), + current_branch_name: Default::default(), + branches: Default::default(), + simulated_index_write_error_message: Default::default(), + } + } +} + +impl FakeGitRepository { + fn with_state(&self, f: F) -> T + where + F: FnOnce(&mut FakeGitRepositoryState) -> T, + { + self.fs.with_git_state(&self.dot_git_path, false, f) + } + + fn with_state_async(&self, write: bool, f: F) -> BoxFuture + where + F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> T, + T: Send, + { + let fs = self.fs.clone(); + let executor = self.executor.clone(); + let dot_git_path = self.dot_git_path.clone(); + async move { + executor.simulate_random_delay().await; + fs.with_git_state(&dot_git_path, write, f) + } + .boxed() + } +} + +impl GitRepository for FakeGitRepository { + fn reload_index(&self) {} + + fn load_index_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture> { + self.with_state_async(false, move |state| { + state.index_contents.get(path.as_ref()).cloned() + }) + } + + fn load_committed_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture> { + self.with_state_async(false, move |state| { + state.head_contents.get(path.as_ref()).cloned() + }) + } + + fn set_index_text( + &self, + path: RepoPath, + content: Option, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + self.with_state_async(true, move |state| { + if let Some(message) = state.simulated_index_write_error_message.clone() { + return Err(anyhow!("{}", message)); + } else if let Some(content) = content { + state.index_contents.insert(path, content); + } else { + state.index_contents.remove(&path); + } + Ok(()) + }) + } + + fn remote_url(&self, _name: &str) -> Option { + None + } + + fn head_sha(&self) -> Option { + None + } + + fn merge_head_shas(&self) -> Vec { + vec![] + } + + fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture> { + unimplemented!() + } + + fn reset( + &self, + _commit: String, + _mode: ResetMode, + _env: HashMap, + ) -> BoxFuture> { + unimplemented!() + } + + fn checkout_files( + &self, + _commit: String, + _paths: Vec, + _env: HashMap, + ) -> BoxFuture> { + unimplemented!() + } + + fn path(&self) -> PathBuf { + self.with_state(|state| state.path.clone()) + } + + fn main_repository_path(&self) -> PathBuf { + self.path() + } + + fn status(&self, path_prefixes: &[RepoPath]) -> Result { + let workdir_path = self.dot_git_path.parent().unwrap(); + + // Load gitignores + let ignores = workdir_path + .ancestors() + .filter_map(|dir| { + let ignore_path = dir.join(".gitignore"); + let content = self.fs.read_file_sync(ignore_path).ok()?; + let content = String::from_utf8(content).ok()?; + let mut builder = GitignoreBuilder::new(dir); + for line in content.lines() { + builder.add_line(Some(dir.into()), line).ok()?; + } + builder.build().ok() + }) + .collect::>(); + + // Load working copy files. + let git_files: HashMap = self + .fs + .files() + .iter() + .filter_map(|path| { + let repo_path = path.strip_prefix(workdir_path).ok()?; + let mut is_ignored = false; + for ignore in &ignores { + match ignore.matched_path_or_any_parents(path, false) { + ignore::Match::None => {} + ignore::Match::Ignore(_) => is_ignored = true, + ignore::Match::Whitelist(_) => break, + } + } + let content = self + .fs + .read_file_sync(path) + .ok() + .map(|content| String::from_utf8(content).unwrap())?; + Some((repo_path.into(), (content, is_ignored))) + }) + .collect(); + + self.with_state(|state| { + let mut entries = Vec::new(); + let paths = state + .head_contents + .keys() + .chain(state.index_contents.keys()) + .chain(git_files.keys()) + .collect::>(); + for path in paths { + if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) { + continue; + } + + let head = state.head_contents.get(path); + let index = state.index_contents.get(path); + let unmerged = state.unmerged_paths.get(path); + let fs = git_files.get(path); + let status = match (unmerged, head, index, fs) { + (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged), + (_, Some(head), Some(index), Some((fs, _))) => { + FileStatus::Tracked(TrackedStatus { + index_status: if head == index { + StatusCode::Unmodified + } else { + StatusCode::Modified + }, + worktree_status: if fs == index { + StatusCode::Unmodified + } else { + StatusCode::Modified + }, + }) + } + (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus { + index_status: if head == index { + StatusCode::Unmodified + } else { + StatusCode::Modified + }, + worktree_status: StatusCode::Deleted, + }), + (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Deleted, + worktree_status: StatusCode::Added, + }), + (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Deleted, + worktree_status: StatusCode::Deleted, + }), + (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Added, + worktree_status: if fs == index { + StatusCode::Unmodified + } else { + StatusCode::Modified + }, + }), + (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Added, + worktree_status: StatusCode::Deleted, + }), + (_, None, None, Some((_, is_ignored))) => { + if *is_ignored { + continue; + } + FileStatus::Untracked + } + (_, None, None, None) => { + unreachable!(); + } + }; + if status + != FileStatus::Tracked(TrackedStatus { + index_status: StatusCode::Unmodified, + worktree_status: StatusCode::Unmodified, + }) + { + entries.push((path.clone(), status)); + } + } + entries.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(GitStatus { + entries: entries.into(), + }) + }) + } + + fn branches(&self) -> BoxFuture>> { + self.with_state_async(false, move |state| { + let current_branch = &state.current_branch_name; + Ok(state + .branches + .iter() + .map(|branch_name| Branch { + is_head: Some(branch_name) == current_branch.as_ref(), + name: branch_name.into(), + most_recent_commit: None, + upstream: None, + }) + .collect()) + }) + } + + fn change_branch(&self, name: String, _cx: AsyncApp) -> BoxFuture> { + self.with_state_async(true, |state| { + state.current_branch_name = Some(name); + Ok(()) + }) + } + + fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture> { + self.with_state_async(true, move |state| { + state.branches.insert(name.to_owned()); + Ok(()) + }) + } + + fn blame( + &self, + path: RepoPath, + _content: Rope, + _cx: &mut AsyncApp, + ) -> BoxFuture> { + self.with_state_async(false, move |state| { + state + .blames + .get(&path) + .with_context(|| format!("failed to get blame for {:?}", path.0)) + .cloned() + }) + } + + fn stage_paths( + &self, + _paths: Vec, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn unstage_paths( + &self, + _paths: Vec, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn commit( + &self, + _message: gpui::SharedString, + _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn push( + &self, + _branch: String, + _remote: String, + _options: Option, + _askpass: AskPassSession, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn pull( + &self, + _branch: String, + _remote: String, + _askpass: AskPassSession, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn fetch( + &self, + _askpass: AskPassSession, + _env: HashMap, + _cx: AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } + + fn get_remotes( + &self, + _branch: Option, + _cx: AsyncApp, + ) -> BoxFuture>> { + unimplemented!() + } + + fn check_for_pushed_commit( + &self, + _cx: gpui::AsyncApp, + ) -> BoxFuture>> { + future::ready(Ok(Vec::new())).boxed() + } + + fn diff( + &self, + _diff: git::repository::DiffType, + _cx: gpui::AsyncApp, + ) -> BoxFuture> { + unimplemented!() + } +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index fe27c007c0..afe4fce4d7 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -5,36 +5,23 @@ mod mac_watcher; pub mod fs_watcher; use anyhow::{anyhow, Context as _, Result}; -#[cfg(any(test, feature = "test-support"))] -use collections::HashMap; -#[cfg(any(test, feature = "test-support"))] -use git::status::StatusCode; -#[cfg(any(test, feature = "test-support"))] -use git::status::TrackedStatus; -#[cfg(any(test, feature = "test-support"))] -use git::{repository::RepoPath, status::FileStatus}; - #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; +use gpui::App; +use gpui::Global; +use gpui::ReadGlobal as _; use std::borrow::Cow; -#[cfg(any(test, feature = "test-support"))] -use std::collections::HashSet; -#[cfg(unix)] -use std::os::fd::AsFd; -#[cfg(unix)] -use std::os::fd::AsRawFd; use util::command::new_std_command; #[cfg(unix)] -use std::os::unix::fs::MetadataExt; +use std::os::fd::{AsFd, AsRawFd}; #[cfg(unix)] -use std::os::unix::fs::FileTypeExt; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; use async_tar::Archive; use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt}; use git::repository::{GitRepository, RealGitRepository}; -use gpui::{App, Global, ReadGlobal}; use rope::Rope; use serde::{Deserialize, Serialize}; use smol::io::AsyncWriteExt; @@ -47,12 +34,18 @@ use std::{ }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; -use util::ResultExt; +#[cfg(any(test, feature = "test-support"))] +mod fake_git_repo; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] -use git::FakeGitRepositoryState; +use fake_git_repo::FakeGitRepositoryState; +#[cfg(any(test, feature = "test-support"))] +use git::{ + repository::RepoPath, + status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus}, +}; #[cfg(any(test, feature = "test-support"))] use parking_lot::Mutex; #[cfg(any(test, feature = "test-support"))] @@ -708,7 +701,7 @@ impl Fs for RealFs { Arc, ) { use parking_lot::Mutex; - use util::paths::SanitizedPath; + use util::{paths::SanitizedPath, ResultExt as _}; let (tx, rx) = smol::channel::unbounded(); let pending_paths: Arc>> = Default::default(); @@ -758,14 +751,10 @@ impl Fs for RealFs { } fn open_repo(&self, dotgit_path: &Path) -> Option> { - // with libgit2, we can open git repo from an existing work dir - // https://libgit2.org/docs/reference/main/repository/git_repository_open.html - let workdir_root = dotgit_path.parent()?; - let repo = git2::Repository::open(workdir_root).log_err()?; Some(Arc::new(RealGitRepository::new( - repo, + dotgit_path, self.git_binary_path.clone(), - ))) + )?)) } fn git_init(&self, abs_work_directory_path: &Path, fallback_branch_name: String) -> Result<()> { @@ -885,7 +874,7 @@ enum FakeFsEntry { mtime: MTime, len: u64, entries: BTreeMap>>, - git_repo_state: Option>>, + git_repo_state: Option>>, }, Symlink { target: PathBuf, @@ -1254,9 +1243,9 @@ impl FakeFs { .boxed() } - pub fn with_git_state(&self, dot_git: &Path, emit_git_event: bool, f: F) + pub fn with_git_state(&self, dot_git: &Path, emit_git_event: bool, f: F) -> T where - F: FnOnce(&mut FakeGitRepositoryState), + F: FnOnce(&mut FakeGitRepositoryState) -> T, { let mut state = self.state.lock(); let entry = state.read_path(dot_git).unwrap(); @@ -1271,11 +1260,13 @@ impl FakeFs { }); let mut repo_state = repo_state.lock(); - f(&mut repo_state); + let result = f(&mut repo_state); if emit_git_event { state.emit_event([(dot_git, None)]); } + + result } else { panic!("not a directory"); } @@ -1302,6 +1293,21 @@ impl FakeFs { }) } + pub fn set_unmerged_paths_for_repo( + &self, + dot_git: &Path, + unmerged_state: &[(RepoPath, UnmergedStatus)], + ) { + self.with_git_state(dot_git, true, |state| { + state.unmerged_paths.clear(); + state.unmerged_paths.extend( + unmerged_state + .iter() + .map(|(path, content)| (path.clone(), *content)), + ); + }); + } + pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(RepoPath, String)]) { self.with_git_state(dot_git, true, |state| { state.index_contents.clear(); @@ -1346,80 +1352,20 @@ impl FakeFs { }, )); }); - self.recalculate_git_status(dot_git); } - pub fn recalculate_git_status(&self, dot_git: &Path) { - let git_files: HashMap<_, _> = self - .files() - .iter() - .filter_map(|path| { - let repo_path = - RepoPath::new(path.strip_prefix(dot_git.parent().unwrap()).ok()?.into()); - let content = self - .read_file_sync(path) - .ok() - .map(|content| String::from_utf8(content).unwrap()); - Some((repo_path, content?)) - }) - .collect(); - self.with_git_state(dot_git, false, |state| { - state.statuses.clear(); - let mut paths: HashSet<_> = state.head_contents.keys().collect(); - paths.extend(state.index_contents.keys()); - paths.extend(git_files.keys()); - for path in paths { - let head = state.head_contents.get(path); - let index = state.index_contents.get(path); - let fs = git_files.get(path); - let status = match (head, index, fs) { - (Some(head), Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus { - index_status: if head == index { - StatusCode::Unmodified - } else { - StatusCode::Modified - }, - worktree_status: if fs == index { - StatusCode::Unmodified - } else { - StatusCode::Modified - }, - }), - (Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus { - index_status: if head == index { - StatusCode::Unmodified - } else { - StatusCode::Modified - }, - worktree_status: StatusCode::Deleted, - }), - (Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Deleted, - worktree_status: StatusCode::Added, - }), - (Some(_), None, None) => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Deleted, - worktree_status: StatusCode::Deleted, - }), - (None, Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: if fs == index { - StatusCode::Unmodified - } else { - StatusCode::Modified - }, - }), - (None, Some(_), None) => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: StatusCode::Deleted, - }), - (None, None, Some(_)) => FileStatus::Untracked, - (None, None, None) => { - unreachable!(); - } - }; - state.statuses.insert(path.clone(), status); - } + pub fn set_head_and_index_for_repo( + &self, + dot_git: &Path, + contents_by_path: &[(RepoPath, String)], + ) { + self.with_git_state(dot_git, true, |state| { + state.head_contents.clear(); + state.index_contents.clear(); + state.head_contents.extend(contents_by_path.iter().cloned()); + state + .index_contents + .extend(contents_by_path.iter().cloned()); }); } @@ -1430,38 +1376,85 @@ impl FakeFs { }); } - pub fn set_status_for_repo_via_working_copy_change( - &self, - dot_git: &Path, - statuses: &[(&Path, FileStatus)], - ) { - self.with_git_state(dot_git, false, |state| { - state.statuses.clear(); - state.statuses.extend( - statuses - .iter() - .map(|(path, content)| ((**path).into(), *content)), - ); - }); - self.state.lock().emit_event( - statuses - .iter() - .map(|(path, _)| (dot_git.parent().unwrap().join(path), None)), - ); - } - - pub fn set_status_for_repo_via_git_operation( - &self, - dot_git: &Path, - statuses: &[(&Path, FileStatus)], - ) { + /// Put the given git repository into a state with the given status, + /// by mutating the head, index, and unmerged state. + pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) { + let workdir_path = dot_git.parent().unwrap(); + let workdir_contents = self.files_with_contents(&workdir_path); self.with_git_state(dot_git, true, |state| { - state.statuses.clear(); - state.statuses.extend( - statuses + state.index_contents.clear(); + state.head_contents.clear(); + state.unmerged_paths.clear(); + for (path, content) in workdir_contents { + let repo_path: RepoPath = path.strip_prefix(&workdir_path).unwrap().into(); + let status = statuses .iter() - .map(|(path, content)| ((**path).into(), *content)), - ); + .find_map(|(p, status)| (**p == *repo_path.0).then_some(status)); + let mut content = String::from_utf8_lossy(&content).to_string(); + + let mut index_content = None; + let mut head_content = None; + match status { + None => { + index_content = Some(content.clone()); + head_content = Some(content); + } + Some(FileStatus::Untracked | FileStatus::Ignored) => {} + Some(FileStatus::Unmerged(unmerged_status)) => { + state + .unmerged_paths + .insert(repo_path.clone(), *unmerged_status); + content.push_str(" (unmerged)"); + index_content = Some(content.clone()); + head_content = Some(content); + } + Some(FileStatus::Tracked(TrackedStatus { + index_status, + worktree_status, + })) => { + match worktree_status { + StatusCode::Modified => { + let mut content = content.clone(); + content.push_str(" (modified in working copy)"); + index_content = Some(content); + } + StatusCode::TypeChanged | StatusCode::Unmodified => { + index_content = Some(content.clone()); + } + StatusCode::Added => {} + StatusCode::Deleted | StatusCode::Renamed | StatusCode::Copied => { + panic!("cannot create these statuses for an existing file"); + } + }; + match index_status { + StatusCode::Modified => { + let mut content = index_content.clone().expect( + "file cannot be both modified in index and created in working copy", + ); + content.push_str(" (modified in index)"); + head_content = Some(content); + } + StatusCode::TypeChanged | StatusCode::Unmodified => { + head_content = Some(index_content.clone().expect("file cannot be both unmodified in index and created in working copy")); + } + StatusCode::Added => {} + StatusCode::Deleted => { + head_content = Some("".into()); + } + StatusCode::Renamed | StatusCode::Copied => { + panic!("cannot create these statuses for an existing file"); + } + }; + } + }; + + if let Some(content) = index_content { + state.index_contents.insert(repo_path.clone(), content); + } + if let Some(content) = head_content { + state.head_contents.insert(repo_path.clone(), content); + } + } }); } @@ -1541,6 +1534,32 @@ impl FakeFs { result } + pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec)> { + let mut result = Vec::new(); + let mut queue = collections::VecDeque::new(); + queue.push_back(( + PathBuf::from(util::path!("/")), + self.state.lock().root.clone(), + )); + while let Some((path, entry)) = queue.pop_front() { + let e = entry.lock(); + match &*e { + FakeFsEntry::File { content, .. } => { + if path.starts_with(prefix) { + result.push((path, content.clone())); + } + } + FakeFsEntry::Dir { entries, .. } => { + for (name, entry) in entries { + queue.push_back((path.join(name), entry.clone())); + } + } + FakeFsEntry::Symlink { .. } => {} + } + } + result + } + /// How many `read_dir` calls have been issued. pub fn read_dir_call_count(&self) -> usize { self.state.lock().read_dir_call_count @@ -2087,15 +2106,17 @@ impl Fs for FakeFs { let entry = state.read_path(abs_dot_git).unwrap(); let mut entry = entry.lock(); if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { - let state = git_repo_state - .get_or_insert_with(|| { - Arc::new(Mutex::new(FakeGitRepositoryState::new( - abs_dot_git.to_path_buf(), - state.git_event_tx.clone(), - ))) - }) - .clone(); - Some(git::FakeGitRepository::open(state)) + git_repo_state.get_or_insert_with(|| { + Arc::new(Mutex::new(FakeGitRepositoryState::new( + abs_dot_git.to_path_buf(), + state.git_event_tx.clone(), + ))) + }); + Some(Arc::new(fake_git_repo::FakeGitRepository { + fs: self.this.upgrade().unwrap(), + executor: self.executor.clone(), + dot_git_path: abs_dot_git.to_path_buf(), + })) } else { None } diff --git a/crates/git/src/fake_repository.rs b/crates/git/src/fake_repository.rs deleted file mode 100644 index 553dc6331b..0000000000 --- a/crates/git/src/fake_repository.rs +++ /dev/null @@ -1,304 +0,0 @@ -use crate::{ - blame::Blame, - repository::{ - Branch, CommitDetails, DiffType, GitRepository, PushOptions, Remote, RemoteCommandOutput, - RepoPath, ResetMode, - }, - status::{FileStatus, GitStatus}, -}; -use anyhow::{Context, Result}; -use askpass::AskPassSession; -use collections::{HashMap, HashSet}; -use futures::{future::BoxFuture, FutureExt as _}; -use gpui::{AsyncApp, SharedString}; -use parking_lot::Mutex; -use rope::Rope; -use std::{path::PathBuf, sync::Arc}; - -#[derive(Debug, Clone)] -pub struct FakeGitRepository { - state: Arc>, -} - -#[derive(Debug, Clone)] -pub struct FakeGitRepositoryState { - pub path: PathBuf, - pub event_emitter: smol::channel::Sender, - pub head_contents: HashMap, - pub index_contents: HashMap, - pub blames: HashMap, - pub statuses: HashMap, - pub current_branch_name: Option, - pub branches: HashSet, - pub simulated_index_write_error_message: Option, -} - -impl FakeGitRepository { - pub fn open(state: Arc>) -> Arc { - Arc::new(FakeGitRepository { state }) - } -} - -impl FakeGitRepositoryState { - pub fn new(path: PathBuf, event_emitter: smol::channel::Sender) -> Self { - FakeGitRepositoryState { - path, - event_emitter, - head_contents: Default::default(), - index_contents: Default::default(), - blames: Default::default(), - statuses: Default::default(), - current_branch_name: Default::default(), - branches: Default::default(), - simulated_index_write_error_message: None, - } - } -} - -impl GitRepository for FakeGitRepository { - fn reload_index(&self) {} - - fn load_index_text(&self, path: RepoPath, cx: AsyncApp) -> BoxFuture> { - let state = self.state.lock(); - let content = state.index_contents.get(path.as_ref()).cloned(); - let executor = cx.background_executor().clone(); - async move { - executor.simulate_random_delay().await; - content - } - .boxed() - } - - fn load_committed_text(&self, path: RepoPath, cx: AsyncApp) -> BoxFuture> { - let state = self.state.lock(); - let content = state.head_contents.get(path.as_ref()).cloned(); - let executor = cx.background_executor().clone(); - async move { - executor.simulate_random_delay().await; - content - } - .boxed() - } - - fn set_index_text( - &self, - path: RepoPath, - content: Option, - _env: HashMap, - cx: AsyncApp, - ) -> BoxFuture> { - let state = self.state.clone(); - let executor = cx.background_executor().clone(); - async move { - executor.simulate_random_delay().await; - - let mut state = state.lock(); - if let Some(message) = state.simulated_index_write_error_message.clone() { - return Err(anyhow::anyhow!(message)); - } - - if let Some(content) = content { - state.index_contents.insert(path.clone(), content); - } else { - state.index_contents.remove(&path); - } - state - .event_emitter - .try_send(state.path.clone()) - .expect("Dropped repo change event"); - - Ok(()) - } - .boxed() - } - - fn remote_url(&self, _name: &str) -> Option { - None - } - - fn head_sha(&self) -> Option { - None - } - - fn merge_head_shas(&self) -> Vec { - vec![] - } - - fn show(&self, _: String, _: AsyncApp) -> BoxFuture> { - unimplemented!() - } - - fn reset(&self, _: String, _: ResetMode, _: HashMap) -> BoxFuture> { - unimplemented!() - } - - fn checkout_files( - &self, - _: String, - _: Vec, - _: HashMap, - ) -> BoxFuture> { - unimplemented!() - } - - fn path(&self) -> PathBuf { - let state = self.state.lock(); - state.path.clone() - } - - fn main_repository_path(&self) -> PathBuf { - self.path() - } - - fn status(&self, path_prefixes: &[RepoPath]) -> Result { - let state = self.state.lock(); - - let mut entries = state - .statuses - .iter() - .filter_map(|(repo_path, status)| { - if path_prefixes - .iter() - .any(|path_prefix| repo_path.0.starts_with(path_prefix)) - { - Some((repo_path.to_owned(), *status)) - } else { - None - } - }) - .collect::>(); - entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b)); - - Ok(GitStatus { - entries: entries.into(), - }) - } - - fn branches(&self) -> BoxFuture>> { - let state = self.state.lock(); - let current_branch = &state.current_branch_name; - let result = Ok(state - .branches - .iter() - .map(|branch_name| Branch { - is_head: Some(branch_name) == current_branch.as_ref(), - name: branch_name.into(), - most_recent_commit: None, - upstream: None, - }) - .collect()); - - async { result }.boxed() - } - - fn change_branch(&self, name: String, _: AsyncApp) -> BoxFuture> { - let mut state = self.state.lock(); - state.current_branch_name = Some(name.to_owned()); - state - .event_emitter - .try_send(state.path.clone()) - .expect("Dropped repo change event"); - async { Ok(()) }.boxed() - } - - fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture> { - let mut state = self.state.lock(); - state.branches.insert(name.to_owned()); - state - .event_emitter - .try_send(state.path.clone()) - .expect("Dropped repo change event"); - async { Ok(()) }.boxed() - } - - fn blame( - &self, - path: RepoPath, - _content: Rope, - _cx: &mut AsyncApp, - ) -> BoxFuture> { - let state = self.state.lock(); - let result = state - .blames - .get(&path) - .with_context(|| format!("failed to get blame for {:?}", path.0)) - .cloned(); - async { result }.boxed() - } - - fn stage_paths( - &self, - _paths: Vec, - _env: HashMap, - _cx: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn unstage_paths( - &self, - _paths: Vec, - _env: HashMap, - _cx: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn commit( - &self, - _message: SharedString, - _name_and_email: Option<(SharedString, SharedString)>, - _env: HashMap, - _: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn push( - &self, - _branch: String, - _remote: String, - _options: Option, - _ask_pass: AskPassSession, - _env: HashMap, - _cx: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn pull( - &self, - _branch: String, - _remote: String, - _ask_pass: AskPassSession, - _env: HashMap, - _cx: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn fetch( - &self, - _ask_pass: AskPassSession, - _env: HashMap, - _cx: AsyncApp, - ) -> BoxFuture> { - unimplemented!() - } - - fn get_remotes( - &self, - _branch: Option, - _cx: AsyncApp, - ) -> BoxFuture>> { - unimplemented!() - } - - fn check_for_pushed_commit(&self, _cx: AsyncApp) -> BoxFuture>> { - unimplemented!() - } - - fn diff(&self, _diff: DiffType, _cx: AsyncApp) -> BoxFuture> { - unimplemented!() - } -} diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 4eb1c09250..bc1605a87b 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -5,12 +5,6 @@ mod remote; pub mod repository; pub mod status; -#[cfg(any(test, feature = "test-support"))] -mod fake_repository; - -#[cfg(any(test, feature = "test-support"))] -pub use fake_repository::*; - pub use crate::hosting_provider::*; pub use crate::remote::*; use anyhow::{anyhow, Context as _, Result}; diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 8315773f82..8bd43fe959 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,7 +1,6 @@ use crate::status::GitStatus; use crate::SHORT_SHA_LENGTH; use anyhow::{anyhow, Context as _, Result}; -use askpass::{AskPassResult, AskPassSession}; use collections::HashMap; use futures::future::BoxFuture; use futures::{select_biased, AsyncWriteExt, FutureExt as _}; @@ -24,6 +23,8 @@ use sum_tree::MapSeekTarget; use util::command::new_smol_command; use util::ResultExt; +pub use askpass::{AskPassResult, AskPassSession}; + pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -311,11 +312,13 @@ pub struct RealGitRepository { } impl RealGitRepository { - pub fn new(repository: git2::Repository, git_binary_path: Option) -> Self { - Self { + pub fn new(dotgit_path: &Path, git_binary_path: Option) -> Option { + let workdir_root = dotgit_path.parent()?; + let repository = git2::Repository::open(workdir_root).log_err()?; + Some(Self { repository: Arc::new(Mutex::new(repository)), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), - } + }) } fn working_directory(&self) -> Result { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 93cd4eba55..f5f4e22ddb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4474,7 +4474,7 @@ mod tests { ) .await; - fs.set_status_for_repo_via_git_operation( + fs.set_status_for_repo( Path::new(path!("/root/zed/.git")), &[ ( diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index fc8b3945c5..010d2cc5ea 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1291,16 +1291,13 @@ mod preview { #[cfg(not(target_os = "windows"))] #[cfg(test)] mod tests { - use std::path::Path; - - use collections::HashMap; use db::indoc; use editor::test::editor_test_context::{assert_state_with_diff, EditorTestContext}; - use git::status::{StatusCode, TrackedStatus}; use gpui::TestAppContext; use project::FakeFs; use serde_json::json; use settings::SettingsStore; + use std::path::Path; use unindent::Unindent as _; use util::path; @@ -1353,16 +1350,6 @@ mod tests { path!("/project/.git").as_ref(), &[("foo.txt".into(), "foo\n".into())], ); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.statuses = HashMap::from_iter([( - "foo.txt".into(), - TrackedStatus { - index_status: StatusCode::Unmodified, - worktree_status: StatusCode::Modified, - } - .into(), - )]); - }); cx.run_until_parked(); let editor = diff.update(cx, |diff, _| diff.editor.clone()); @@ -1409,33 +1396,13 @@ mod tests { }); cx.run_until_parked(); - fs.set_head_for_repo( + fs.set_head_and_index_for_repo( path!("/project/.git").as_ref(), &[ ("bar".into(), "bar\n".into()), ("foo".into(), "foo\n".into()), ], ); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.statuses = HashMap::from_iter([ - ( - "bar".into(), - TrackedStatus { - index_status: StatusCode::Unmodified, - worktree_status: StatusCode::Modified, - } - .into(), - ), - ( - "foo".into(), - TrackedStatus { - index_status: StatusCode::Unmodified, - worktree_status: StatusCode::Modified, - } - .into(), - ), - ]); - }); cx.run_until_parked(); let editor = cx.update_window_entity(&diff, |diff, window, cx| { @@ -1515,16 +1482,6 @@ mod tests { path!("/project/.git").as_ref(), &[("foo".into(), "original\n".into())], ); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.statuses = HashMap::from_iter([( - "foo".into(), - TrackedStatus { - index_status: StatusCode::Unmodified, - worktree_status: StatusCode::Modified, - } - .into(), - )]); - }); cx.run_until_parked(); let diff_editor = diff.update(cx, |diff, _| diff.editor.clone()); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6e4883bf2e..8647b3e821 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -6050,11 +6050,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { ) .await; - fs.set_head_for_repo( - "/dir/.git".as_ref(), - &[("file.txt".into(), committed_contents.clone())], - ); - fs.set_index_for_repo( + fs.set_head_and_index_for_repo( "/dir/.git".as_ref(), &[("file.txt".into(), committed_contents.clone())], ); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5246374505..6538a52bb0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6756,74 +6756,60 @@ mod tests { #[gpui::test] async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { - use git::status::{FileStatus, StatusCode, TrackedStatus}; - use std::path::Path; - init_test_with_editor(cx); let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "tree1": { ".git": {}, "dir1": { - "modified1.txt": "", - "unmodified1.txt": "", - "modified2.txt": "", + "modified1.txt": "1", + "unmodified1.txt": "1", + "modified2.txt": "1", }, "dir2": { - "modified3.txt": "", - "unmodified2.txt": "", + "modified3.txt": "1", + "unmodified2.txt": "1", }, - "modified4.txt": "", - "unmodified3.txt": "", + "modified4.txt": "1", + "unmodified3.txt": "1", }, "tree2": { ".git": {}, "dir3": { - "modified5.txt": "", - "unmodified4.txt": "", + "modified5.txt": "1", + "unmodified4.txt": "1", }, - "modified6.txt": "", - "unmodified5.txt": "", + "modified6.txt": "1", + "unmodified5.txt": "1", } }), ) .await; // Mark files as git modified - let tree1_modified_files = [ - "dir1/modified1.txt", - "dir1/modified2.txt", - "modified4.txt", - "dir2/modified3.txt", - ]; - - let tree2_modified_files = ["dir3/modified5.txt", "modified6.txt"]; - - let root1_dot_git = Path::new("/root/tree1/.git"); - let root2_dot_git = Path::new("/root/tree2/.git"); - let set_value = FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Modified, - worktree_status: StatusCode::Modified, - }); - - fs.with_git_state(&root1_dot_git, true, |git_repo_state| { - for file_path in tree1_modified_files { - git_repo_state.statuses.insert(file_path.into(), set_value); - } - }); - - fs.with_git_state(&root2_dot_git, true, |git_repo_state| { - for file_path in tree2_modified_files { - git_repo_state.statuses.insert(file_path.into(), set_value); - } - }); + fs.set_git_content_for_repo( + path!("/root/tree1/.git").as_ref(), + &[ + ("dir1/modified1.txt".into(), "modified".into(), None), + ("dir1/modified2.txt".into(), "modified".into(), None), + ("modified4.txt".into(), "modified".into(), None), + ("dir2/modified3.txt".into(), "modified".into(), None), + ], + ); + fs.set_git_content_for_repo( + path!("/root/tree2/.git").as_ref(), + &[ + ("dir3/modified5.txt".into(), "modified".into(), None), + ("modified6.txt".into(), "modified".into(), None), + ], + ); let project = Project::test( fs.clone(), - ["/root/tree1".as_ref(), "/root/tree2".as_ref()], + [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()], cx, ) .await; diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 2f40a02653..c838463ae7 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -700,7 +700,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { }); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", "tree": { @@ -717,9 +717,16 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { }), ) .await; + fs.set_head_and_index_for_repo( + path!("/root/tree/.git").as_ref(), + &[ + (".gitignore".into(), "ignored-dir\n".into()), + ("tracked-dir/tracked-file1".into(), "".into()), + ], + ); let tree = Worktree::local( - "/root/tree".as_ref(), + path!("/root/tree").as_ref(), true, fs.clone(), Default::default(), @@ -745,28 +752,28 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true); }); - fs.set_status_for_repo_via_working_copy_change( - Path::new("/root/tree/.git"), - &[( - Path::new("tracked-dir/tracked-file2"), - StatusCode::Added.index(), - )], + fs.create_file( + path!("/root/tree/tracked-dir/tracked-file2").as_ref(), + Default::default(), + ) + .await + .unwrap(); + fs.set_index_for_repo( + path!("/root/tree/.git").as_ref(), + &[ + (".gitignore".into(), "ignored-dir\n".into()), + ("tracked-dir/tracked-file1".into(), "".into()), + ("tracked-dir/tracked-file2".into(), "".into()), + ], ); - fs.create_file( - "/root/tree/tracked-dir/tracked-file2".as_ref(), + path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(), Default::default(), ) .await .unwrap(); fs.create_file( - "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(), - Default::default(), - ) - .await - .unwrap(); - fs.create_file( - "/root/tree/ignored-dir/ignored-file2".as_ref(), + path!("/root/tree/ignored-dir/ignored-file2").as_ref(), Default::default(), ) .await @@ -792,7 +799,7 @@ async fn test_update_gitignore(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ ".git": {}, ".gitignore": "*.txt\n", @@ -802,8 +809,16 @@ async fn test_update_gitignore(cx: &mut TestAppContext) { ) .await; + fs.set_head_and_index_for_repo( + path!("/root/.git").as_ref(), + &[ + (".gitignore".into(), "*.txt\n".into()), + ("a.xml".into(), "".into()), + ], + ); + let tree = Worktree::local( - "/root".as_ref(), + path!("/root").as_ref(), true, fs.clone(), Default::default(), @@ -822,19 +837,24 @@ async fn test_update_gitignore(cx: &mut TestAppContext) { .recv() .await; + // One file is unmodified, the other is ignored. cx.read(|cx| { let tree = tree.read(cx); assert_entry_git_state(tree, "a.xml", None, false); assert_entry_git_state(tree, "b.txt", None, true); }); - fs.atomic_write("/root/.gitignore".into(), "*.xml".into()) + // Change the gitignore, and stage the newly non-ignored file. + fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into()) .await .unwrap(); - - fs.set_status_for_repo_via_working_copy_change( - Path::new("/root/.git"), - &[(Path::new("b.txt"), StatusCode::Added.index())], + fs.set_index_for_repo( + Path::new(path!("/root/.git")), + &[ + (".gitignore".into(), "*.txt\n".into()), + ("a.xml".into(), "".into()), + ("b.txt".into(), "Some text".into()), + ], ); cx.executor().run_until_parked(); @@ -1458,19 +1478,24 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { // Create a worktree with a git directory. let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ ".git": {}, "a.txt": "", - "b": { + "b": { "c.txt": "", }, }), ) .await; + fs.set_head_and_index_for_repo( + path!("/root/.git").as_ref(), + &[("a.txt".into(), "".into()), ("b/c.txt".into(), "".into())], + ); + cx.run_until_parked(); let tree = Worktree::local( - "/root".as_ref(), + path!("/root").as_ref(), true, fs.clone(), Default::default(), @@ -1490,7 +1515,7 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { // Regression test: after the directory is scanned, touch the git repo's // working directory, bumping its mtime. That directory keeps its project // entry id after the directories are re-scanned. - fs.touch_path("/root").await; + fs.touch_path(path!("/root")).await; cx.executor().run_until_parked(); let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| { @@ -1504,9 +1529,12 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { // Regression test: changes to the git repository should still be // detected. - fs.set_status_for_repo_via_git_operation( - Path::new("/root/.git"), - &[(Path::new("b/c.txt"), StatusCode::Modified.index())], + fs.set_head_for_repo( + path!("/root/.git").as_ref(), + &[ + ("a.txt".into(), "".into()), + ("b/c.txt".into(), "something-else".into()), + ], ); cx.executor().run_until_parked(); cx.executor().advance_clock(Duration::from_secs(1)); @@ -2886,7 +2914,7 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "x": { ".git": {}, @@ -2908,24 +2936,24 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) { ) .await; - fs.set_status_for_repo_via_git_operation( - Path::new("/root/x/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/x/.git")), &[ (Path::new("x2.txt"), StatusCode::Modified.index()), (Path::new("z.txt"), StatusCode::Added.index()), ], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/x/y/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/x/y/.git")), &[(Path::new("y1.txt"), CONFLICT)], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/z/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/z/.git")), &[(Path::new("z2.txt"), StatusCode::Added.index())], ); let tree = Worktree::local( - Path::new("/root"), + Path::new(path!("/root")), true, fs.clone(), Default::default(), @@ -2973,7 +3001,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ ".git": {}, "a": { @@ -2998,8 +3026,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { ) .await; - fs.set_status_for_repo_via_git_operation( - Path::new("/root/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/.git")), &[ (Path::new("a/b/c1.txt"), StatusCode::Added.index()), (Path::new("a/d/e2.txt"), StatusCode::Modified.index()), @@ -3008,7 +3036,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { ); let tree = Worktree::local( - Path::new("/root"), + Path::new(path!("/root")), true, fs.clone(), Default::default(), @@ -3081,7 +3109,7 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "x": { ".git": {}, @@ -3102,24 +3130,24 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext ) .await; - fs.set_status_for_repo_via_git_operation( - Path::new("/root/x/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/x/.git")), &[(Path::new("x1.txt"), StatusCode::Added.index())], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/y/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/y/.git")), &[ (Path::new("y1.txt"), CONFLICT), (Path::new("y2.txt"), StatusCode::Modified.index()), ], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/z/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/z/.git")), &[(Path::new("z2.txt"), StatusCode::Modified.index())], ); let tree = Worktree::local( - Path::new("/root"), + Path::new(path!("/root")), true, fs.clone(), Default::default(), @@ -3183,7 +3211,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "x": { ".git": {}, @@ -3205,25 +3233,25 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { ) .await; - fs.set_status_for_repo_via_git_operation( - Path::new("/root/x/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/x/.git")), &[ (Path::new("x2.txt"), StatusCode::Modified.index()), (Path::new("z.txt"), StatusCode::Added.index()), ], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/x/y/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/x/y/.git")), &[(Path::new("y1.txt"), CONFLICT)], ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/z/.git"), + fs.set_status_for_repo( + Path::new(path!("/root/z/.git")), &[(Path::new("z2.txt"), StatusCode::Added.index())], ); let tree = Worktree::local( - Path::new("/root"), + Path::new(path!("/root")), true, fs.clone(), Default::default(), @@ -3639,6 +3667,7 @@ fn init_test(cx: &mut gpui::TestAppContext) { }); } +#[track_caller] fn assert_entry_git_state( tree: &Worktree, path: &str,