Migrate most callers of git-related worktree APIs to use the GitStore (#27225)
This is a pure refactoring PR that goes through all the git-related APIs exposed by the worktree crate and minimizes their use outside that crate, migrating callers of those APIs to read from the GitStore instead. This is to prepare for evacuating git repository state from worktrees and making the GitStore the new source of truth. Other drive-by changes: - `project::git` is now `project::git_store`, for consistency with the other project stores - the project panel's test module has been split into its own file Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
9134630841
commit
cf7d639fbc
26 changed files with 6480 additions and 6429 deletions
|
@ -66,9 +66,7 @@ use std::{
|
|||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use sum_tree::{
|
||||
Bias, Cursor, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit,
|
||||
};
|
||||
use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit};
|
||||
use text::{LineEnding, Rope};
|
||||
use util::{
|
||||
paths::{home_dir, PathMatcher, SanitizedPath},
|
||||
|
@ -197,7 +195,7 @@ pub struct RepositoryEntry {
|
|||
/// With this setup, this field would contain 2 entries, like so:
|
||||
/// - my_sub_folder_1/project_root/changed_file_1
|
||||
/// - my_sub_folder_2/changed_file_2
|
||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||
pub statuses_by_path: SumTree<StatusEntry>,
|
||||
work_directory_id: ProjectEntryId,
|
||||
pub work_directory: WorkDirectory,
|
||||
work_directory_abs_path: PathBuf,
|
||||
|
@ -2700,6 +2698,7 @@ impl Snapshot {
|
|||
Some(removed_entry.path)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<FileStatus> {
|
||||
let path = path.as_ref();
|
||||
self.repository_for_path(path).and_then(|repo| {
|
||||
|
@ -2955,19 +2954,12 @@ impl Snapshot {
|
|||
self.traverse_from_offset(true, true, include_ignored, start)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "test-support", test))]
|
||||
pub fn git_status(&self, work_dir: &Path) -> Option<Vec<StatusEntry>> {
|
||||
self.repositories
|
||||
.get(&PathKey(work_dir.into()), &())
|
||||
.map(|repo| repo.status().collect())
|
||||
}
|
||||
|
||||
pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
|
||||
&self.repositories
|
||||
}
|
||||
|
||||
/// Get the repository whose work directory corresponds to the given path.
|
||||
pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
|
||||
fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
|
||||
self.repositories.get(&work_directory, &()).cloned()
|
||||
}
|
||||
|
||||
|
@ -2982,13 +2974,14 @@ impl Snapshot {
|
|||
|
||||
/// Given an ordered iterator of entries, returns an iterator of those entries,
|
||||
/// along with their containing git repository.
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub fn entries_with_repositories<'a>(
|
||||
fn entries_with_repositories<'a>(
|
||||
&'a self,
|
||||
entries: impl 'a + Iterator<Item = &'a Entry>,
|
||||
) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
|
||||
let mut containing_repos = Vec::<&RepositoryEntry>::new();
|
||||
let mut repositories = self.repositories().iter().peekable();
|
||||
let mut repositories = self.repositories.iter().peekable();
|
||||
entries.map(move |entry| {
|
||||
while let Some(repository) = containing_repos.last() {
|
||||
if repository.directory_contains(&entry.path) {
|
||||
|
@ -3062,22 +3055,6 @@ impl Snapshot {
|
|||
&self.root_name
|
||||
}
|
||||
|
||||
pub fn root_git_entry(&self) -> Option<RepositoryEntry> {
|
||||
self.repositories
|
||||
.get(&PathKey(Path::new("").into()), &())
|
||||
.map(|entry| entry.to_owned())
|
||||
}
|
||||
|
||||
pub fn git_entry(&self, work_directory_path: Arc<Path>) -> Option<RepositoryEntry> {
|
||||
self.repositories
|
||||
.get(&PathKey(work_directory_path), &())
|
||||
.map(|entry| entry.to_owned())
|
||||
}
|
||||
|
||||
pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
|
||||
self.repositories.iter()
|
||||
}
|
||||
|
||||
pub fn scan_id(&self) -> usize {
|
||||
self.scan_id
|
||||
}
|
||||
|
@ -4087,8 +4064,8 @@ impl TryFrom<proto::StatusEntry> for StatusEntry {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PathProgress<'a> {
|
||||
max_path: &'a Path,
|
||||
pub struct PathProgress<'a> {
|
||||
pub max_path: &'a Path,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -6036,8 +6013,8 @@ impl WorktreeModelHandle for Entity<Worktree> {
|
|||
let tree = self.clone();
|
||||
let (fs, root_path, mut git_dir_scan_id) = self.update(cx, |tree, _| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
let root_entry = tree.root_git_entry().unwrap();
|
||||
let local_repo_entry = tree.get_local_repo(&root_entry).unwrap();
|
||||
let repository = tree.repositories.first().unwrap();
|
||||
let local_repo_entry = tree.get_local_repo(&repository).unwrap();
|
||||
(
|
||||
tree.fs.clone(),
|
||||
local_repo_entry.dot_git_dir_abs_path.clone(),
|
||||
|
@ -6046,11 +6023,11 @@ impl WorktreeModelHandle for Entity<Worktree> {
|
|||
});
|
||||
|
||||
let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| {
|
||||
let root_entry = tree.root_git_entry().unwrap();
|
||||
let repository = tree.repositories.first().unwrap();
|
||||
let local_repo_entry = tree
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.get_local_repo(&root_entry)
|
||||
.get_local_repo(&repository)
|
||||
.unwrap();
|
||||
|
||||
if local_repo_entry.git_dir_scan_id > *git_dir_scan_id {
|
||||
|
@ -6139,171 +6116,6 @@ impl Default for TraversalProgress<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GitEntryRef<'a> {
|
||||
pub entry: &'a Entry,
|
||||
pub git_summary: GitSummary,
|
||||
}
|
||||
|
||||
impl GitEntryRef<'_> {
|
||||
pub fn to_owned(&self) -> GitEntry {
|
||||
GitEntry {
|
||||
entry: self.entry.clone(),
|
||||
git_summary: self.git_summary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for GitEntryRef<'_> {
|
||||
type Target = Entry;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entry
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Entry> for GitEntryRef<'_> {
|
||||
fn as_ref(&self) -> &Entry {
|
||||
self.entry
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitEntry {
|
||||
pub entry: Entry,
|
||||
pub git_summary: GitSummary,
|
||||
}
|
||||
|
||||
impl GitEntry {
|
||||
pub fn to_ref(&self) -> GitEntryRef {
|
||||
GitEntryRef {
|
||||
entry: &self.entry,
|
||||
git_summary: self.git_summary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for GitEntry {
|
||||
type Target = Entry;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entry
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Entry> for GitEntry {
|
||||
fn as_ref(&self) -> &Entry {
|
||||
&self.entry
|
||||
}
|
||||
}
|
||||
|
||||
/// Walks the worktree entries and their associated git statuses.
|
||||
pub struct GitTraversal<'a> {
|
||||
traversal: Traversal<'a>,
|
||||
current_entry_summary: Option<GitSummary>,
|
||||
repo_location: Option<(
|
||||
&'a RepositoryEntry,
|
||||
Cursor<'a, StatusEntry, PathProgress<'a>>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl<'a> GitTraversal<'a> {
|
||||
fn synchronize_statuses(&mut self, reset: bool) {
|
||||
self.current_entry_summary = None;
|
||||
|
||||
let Some(entry) = self.traversal.cursor.item() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(repo) = self.traversal.snapshot.repository_for_path(&entry.path) else {
|
||||
self.repo_location = None;
|
||||
return;
|
||||
};
|
||||
|
||||
// Update our state if we changed repositories.
|
||||
if reset
|
||||
|| self
|
||||
.repo_location
|
||||
.as_ref()
|
||||
.map(|(prev_repo, _)| &prev_repo.work_directory)
|
||||
!= Some(&repo.work_directory)
|
||||
{
|
||||
self.repo_location = Some((repo, repo.statuses_by_path.cursor::<PathProgress>(&())));
|
||||
}
|
||||
|
||||
let Some((repo, statuses)) = &mut self.repo_location else {
|
||||
return;
|
||||
};
|
||||
|
||||
let repo_path = repo.relativize(&entry.path).unwrap();
|
||||
|
||||
if entry.is_dir() {
|
||||
let mut statuses = statuses.clone();
|
||||
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
|
||||
let summary =
|
||||
statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &());
|
||||
|
||||
self.current_entry_summary = Some(summary);
|
||||
} else if entry.is_file() {
|
||||
// For a file entry, park the cursor on the corresponding status
|
||||
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
||||
// TODO: Investigate statuses.item() being None here.
|
||||
self.current_entry_summary = statuses.item().map(|item| item.status.into());
|
||||
} else {
|
||||
self.current_entry_summary = Some(GitSummary::UNCHANGED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) -> bool {
|
||||
self.advance_by(1)
|
||||
}
|
||||
|
||||
pub fn advance_by(&mut self, count: usize) -> bool {
|
||||
let found = self.traversal.advance_by(count);
|
||||
self.synchronize_statuses(false);
|
||||
found
|
||||
}
|
||||
|
||||
pub fn advance_to_sibling(&mut self) -> bool {
|
||||
let found = self.traversal.advance_to_sibling();
|
||||
self.synchronize_statuses(false);
|
||||
found
|
||||
}
|
||||
|
||||
pub fn back_to_parent(&mut self) -> bool {
|
||||
let found = self.traversal.back_to_parent();
|
||||
self.synchronize_statuses(true);
|
||||
found
|
||||
}
|
||||
|
||||
pub fn start_offset(&self) -> usize {
|
||||
self.traversal.start_offset()
|
||||
}
|
||||
|
||||
pub fn end_offset(&self) -> usize {
|
||||
self.traversal.end_offset()
|
||||
}
|
||||
|
||||
pub fn entry(&self) -> Option<GitEntryRef<'a>> {
|
||||
let entry = self.traversal.cursor.item()?;
|
||||
let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED);
|
||||
Some(GitEntryRef { entry, git_summary })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GitTraversal<'a> {
|
||||
type Item = GitEntryRef<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(item) = self.entry() {
|
||||
self.advance();
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Traversal<'a> {
|
||||
snapshot: &'a Snapshot,
|
||||
|
@ -6336,16 +6148,6 @@ impl<'a> Traversal<'a> {
|
|||
traversal
|
||||
}
|
||||
|
||||
pub fn with_git_statuses(self) -> GitTraversal<'a> {
|
||||
let mut this = GitTraversal {
|
||||
traversal: self,
|
||||
current_entry_summary: None,
|
||||
repo_location: None,
|
||||
};
|
||||
this.synchronize_statuses(true);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) -> bool {
|
||||
self.advance_by(1)
|
||||
}
|
||||
|
@ -6391,6 +6193,10 @@ impl<'a> Traversal<'a> {
|
|||
self.cursor.item()
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> &'a Snapshot {
|
||||
self.snapshot
|
||||
}
|
||||
|
||||
pub fn start_offset(&self) -> usize {
|
||||
self.cursor
|
||||
.start()
|
||||
|
@ -6418,7 +6224,7 @@ impl<'a> Iterator for Traversal<'a> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum PathTarget<'a> {
|
||||
pub enum PathTarget<'a> {
|
||||
Path(&'a Path),
|
||||
Successor(&'a Path),
|
||||
}
|
||||
|
@ -6517,20 +6323,6 @@ pub struct ChildEntriesIter<'a> {
|
|||
traversal: Traversal<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ChildEntriesIter<'a> {
|
||||
pub fn with_git_statuses(self) -> ChildEntriesGitIter<'a> {
|
||||
ChildEntriesGitIter {
|
||||
parent_path: self.parent_path,
|
||||
traversal: self.traversal.with_git_statuses(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChildEntriesGitIter<'a> {
|
||||
parent_path: &'a Path,
|
||||
traversal: GitTraversal<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ChildEntriesIter<'a> {
|
||||
type Item = &'a Entry;
|
||||
|
||||
|
@ -6545,20 +6337,6 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ChildEntriesGitIter<'a> {
|
||||
type Item = GitEntryRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(item) = self.traversal.entry() {
|
||||
if item.path.starts_with(self.parent_path) {
|
||||
self.traversal.advance_to_sibling();
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Entry> for proto::Entry {
|
||||
fn from(entry: &'a Entry) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use crate::{
|
||||
worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot,
|
||||
WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, WorkDirectory,
|
||||
Worktree, WorktreeModelHandle,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::{
|
||||
repository::RepoPath,
|
||||
status::{
|
||||
FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
|
||||
UnmergedStatusCode,
|
||||
},
|
||||
status::{FileStatus, StatusCode, TrackedStatus},
|
||||
GITIGNORE,
|
||||
};
|
||||
use git2::RepositoryInitOptions;
|
||||
|
@ -27,7 +24,6 @@ use std::{
|
|||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{path, test::TempTree, ResultExt};
|
||||
|
||||
|
@ -1472,86 +1468,6 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// Create a worktree with a git directory.
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": "",
|
||||
"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(
|
||||
path!("/root").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
|
||||
(
|
||||
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
||||
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
|
||||
// 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(path!("/root")).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
|
||||
(
|
||||
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
||||
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
assert_eq!(new_entry_ids, old_entry_ids);
|
||||
assert_ne!(new_mtimes, old_mtimes);
|
||||
|
||||
// Regression test: changes to the git repository should still be
|
||||
// detected.
|
||||
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));
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), MODIFIED),
|
||||
(Path::new("a.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("b/c.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2196,11 +2112,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Updated,
|
||||
});
|
||||
|
||||
// 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.
|
||||
|
@ -2244,7 +2155,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
|||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories().iter().next().unwrap();
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory,
|
||||
WorkDirectory::in_project("projects/project1")
|
||||
|
@ -2268,7 +2179,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
|||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories().iter().next().unwrap();
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory,
|
||||
WorkDirectory::in_project("projects/project2")
|
||||
|
@ -2529,8 +2440,8 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
|||
// 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!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.work_directory,
|
||||
WorkDirectory::in_project("project")
|
||||
|
@ -2705,7 +2616,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
|||
// 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 repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
@ -2727,7 +2638,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
|||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repository = snapshot.repositories().iter().next().unwrap();
|
||||
let repository = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repository.status().collect::<Vec<_>>();
|
||||
|
||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||
|
@ -2760,7 +2671,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
|||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().iter().next().unwrap();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// Deleting an untracked entry, b.txt, should leave no status
|
||||
|
@ -2814,7 +2725,7 @@ async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
|
|||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().iter().next().unwrap();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// `sub` doesn't appear in our computed statuses.
|
||||
|
@ -2883,8 +2794,8 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
|||
// 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!(snapshot.repositories.iter().count(), 1);
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory.canonicalize(),
|
||||
WorkDirectory::AboveProject {
|
||||
|
@ -2913,442 +2824,13 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
|||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
|
||||
assert!(snapshot.repositories().iter().next().is_some());
|
||||
assert!(snapshot.repositories.iter().next().is_some());
|
||||
|
||||
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
||||
assert_eq!(snapshot.status_for_file("d/e.txt"), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar",
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z.txt": "sneaky..."
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
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(
|
||||
Path::new(path!("/root/x/y/.git")),
|
||||
&[(Path::new("y1.txt"), CONFLICT)],
|
||||
);
|
||||
fs.set_status_for_repo(
|
||||
Path::new(path!("/root/z/.git")),
|
||||
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
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();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
let mut traversal = snapshot
|
||||
.traverse_from_path(true, false, true, Path::new("x"))
|
||||
.with_git_statuses();
|
||||
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
||||
assert_eq!(entry.git_summary, MODIFIED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
"a": {
|
||||
"b": {
|
||||
"c1.txt": "",
|
||||
"c2.txt": "",
|
||||
},
|
||||
"d": {
|
||||
"e1.txt": "",
|
||||
"e2.txt": "",
|
||||
"e3.txt": "",
|
||||
}
|
||||
},
|
||||
"f": {
|
||||
"no-status.txt": ""
|
||||
},
|
||||
"g": {
|
||||
"h1.txt": "",
|
||||
"h2.txt": ""
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
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()),
|
||||
(Path::new("g/h2.txt"), CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
|
||||
(Path::new("g"), GitSummary::CONFLICT),
|
||||
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
|
||||
(Path::new("a"), ADDED + MODIFIED),
|
||||
(Path::new("a/b"), ADDED),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("a/d"), MODIFIED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f"), GitSummary::UNCHANGED),
|
||||
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("g"), GitSummary::CONFLICT),
|
||||
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b"), ADDED),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("a/d"), MODIFIED),
|
||||
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f"), GitSummary::UNCHANGED),
|
||||
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("g"), GitSummary::CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar"
|
||||
},
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo(
|
||||
Path::new(path!("/root/x/.git")),
|
||||
&[(Path::new("x1.txt"), StatusCode::Added.index())],
|
||||
);
|
||||
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(
|
||||
Path::new(path!("/root/z/.git")),
|
||||
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
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();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
|
||||
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
||||
(Path::new("y/y2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), MODIFIED),
|
||||
(Path::new("z/z2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), ADDED),
|
||||
(Path::new("x/x1.txt"), ADDED),
|
||||
(Path::new("x/x2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
|
||||
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
||||
(Path::new("y/y2.txt"), MODIFIED),
|
||||
(Path::new("z"), MODIFIED),
|
||||
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("z/z2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"x": {
|
||||
".git": {},
|
||||
"x1.txt": "foo",
|
||||
"x2.txt": "bar",
|
||||
"y": {
|
||||
".git": {},
|
||||
"y1.txt": "baz",
|
||||
"y2.txt": "qux"
|
||||
},
|
||||
"z.txt": "sneaky..."
|
||||
},
|
||||
"z": {
|
||||
".git": {},
|
||||
"z1.txt": "quux",
|
||||
"z2.txt": "quuux"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
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(
|
||||
Path::new(path!("/root/x/y/.git")),
|
||||
&[(Path::new("y1.txt"), CONFLICT)],
|
||||
);
|
||||
|
||||
fs.set_status_for_repo(
|
||||
Path::new(path!("/root/z/.git")),
|
||||
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
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();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
// Sanity check the propagation for x/y and z
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||
],
|
||||
);
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), ADDED),
|
||||
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("z/z2.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
|
||||
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
// Sanity check everything around it
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("x/x2.txt"), MODIFIED),
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("x/z.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
|
||||
// Test the other fundamental case, transitioning from git repository to non-git repository
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||
],
|
||||
);
|
||||
|
||||
// And all together now
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("x/x2.txt"), MODIFIED),
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("x/z.txt"), ADDED),
|
||||
(Path::new("z"), ADDED),
|
||||
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||
(Path::new("z/z2.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -3403,7 +2885,7 @@ async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
|||
);
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.git_entries().nth(0).expect("No git entry").clone();
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
|
@ -3420,7 +2902,7 @@ async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
|||
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.git_entries().nth(0).expect("No git entry").clone();
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
|
@ -3490,34 +2972,6 @@ fn test_unrelativize() {
|
|||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
|
||||
let mut traversal = snapshot
|
||||
.traverse_from_path(true, true, false, "".as_ref())
|
||||
.with_git_statuses();
|
||||
let found_statuses = expected_statuses
|
||||
.iter()
|
||||
.map(|&(path, _)| {
|
||||
let git_entry = traversal
|
||||
.find(|git_entry| &*git_entry.path == path)
|
||||
.unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
|
||||
(path, git_entry.git_summary)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(found_statuses, expected_statuses);
|
||||
}
|
||||
|
||||
const ADDED: GitSummary = GitSummary {
|
||||
index: TrackedSummary::ADDED,
|
||||
count: 1,
|
||||
..GitSummary::UNCHANGED
|
||||
};
|
||||
const MODIFIED: GitSummary = GitSummary {
|
||||
index: TrackedSummary::MODIFIED,
|
||||
count: 1,
|
||||
..GitSummary::UNCHANGED
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
let mut init_opts = RepositoryInitOptions::new();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue