Cargo.lock 🔗
@@ -5238,8 +5238,8 @@ dependencies = [
"fsevent",
"futures 0.3.31",
"git",
- "git2",
"gpui",
+ "ignore",
"libc",
"log",
"notify 6.1.1",
Max Brunsfeld and Junkui Zhang created
This PR reworks the `FakeGitRepository` type that we use for testing git
interactions, to make it more realistic. In particular, the `status`
method now derives the Git status from the differences between HEAD, the
index, and the working copy. This way, if you modify a file in the
`FakeFs`, the Git repository's `status` method will reflect that
modification.
Release Notes:
- N/A
---------
Co-authored-by: Junkui Zhang <364772080@qq.com>
Cargo.lock | 2
crates/collab/src/tests/git_tests.rs | 1
crates/collab/src/tests/integration_tests.rs | 94
crates/collab/src/tests/random_project_collaboration_tests.rs | 59
crates/fs/Cargo.toml | 2
crates/fs/src/fake_git_repo.rs | 411 +++++
crates/fs/src/fs.rs | 309 ++-
crates/git/src/fake_repository.rs | 304 ---
crates/git/src/git.rs | 6
crates/git/src/repository.rs | 11
crates/git_ui/src/git_panel.rs | 2
crates/git_ui/src/project_diff.rs | 47
crates/project/src/project_tests.rs | 6
crates/project_panel/src/project_panel.rs | 72
crates/worktree/src/worktree_tests.rs | 139 +
15 files changed, 788 insertions(+), 677 deletions(-)
@@ -5238,8 +5238,8 @@ dependencies = [
"fsevent",
"futures 0.3.31",
"git",
- "git2",
"gpui",
+ "ignore",
"libc",
"log",
"notify 6.1.1",
@@ -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| {
@@ -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<Path>,
+ file: impl AsRef<Path>,
status: Option<FileStatus>,
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);
});
}
@@ -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::<Vec<_>>();
-
- let git_operation = rng.gen::<bool>();
-
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!(),
}
}
@@ -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
@@ -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<FakeFs>,
+ 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<PathBuf>,
+ pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
+ pub head_contents: HashMap<RepoPath, String>,
+ pub index_contents: HashMap<RepoPath, String>,
+ pub blames: HashMap<RepoPath, Blame>,
+ pub current_branch_name: Option<String>,
+ pub branches: HashSet<String>,
+ pub simulated_index_write_error_message: Option<String>,
+}
+
+impl FakeGitRepositoryState {
+ pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> 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<F, T>(&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<F, T>(&self, write: bool, f: F) -> BoxFuture<T>
+ 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<Option<String>> {
+ 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<Option<String>> {
+ self.with_state_async(false, move |state| {
+ state.head_contents.get(path.as_ref()).cloned()
+ })
+ }
+
+ fn set_index_text(
+ &self,
+ path: RepoPath,
+ content: Option<String>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<anyhow::Result<()>> {
+ 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<String> {
+ None
+ }
+
+ fn head_sha(&self) -> Option<String> {
+ None
+ }
+
+ fn merge_head_shas(&self) -> Vec<String> {
+ vec![]
+ }
+
+ fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
+ unimplemented!()
+ }
+
+ fn reset(
+ &self,
+ _commit: String,
+ _mode: ResetMode,
+ _env: HashMap<String, String>,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn checkout_files(
+ &self,
+ _commit: String,
+ _paths: Vec<RepoPath>,
+ _env: HashMap<String, String>,
+ ) -> BoxFuture<Result<()>> {
+ 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<GitStatus> {
+ 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::<Vec<_>>();
+
+ // Load working copy files.
+ let git_files: HashMap<RepoPath, (String, bool)> = 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::<HashSet<_>>();
+ 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<Result<Vec<Branch>>> {
+ 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<Result<()>> {
+ self.with_state_async(true, |state| {
+ state.current_branch_name = Some(name);
+ Ok(())
+ })
+ }
+
+ fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
+ 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<Result<git::blame::Blame>> {
+ 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<RepoPath>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn unstage_paths(
+ &self,
+ _paths: Vec<RepoPath>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn commit(
+ &self,
+ _message: gpui::SharedString,
+ _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn push(
+ &self,
+ _branch: String,
+ _remote: String,
+ _options: Option<PushOptions>,
+ _askpass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn pull(
+ &self,
+ _branch: String,
+ _remote: String,
+ _askpass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn fetch(
+ &self,
+ _askpass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn get_remotes(
+ &self,
+ _branch: Option<String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<Vec<Remote>>> {
+ unimplemented!()
+ }
+
+ fn check_for_pushed_commit(
+ &self,
+ _cx: gpui::AsyncApp,
+ ) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
+ future::ready(Ok(Vec::new())).boxed()
+ }
+
+ fn diff(
+ &self,
+ _diff: git::repository::DiffType,
+ _cx: gpui::AsyncApp,
+ ) -> BoxFuture<Result<String>> {
+ unimplemented!()
+ }
+}
@@ -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<dyn Watcher>,
) {
use parking_lot::Mutex;
- use util::paths::SanitizedPath;
+ use util::{paths::SanitizedPath, ResultExt as _};
let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
@@ -758,14 +751,10 @@ impl Fs for RealFs {
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
- // 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<String, Arc<Mutex<FakeFsEntry>>>,
- git_repo_state: Option<Arc<Mutex<git::FakeGitRepositoryState>>>,
+ git_repo_state: Option<Arc<Mutex<FakeGitRepositoryState>>>,
},
Symlink {
target: PathBuf,
@@ -1254,9 +1243,9 @@ impl FakeFs {
.boxed()
}
- pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
+ pub fn with_git_state<T, F>(&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<u8>)> {
+ 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
}
@@ -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<Mutex<FakeGitRepositoryState>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct FakeGitRepositoryState {
- pub path: PathBuf,
- pub event_emitter: smol::channel::Sender<PathBuf>,
- pub head_contents: HashMap<RepoPath, String>,
- pub index_contents: HashMap<RepoPath, String>,
- pub blames: HashMap<RepoPath, Blame>,
- pub statuses: HashMap<RepoPath, FileStatus>,
- pub current_branch_name: Option<String>,
- pub branches: HashSet<String>,
- pub simulated_index_write_error_message: Option<String>,
-}
-
-impl FakeGitRepository {
- pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
- Arc::new(FakeGitRepository { state })
- }
-}
-
-impl FakeGitRepositoryState {
- pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> 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<Option<String>> {
- 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<Option<String>> {
- 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<String>,
- _env: HashMap<String, String>,
- cx: AsyncApp,
- ) -> BoxFuture<anyhow::Result<()>> {
- 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<String> {
- None
- }
-
- fn head_sha(&self) -> Option<String> {
- None
- }
-
- fn merge_head_shas(&self) -> Vec<String> {
- vec![]
- }
-
- fn show(&self, _: String, _: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
- unimplemented!()
- }
-
- fn reset(&self, _: String, _: ResetMode, _: HashMap<String, String>) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn checkout_files(
- &self,
- _: String,
- _: Vec<RepoPath>,
- _: HashMap<String, String>,
- ) -> BoxFuture<Result<()>> {
- 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<GitStatus> {
- 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::<Vec<_>>();
- entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
-
- Ok(GitStatus {
- entries: entries.into(),
- })
- }
-
- fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
- 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<Result<()>> {
- 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<Result<()>> {
- 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<Result<crate::blame::Blame>> {
- 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<RepoPath>,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn unstage_paths(
- &self,
- _paths: Vec<RepoPath>,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn commit(
- &self,
- _message: SharedString,
- _name_and_email: Option<(SharedString, SharedString)>,
- _env: HashMap<String, String>,
- _: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn push(
- &self,
- _branch: String,
- _remote: String,
- _options: Option<PushOptions>,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn pull(
- &self,
- _branch: String,
- _remote: String,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn fetch(
- &self,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn get_remotes(
- &self,
- _branch: Option<String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<Vec<Remote>>> {
- unimplemented!()
- }
-
- fn check_for_pushed_commit(&self, _cx: AsyncApp) -> BoxFuture<Result<Vec<SharedString>>> {
- unimplemented!()
- }
-
- fn diff(&self, _diff: DiffType, _cx: AsyncApp) -> BoxFuture<Result<String>> {
- unimplemented!()
- }
-}
@@ -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};
@@ -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<PathBuf>) -> Self {
- Self {
+ pub fn new(dotgit_path: &Path, git_binary_path: Option<PathBuf>) -> Option<Self> {
+ 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<PathBuf> {
@@ -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")),
&[
(
@@ -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());
@@ -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())],
);
@@ -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;
@@ -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(
- "/root/tree/tracked-dir/tracked-file2".as_ref(),
+ 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/ancestor-ignored-file2".as_ref(),
+ path!("/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(), "<a></a>".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(), "<a></a>".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,