Move git status out of Entry (#22224)

- [x] Rewrite worktree git handling
- [x] Fix tests
- [x] Fix `test_propagate_statuses_for_repos_under_project`
- [x] Replace `WorkDirectoryEntry` with `WorkDirectory` in
`RepositoryEntry`
- [x] Add a worktree event for capturing git status changes
- [x] Confirm that the local repositories are correctly updating the new
WorkDirectory field
- [x] Implement the git statuses query as a join when pulling entries
out of worktree
- [x] Use this new join to implement the project panel and outline
panel.
- [x] Synchronize git statuses over the wire for collab and remote dev
(use the existing `worktree_repository_statuses` table, adjust as
needed)
- [x] Only send changed statuses to collab

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.com>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Mikayla Maki 2025-01-03 17:00:16 -08:00 committed by GitHub
parent 72057e5716
commit 9613084f59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 2824 additions and 1254 deletions

View file

@ -63,7 +63,7 @@ use workspace::{
notifications::{DetachAndPromptErr, NotifyTaskExt},
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
};
use worktree::CreatedEntry;
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
@ -76,7 +76,7 @@ pub struct ProjectPanel {
// An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
// hovered over the start/end of a list.
hover_scroll_task: Option<Task<()>>,
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
/// Maps from leaf project entry ID to the currently selected ancestor.
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
/// project entries (and all non-leaf nodes are guaranteed to be directories).
@ -311,7 +311,8 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
project::Event::GitRepositoryUpdated
| project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
@ -1366,9 +1367,10 @@ impl ProjectPanel {
let parent_entry = worktree.entry_for_path(parent_path)?;
// Remove all siblings that are being deleted except the last marked entry
let mut siblings: Vec<Entry> = worktree
let mut siblings: Vec<_> = worktree
.snapshot()
.child_entries(parent_path)
.with_git_statuses()
.filter(|sibling| {
sibling.id == latest_entry.id
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
@ -1376,7 +1378,7 @@ impl ProjectPanel {
entry_id: sibling.id,
})
})
.cloned()
.map(|entry| entry.to_owned())
.collect();
project::sort_worktree_entries(&mut siblings);
@ -2334,7 +2336,7 @@ impl ProjectPanel {
}
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
let mut entry_iter = snapshot.entries(true, 0).with_git_statuses();
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
if auto_collapse_dirs && entry.kind.is_dir() {
@ -2376,7 +2378,7 @@ impl ProjectPanel {
}
}
auto_folded_ancestors.clear();
visible_worktree_entries.push(entry.clone());
visible_worktree_entries.push(entry.to_owned());
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
entry.id == new_entry_id || {
self.ancestors.get(&entry.id).map_or(false, |entries| {
@ -2390,25 +2392,27 @@ impl ProjectPanel {
false
};
if precedes_new_entry {
visible_worktree_entries.push(Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
visible_worktree_entries.push(GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
},
git_status: entry.git_status,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
});
}
let worktree_abs_path = worktree.read(cx).abs_path();
let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
let Some(path_name) = worktree_abs_path
.file_name()
.with_context(|| {
@ -2485,8 +2489,8 @@ impl ProjectPanel {
entry_iter.advance();
}
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
}
@ -2714,13 +2718,13 @@ impl ProjectPanel {
None
}
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
if visible_worktree_entries.len() > offset + index {
return visible_worktree_entries
.get(index)
.map(|entry| (*worktree_id, entry));
.map(|entry| (*worktree_id, entry.to_ref()));
}
offset += visible_worktree_entries.len();
}
@ -2753,7 +2757,7 @@ impl ProjectPanel {
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
callback(entry, entries, cx);
callback(&entry, entries, cx);
}
ix = end_ix;
}
@ -2822,7 +2826,7 @@ impl ProjectPanel {
};
let (depth, difference) =
ProjectPanel::calculate_depth_and_difference(entry, entries);
ProjectPanel::calculate_depth_and_difference(&entry, entries);
let filename = match difference {
diff if diff > 1 => entry
@ -2951,9 +2955,9 @@ impl ProjectPanel {
worktree_id: WorktreeId,
reverse_search: bool,
only_visible_entries: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<Entry> {
) -> Option<GitEntry> {
if only_visible_entries {
let entries = self
.visible_entries
@ -2968,15 +2972,18 @@ impl ProjectPanel {
.clone();
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
.find(|ele| predicate(ele, worktree_id))
.find(|ele| predicate(ele.to_ref(), worktree_id))
.cloned();
}
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
worktree.update(cx, |tree, _| {
utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search)
.find_single_ended(|ele| predicate(ele, worktree_id))
.cloned()
utils::ReversibleIterable::new(
tree.entries(true, 0usize).with_git_statuses(),
reverse_search,
)
.find_single_ended(|ele| predicate(*ele, worktree_id))
.map(|ele| ele.to_owned())
})
}
@ -2984,7 +2991,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@ -3006,7 +3013,9 @@ impl ProjectPanel {
let root_entry = tree.root_entry()?;
let tree_id = tree.id();
let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref());
let mut first_iter = tree
.traverse_from_path(true, true, true, entry.path.as_ref())
.with_git_statuses();
if reverse_search {
first_iter.next();
@ -3014,25 +3023,25 @@ impl ProjectPanel {
let first = first_iter
.enumerate()
.take_until(|(count, ele)| *ele == root_entry && *count != 0usize)
.map(|(_, ele)| ele)
.find(|ele| predicate(ele, tree_id))
.cloned();
.take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
.map(|(_, entry)| entry)
.find(|ele| predicate(*ele, tree_id))
.map(|ele| ele.to_owned());
let second_iter = tree.entries(true, 0usize);
let second_iter = tree.entries(true, 0usize).with_git_statuses();
let second = if reverse_search {
second_iter
.take_until(|ele| ele.id == start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
} else {
second_iter
.take_while(|ele| ele.id != start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
};
if reverse_search {
@ -3089,7 +3098,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@ -3131,8 +3140,8 @@ impl ProjectPanel {
)
};
let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id));
let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
if first_search.is_some() {
return first_search.map(|entry| SelectedEntry {