Finish removing git repository state and scanning logic from worktrees (#27568)
This PR completes the process of moving git repository state storage and scanning logic from the worktree crate to `project::git_store`. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
parent
8f25251faf
commit
e7290df02b
39 changed files with 3121 additions and 3529 deletions
|
@ -30,7 +30,6 @@ fs.workspace = true
|
|||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
git_hosting_providers.workspace = true
|
||||
gpui.workspace = true
|
||||
ignore.workspace = true
|
||||
language.workspace = true
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,10 @@
|
|||
use crate::{
|
||||
Entry, EntryKind, Event, PathChange, StatusEntry, WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
worktree_settings::WorktreeSettings,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::{
|
||||
GITIGNORE,
|
||||
repository::RepoPath,
|
||||
status::{FileStatus, StatusCode, TrackedStatus},
|
||||
};
|
||||
use git2::RepositoryInitOptions;
|
||||
use git::GITIGNORE;
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
|
@ -685,183 +680,6 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
|||
assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "",
|
||||
"ancestor-ignored-file1": "",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.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(
|
||||
path!("/root/tree").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
|
||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
|
||||
assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
|
||||
});
|
||||
|
||||
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(
|
||||
path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file(
|
||||
path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(
|
||||
tree,
|
||||
"tracked-dir/tracked-file2",
|
||||
Some(StatusCode::Added),
|
||||
false,
|
||||
);
|
||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
|
||||
assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
|
||||
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_update_gitignore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "*.txt\n",
|
||||
"a.xml": "<a></a>",
|
||||
"b.txt": "Some text"
|
||||
}),
|
||||
)
|
||||
.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(
|
||||
path!("/root").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("").into()])
|
||||
})
|
||||
.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);
|
||||
});
|
||||
|
||||
// Change the gitignore, and stage the newly non-ignored file.
|
||||
fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
|
||||
.await
|
||||
.unwrap();
|
||||
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();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(tree, "a.xml", None, true);
|
||||
assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2106,655 +1924,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
|
||||
// a directory which some program has already open.
|
||||
// This is a limitation of the Windows.
|
||||
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
let root = TempTree::new(json!({
|
||||
"projects": {
|
||||
"project1": {
|
||||
"a": "",
|
||||
"b": "",
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("projects/project1"));
|
||||
git_add("a", &repo);
|
||||
git_commit("init", &repo);
|
||||
std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path,
|
||||
root_path.join("projects/project1")
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"a".into()).map(|entry| entry.status),
|
||||
Some(StatusCode::Modified.worktree()),
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"b".into()).map(|entry| entry.status),
|
||||
Some(FileStatus::Untracked),
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::rename(
|
||||
root_path.join("projects/project1"),
|
||||
root_path.join("projects/project2"),
|
||||
)
|
||||
.unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path,
|
||||
root_path.join("projects/project2")
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"a".into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"b".into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
|
||||
// you can't rename a directory which some program has already open. This is a
|
||||
// limitation of the Windows. See:
|
||||
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_file_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
const IGNORE_RULE: &str = "**/target";
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
"b.txt": "bb",
|
||||
"c": {
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
"f.txt": "ffff",
|
||||
"target": {
|
||||
"build_file": "???"
|
||||
},
|
||||
".gitignore": IGNORE_RULE
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const A_TXT: &str = "a.txt";
|
||||
const B_TXT: &str = "b.txt";
|
||||
const E_TXT: &str = "c/d/e.txt";
|
||||
const F_TXT: &str = "f.txt";
|
||||
const DOTGITIGNORE: &str = ".gitignore";
|
||||
const BUILD_FILE: &str = "target/build_file";
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let mut repo = git_init(work_dir.as_path());
|
||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||
git_add(A_TXT, &repo);
|
||||
git_add(E_TXT, &repo);
|
||||
git_add(DOTGITIGNORE, &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let root_path = root.path();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.work_directory_abs_path,
|
||||
root_path.join("project")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
|
||||
// Modify a file in the working copy.
|
||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// The worktree detects that the file's git status has changed.
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&A_TXT.into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
|
||||
// Create a commit in the git repository.
|
||||
git_add(A_TXT, &repo);
|
||||
git_add(B_TXT, &repo);
|
||||
git_commit("Committing modified and added", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// The worktree detects that the files' git status have changed.
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(repo_entry.status_for_path(&B_TXT.into()), None);
|
||||
assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
|
||||
});
|
||||
|
||||
// Modify files in the working copy and perform git operations on other files.
|
||||
git_reset(0, &repo);
|
||||
git_remove_index(Path::new(B_TXT), &repo);
|
||||
git_stash(&mut repo);
|
||||
std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
|
||||
std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that more complex repo changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&E_TXT.into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
|
||||
std::fs::remove_dir_all(work_dir.join("c")).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(DOTGITIGNORE),
|
||||
[IGNORE_RULE, "f.txt"].join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Committing modified git ignore", &repo);
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let mut renamed_dir_name = "first_directory/second_directory";
|
||||
const RENAMED_FILE: &str = "rf.txt";
|
||||
|
||||
std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(renamed_dir_name).join(RENAMED_FILE),
|
||||
"new-contents",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry
|
||||
.status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
|
||||
.unwrap()
|
||||
.status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
|
||||
renamed_dir_name = "new_first_directory/second_directory";
|
||||
|
||||
std::fs::rename(
|
||||
work_dir.join("first_directory"),
|
||||
work_dir.join("new_first_directory"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repo_entry
|
||||
.status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
|
||||
.unwrap()
|
||||
.status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a", // Modified
|
||||
"b.txt": "bb", // Added
|
||||
"c.txt": "ccc", // Unchanged
|
||||
"d.txt": "dddd", // Deleted
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_add("d.txt", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
std::fs::remove_file(work_dir.join("d.txt")).unwrap();
|
||||
std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
[
|
||||
StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "b.txt".into(),
|
||||
status: FileStatus::Untracked,
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "d.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repository = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repository.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
[
|
||||
StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "b.txt".into(),
|
||||
status: FileStatus::Untracked,
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "c.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "d.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_remove_index(Path::new("d.txt"), &repo);
|
||||
git_commit("Another commit", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
std::fs::remove_file(work_dir.join("a.txt")).unwrap();
|
||||
std::fs::remove_file(work_dir.join("b.txt")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// Deleting an untracked entry, b.txt, should leave no status
|
||||
// a.txt was tracked, and so should have a status
|
||||
assert_eq!(
|
||||
entries,
|
||||
[StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
}]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"sub": {},
|
||||
"a.txt": "",
|
||||
},
|
||||
}));
|
||||
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
// a.txt exists in HEAD and the working copy but is deleted in the index.
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
git_remove_index("a.txt".as_ref(), &repo);
|
||||
// `sub` is a nested git repository.
|
||||
let _sub = git_init(&work_dir.join("sub"));
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// `sub` doesn't appear in our computed statuses.
|
||||
// a.txt appears with a combined `DA` status.
|
||||
assert_eq!(
|
||||
entries,
|
||||
[StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Added
|
||||
}
|
||||
.into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"my-repo": {
|
||||
// .git folder will go here
|
||||
"a.txt": "a",
|
||||
"sub-folder-1": {
|
||||
"sub-folder-2": {
|
||||
"c.txt": "cc",
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
|
||||
const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let git_repo_work_dir = root.path().join("my-repo");
|
||||
let repo = git_init(git_repo_work_dir.as_path());
|
||||
git_add(C_TXT, &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
// Open the worktree in subfolder
|
||||
let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
|
||||
let tree = Worktree::local(
|
||||
root.path().join(project_root),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
tree.flush_fs_events_in_root_git_repository(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Ensure that the git status is loaded correctly
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path.canonicalize().unwrap(),
|
||||
root.path().join("my-repo").canonicalize().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(repo.status_for_path(&C_TXT.into()), None);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&E_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked
|
||||
);
|
||||
});
|
||||
|
||||
// Now we simulate FS events, but ONLY in the .git folder that's outside
|
||||
// of out project root.
|
||||
// Meaning: we don't produce any FS events for files inside the project.
|
||||
git_add(E_TXT, &repo);
|
||||
git_commit("Second commit", &repo);
|
||||
tree.flush_fs_events_in_root_git_repository(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repos = snapshot.repositories().iter().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(repos.len(), 1);
|
||||
let repo_entry = repos.into_iter().next().unwrap();
|
||||
|
||||
assert!(snapshot.repositories.iter().next().is_some());
|
||||
|
||||
assert_eq!(repo_entry.status_for_path(&C_TXT.into()), None);
|
||||
assert_eq!(repo_entry.status_for_path(&E_TXT.into()), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
},
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("project"));
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("init", &repo);
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
git_branch("other-branch", &repo);
|
||||
git_checkout("refs/heads/other-branch", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("capitalize", &repo);
|
||||
let commit = repo
|
||||
.head()
|
||||
.expect("Failed to get HEAD")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
git_checkout("refs/heads/main", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("improve letter", &repo);
|
||||
git_cherry_pick(&commit, &repo);
|
||||
std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("No CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(
|
||||
git_status(&repo),
|
||||
collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
|
||||
);
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
// Attempt to manually simulate what `git cherry-pick --continue` would do.
|
||||
git_commit("whatevs", &repo);
|
||||
std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("Failed to remove CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, []);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2815,110 +1984,6 @@ fn test_unrelativize() {
|
|||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
let mut init_opts = RepositoryInitOptions::new();
|
||||
init_opts.initial_head("main");
|
||||
git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
||||
let path = path.as_ref();
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.add_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.remove_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_commit(msg: &'static str, repo: &git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
let oid = repo.index().unwrap().write_tree().unwrap();
|
||||
let tree = repo.find_tree(oid).unwrap();
|
||||
if let Ok(head) = repo.head() {
|
||||
let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
|
||||
let parent_commit = parent_obj.as_commit().unwrap();
|
||||
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
&[parent_commit],
|
||||
)
|
||||
.expect("Failed to commit with parent");
|
||||
} else {
|
||||
repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
|
||||
.expect("Failed to commit");
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
|
||||
repo.cherrypick(commit, None).expect("Failed to cherrypick");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_stash(repo: &mut git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
repo.stash_save(&signature, "N/A", None)
|
||||
.expect("Failed to stash");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_reset(offset: usize, repo: &git2::Repository) {
|
||||
let head = repo.head().expect("Couldn't get repo head");
|
||||
let object = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
let commit = object.as_commit().unwrap();
|
||||
let new_head = commit
|
||||
.parents()
|
||||
.inspect(|parnet| {
|
||||
parnet.message();
|
||||
})
|
||||
.nth(offset)
|
||||
.expect("Not enough history");
|
||||
repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
|
||||
.expect("Could not reset");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_branch(name: &str, repo: &git2::Repository) {
|
||||
let head = repo
|
||||
.head()
|
||||
.expect("Couldn't get repo head")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
repo.branch(name, &head, false).expect("Failed to commit");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_checkout(name: &str, repo: &git2::Repository) {
|
||||
repo.set_head(name).expect("Failed to set head");
|
||||
repo.checkout_head(None).expect("Failed to check out head");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_worktree_entries(
|
||||
tree: &Worktree,
|
||||
|
@ -2974,34 +2039,3 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
|||
WorktreeSettings::register(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_entry_git_state(
|
||||
tree: &Worktree,
|
||||
path: &str,
|
||||
index_status: Option<StatusCode>,
|
||||
is_ignored: bool,
|
||||
) {
|
||||
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||
let repos = tree.repositories().iter().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(repos.len(), 1);
|
||||
let repo_entry = repos.into_iter().next().unwrap();
|
||||
let status = repo_entry
|
||||
.status_for_path(&path.into())
|
||||
.map(|entry| entry.status);
|
||||
let expected = index_status.map(|index_status| {
|
||||
TrackedStatus {
|
||||
index_status,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
}
|
||||
.into()
|
||||
});
|
||||
assert_eq!(
|
||||
status, expected,
|
||||
"expected {path} to have git status: {expected:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.is_ignored, is_ignored,
|
||||
"expected {path} to have is_ignored: {is_ignored}"
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue