1mod ignore;
2mod worktree_settings;
3#[cfg(test)]
4mod worktree_tests;
5
6use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
7use anyhow::{anyhow, Context as _, Result};
8use clock::ReplicaId;
9use collections::{HashMap, HashSet, VecDeque};
10use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher};
11use futures::{
12 channel::{
13 mpsc::{self, UnboundedSender},
14 oneshot,
15 },
16 select_biased,
17 task::Poll,
18 FutureExt as _, Stream, StreamExt,
19};
20use fuzzy::CharBag;
21use git::{
22 repository::{GitRepository, RepoPath},
23 status::{
24 FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
25 },
26 GitHostingProviderRegistry, COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK,
27};
28use gpui::{
29 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
30};
31use ignore::IgnoreStack;
32use language::DiskState;
33
34use parking_lot::Mutex;
35use paths::local_settings_folder_relative_path;
36use postage::{
37 barrier,
38 prelude::{Sink as _, Stream as _},
39 watch,
40};
41use rpc::{
42 proto::{self, split_worktree_update, FromProto, ToProto},
43 AnyProtoClient,
44};
45pub use settings::WorktreeId;
46use settings::{Settings, SettingsLocation, SettingsStore};
47use smallvec::{smallvec, SmallVec};
48use smol::channel::{self, Sender};
49use std::{
50 any::Any,
51 cmp::Ordering,
52 collections::hash_map,
53 convert::TryFrom,
54 ffi::OsStr,
55 fmt,
56 future::Future,
57 mem::{self},
58 ops::{Deref, DerefMut},
59 path::{Path, PathBuf},
60 pin::Pin,
61 sync::{
62 atomic::{AtomicUsize, Ordering::SeqCst},
63 Arc,
64 },
65 time::{Duration, Instant},
66};
67use sum_tree::{
68 Bias, Cursor, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit,
69};
70use text::{LineEnding, Rope};
71use util::{
72 paths::{home_dir, PathMatcher, SanitizedPath},
73 ResultExt,
74};
75pub use worktree_settings::WorktreeSettings;
76
77#[cfg(feature = "test-support")]
78pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
79#[cfg(not(feature = "test-support"))]
80pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
81
82/// A set of local or remote files that are being opened as part of a project.
83/// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates.
84/// Stores git repositories data and the diagnostics for the file(s).
85///
86/// Has an absolute path, and may be set to be visible in Zed UI or not.
87/// May correspond to a directory or a single file.
88/// Possible examples:
89/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree
90/// * a directory opened in Zed — may be added as a visible entry to the current worktree
91///
92/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.
93pub enum Worktree {
94 Local(LocalWorktree),
95 Remote(RemoteWorktree),
96}
97
98/// An entry, created in the worktree.
99#[derive(Debug)]
100pub enum CreatedEntry {
101 /// Got created and indexed by the worktree, receiving a corresponding entry.
102 Included(Entry),
103 /// Got created, but not indexed due to falling under exclusion filters.
104 Excluded { abs_path: PathBuf },
105}
106
107pub struct LoadedFile {
108 pub file: Arc<File>,
109 pub text: String,
110}
111
112pub struct LoadedBinaryFile {
113 pub file: Arc<File>,
114 pub content: Vec<u8>,
115}
116
117pub struct LocalWorktree {
118 snapshot: LocalSnapshot,
119 scan_requests_tx: channel::Sender<ScanRequest>,
120 path_prefixes_to_scan_tx: channel::Sender<PathPrefixScanRequest>,
121 is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
122 _background_scanner_tasks: Vec<Task<()>>,
123 update_observer: Option<UpdateObservationState>,
124 fs: Arc<dyn Fs>,
125 fs_case_sensitive: bool,
126 visible: bool,
127 next_entry_id: Arc<AtomicUsize>,
128 settings: WorktreeSettings,
129 share_private_files: bool,
130}
131
132pub struct PathPrefixScanRequest {
133 path: Arc<Path>,
134 done: SmallVec<[barrier::Sender; 1]>,
135}
136
137struct ScanRequest {
138 relative_paths: Vec<Arc<Path>>,
139 done: SmallVec<[barrier::Sender; 1]>,
140}
141
142pub struct RemoteWorktree {
143 snapshot: Snapshot,
144 background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,
145 project_id: u64,
146 client: AnyProtoClient,
147 file_scan_inclusions: PathMatcher,
148 updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
149 update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,
150 snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
151 replica_id: ReplicaId,
152 visible: bool,
153 disconnected: bool,
154}
155
156#[derive(Clone)]
157pub struct Snapshot {
158 id: WorktreeId,
159 abs_path: SanitizedPath,
160 root_name: String,
161 root_char_bag: CharBag,
162 entries_by_path: SumTree<Entry>,
163 entries_by_id: SumTree<PathEntry>,
164 always_included_entries: Vec<Arc<Path>>,
165 repositories: SumTree<RepositoryEntry>,
166
167 /// A number that increases every time the worktree begins scanning
168 /// a set of paths from the filesystem. This scanning could be caused
169 /// by some operation performed on the worktree, such as reading or
170 /// writing a file, or by an event reported by the filesystem.
171 scan_id: usize,
172
173 /// The latest scan id that has completed, and whose preceding scans
174 /// have all completed. The current `scan_id` could be more than one
175 /// greater than the `completed_scan_id` if operations are performed
176 /// on the worktree while it is processing a file-system event.
177 completed_scan_id: usize,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct RepositoryEntry {
182 /// The git status entries for this repository.
183 /// Note that the paths on this repository are relative to the git work directory.
184 /// If the .git folder is external to Zed, these paths will be relative to that folder,
185 /// and this data structure might reference files external to this worktree.
186 ///
187 /// For example:
188 ///
189 /// my_root_folder/ <-- repository root
190 /// .git
191 /// my_sub_folder_1/
192 /// project_root/ <-- Project root, Zed opened here
193 /// changed_file_1 <-- File with changes, in worktree
194 /// my_sub_folder_2/
195 /// changed_file_2 <-- File with changes, out of worktree
196 /// ...
197 ///
198 /// With this setup, this field would contain 2 entries, like so:
199 /// - my_sub_folder_1/project_root/changed_file_1
200 /// - my_sub_folder_2/changed_file_2
201 pub(crate) statuses_by_path: SumTree<StatusEntry>,
202 work_directory_id: ProjectEntryId,
203 pub work_directory: WorkDirectory,
204 pub(crate) branch: Option<Arc<str>>,
205 pub current_merge_conflicts: TreeSet<RepoPath>,
206}
207
208impl Deref for RepositoryEntry {
209 type Target = WorkDirectory;
210
211 fn deref(&self) -> &Self::Target {
212 &self.work_directory
213 }
214}
215
216impl RepositoryEntry {
217 pub fn branch(&self) -> Option<Arc<str>> {
218 self.branch.clone()
219 }
220
221 pub fn work_directory_id(&self) -> ProjectEntryId {
222 self.work_directory_id
223 }
224
225 pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
226 self.statuses_by_path.iter().cloned()
227 }
228
229 pub fn status_len(&self) -> usize {
230 self.statuses_by_path.summary().item_summary.count
231 }
232
233 pub fn status_summary(&self) -> GitSummary {
234 self.statuses_by_path.summary().item_summary
235 }
236
237 pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
238 self.statuses_by_path
239 .get(&PathKey(path.0.clone()), &())
240 .cloned()
241 }
242
243 pub fn initial_update(&self) -> proto::RepositoryEntry {
244 proto::RepositoryEntry {
245 work_directory_id: self.work_directory_id.to_proto(),
246 branch: self.branch.as_ref().map(|branch| branch.to_string()),
247 updated_statuses: self
248 .statuses_by_path
249 .iter()
250 .map(|entry| entry.to_proto())
251 .collect(),
252 removed_statuses: Default::default(),
253 current_merge_conflicts: self
254 .current_merge_conflicts
255 .iter()
256 .map(|repo_path| repo_path.to_proto())
257 .collect(),
258 }
259 }
260
261 pub fn build_update(&self, old: &Self) -> proto::RepositoryEntry {
262 let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
263 let mut removed_statuses: Vec<String> = Vec::new();
264
265 let mut new_statuses = self.statuses_by_path.iter().peekable();
266 let mut old_statuses = old.statuses_by_path.iter().peekable();
267
268 let mut current_new_entry = new_statuses.next();
269 let mut current_old_entry = old_statuses.next();
270 loop {
271 match (current_new_entry, current_old_entry) {
272 (Some(new_entry), Some(old_entry)) => {
273 match new_entry.repo_path.cmp(&old_entry.repo_path) {
274 Ordering::Less => {
275 updated_statuses.push(new_entry.to_proto());
276 current_new_entry = new_statuses.next();
277 }
278 Ordering::Equal => {
279 if new_entry.status != old_entry.status {
280 updated_statuses.push(new_entry.to_proto());
281 }
282 current_old_entry = old_statuses.next();
283 current_new_entry = new_statuses.next();
284 }
285 Ordering::Greater => {
286 removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
287 current_old_entry = old_statuses.next();
288 }
289 }
290 }
291 (None, Some(old_entry)) => {
292 removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
293 current_old_entry = old_statuses.next();
294 }
295 (Some(new_entry), None) => {
296 updated_statuses.push(new_entry.to_proto());
297 current_new_entry = new_statuses.next();
298 }
299 (None, None) => break,
300 }
301 }
302
303 proto::RepositoryEntry {
304 work_directory_id: self.work_directory_id.to_proto(),
305 branch: self.branch.as_ref().map(|branch| branch.to_string()),
306 updated_statuses,
307 removed_statuses,
308 current_merge_conflicts: self
309 .current_merge_conflicts
310 .iter()
311 .map(|path| path.as_ref().to_proto())
312 .collect(),
313 }
314 }
315}
316
317/// This path corresponds to the 'content path' of a repository in relation
318/// to Zed's project root.
319/// In the majority of the cases, this is the folder that contains the .git folder.
320/// But if a sub-folder of a git repository is opened, this corresponds to the
321/// project root and the .git folder is located in a parent directory.
322#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
323pub enum WorkDirectory {
324 InProject {
325 relative_path: Arc<Path>,
326 },
327 AboveProject {
328 absolute_path: Arc<Path>,
329 location_in_repo: Arc<Path>,
330 },
331}
332
333impl WorkDirectory {
334 #[cfg(test)]
335 fn in_project(path: &str) -> Self {
336 let path = Path::new(path);
337 Self::InProject {
338 relative_path: path.into(),
339 }
340 }
341
342 #[cfg(test)]
343 fn canonicalize(&self) -> Self {
344 match self {
345 WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {
346 relative_path: relative_path.clone(),
347 },
348 WorkDirectory::AboveProject {
349 absolute_path,
350 location_in_repo,
351 } => WorkDirectory::AboveProject {
352 absolute_path: absolute_path.canonicalize().unwrap().into(),
353 location_in_repo: location_in_repo.clone(),
354 },
355 }
356 }
357
358 pub fn is_above_project(&self) -> bool {
359 match self {
360 WorkDirectory::InProject { .. } => false,
361 WorkDirectory::AboveProject { .. } => true,
362 }
363 }
364
365 fn path_key(&self) -> PathKey {
366 match self {
367 WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
368 WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),
369 }
370 }
371
372 /// Returns true if the given path is a child of the work directory.
373 ///
374 /// Note that the path may not be a member of this repository, if there
375 /// is a repository in a directory between these two paths
376 /// external .git folder in a parent folder of the project root.
377 #[track_caller]
378 pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
379 let path = path.as_ref();
380 debug_assert!(path.is_relative());
381 match self {
382 WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
383 WorkDirectory::AboveProject { .. } => true,
384 }
385 }
386
387 /// relativize returns the given project path relative to the root folder of the
388 /// repository.
389 /// If the root of the repository (and its .git folder) are located in a parent folder
390 /// of the project root folder, then the returned RepoPath is relative to the root
391 /// of the repository and not a valid path inside the project.
392 pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
393 // path is assumed to be relative to worktree root.
394 debug_assert!(path.is_relative());
395 match self {
396 WorkDirectory::InProject { relative_path } => Ok(path
397 .strip_prefix(relative_path)
398 .map_err(|_| {
399 anyhow!(
400 "could not relativize {:?} against {:?}",
401 path,
402 relative_path
403 )
404 })?
405 .into()),
406 WorkDirectory::AboveProject {
407 location_in_repo, ..
408 } => {
409 // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
410 if path == Path::new("") {
411 Ok(RepoPath(location_in_repo.clone()))
412 } else {
413 Ok(location_in_repo.join(path).into())
414 }
415 }
416 }
417 }
418
419 /// This is the opposite operation to `relativize` above
420 pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
421 match self {
422 WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
423 WorkDirectory::AboveProject {
424 location_in_repo, ..
425 } => {
426 // If we fail to strip the prefix, that means this status entry is
427 // external to this worktree, and we definitely won't have an entry_id
428 path.strip_prefix(location_in_repo).ok().map(Into::into)
429 }
430 }
431 }
432
433 pub fn display_name(&self) -> String {
434 match self {
435 WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
436 WorkDirectory::AboveProject {
437 absolute_path,
438 location_in_repo,
439 } => {
440 let num_of_dots = location_in_repo.components().count();
441
442 "../".repeat(num_of_dots)
443 + &absolute_path
444 .file_name()
445 .map(|s| s.to_string_lossy())
446 .unwrap_or_default()
447 + "/"
448 }
449 }
450 }
451}
452
453impl Default for WorkDirectory {
454 fn default() -> Self {
455 Self::InProject {
456 relative_path: Arc::from(Path::new("")),
457 }
458 }
459}
460
461#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
462pub struct WorkDirectoryEntry(ProjectEntryId);
463
464impl Deref for WorkDirectoryEntry {
465 type Target = ProjectEntryId;
466
467 fn deref(&self) -> &Self::Target {
468 &self.0
469 }
470}
471
472impl From<ProjectEntryId> for WorkDirectoryEntry {
473 fn from(value: ProjectEntryId) -> Self {
474 WorkDirectoryEntry(value)
475 }
476}
477
478#[derive(Debug, Clone)]
479pub struct LocalSnapshot {
480 snapshot: Snapshot,
481 /// All of the gitignore files in the worktree, indexed by their relative path.
482 /// The boolean indicates whether the gitignore needs to be updated.
483 ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
484 /// All of the git repositories in the worktree, indexed by the project entry
485 /// id of their parent directory.
486 git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
487 /// The file handle of the root dir
488 /// (so we can find it after it's been moved)
489 root_file_handle: Option<Arc<dyn fs::FileHandle>>,
490}
491
492struct BackgroundScannerState {
493 snapshot: LocalSnapshot,
494 scanned_dirs: HashSet<ProjectEntryId>,
495 path_prefixes_to_scan: HashSet<Arc<Path>>,
496 paths_to_scan: HashSet<Arc<Path>>,
497 /// The ids of all of the entries that were removed from the snapshot
498 /// as part of the current update. These entry ids may be re-used
499 /// if the same inode is discovered at a new path, or if the given
500 /// path is re-created after being deleted.
501 removed_entries: HashMap<u64, Entry>,
502 changed_paths: Vec<Arc<Path>>,
503 prev_snapshot: Snapshot,
504 git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>,
505}
506
507#[derive(Debug, Clone)]
508pub struct LocalRepositoryEntry {
509 pub(crate) work_directory_id: ProjectEntryId,
510 pub(crate) work_directory: WorkDirectory,
511 pub(crate) git_dir_scan_id: usize,
512 pub(crate) status_scan_id: usize,
513 pub(crate) repo_ptr: Arc<dyn GitRepository>,
514 /// Absolute path to the actual .git folder.
515 /// Note: if .git is a file, this points to the folder indicated by the .git file
516 pub(crate) dot_git_dir_abs_path: Arc<Path>,
517 /// Absolute path to the .git file, if we're in a git worktree.
518 pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>,
519 pub current_merge_head_shas: Vec<String>,
520}
521
522impl sum_tree::Item for LocalRepositoryEntry {
523 type Summary = PathSummary<Unit>;
524
525 fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
526 PathSummary {
527 max_path: self.work_directory.path_key().0,
528 item_summary: Unit,
529 }
530 }
531}
532
533impl KeyedItem for LocalRepositoryEntry {
534 type Key = PathKey;
535
536 fn key(&self) -> Self::Key {
537 self.work_directory.path_key()
538 }
539}
540
541impl LocalRepositoryEntry {
542 pub fn repo(&self) -> &Arc<dyn GitRepository> {
543 &self.repo_ptr
544 }
545}
546
547impl Deref for LocalRepositoryEntry {
548 type Target = WorkDirectory;
549
550 fn deref(&self) -> &Self::Target {
551 &self.work_directory
552 }
553}
554
555impl Deref for LocalSnapshot {
556 type Target = Snapshot;
557
558 fn deref(&self) -> &Self::Target {
559 &self.snapshot
560 }
561}
562
563impl DerefMut for LocalSnapshot {
564 fn deref_mut(&mut self) -> &mut Self::Target {
565 &mut self.snapshot
566 }
567}
568
569enum ScanState {
570 Started,
571 Updated {
572 snapshot: LocalSnapshot,
573 changes: UpdatedEntriesSet,
574 barrier: SmallVec<[barrier::Sender; 1]>,
575 scanning: bool,
576 },
577 RootUpdated {
578 new_path: Option<SanitizedPath>,
579 },
580}
581
582struct UpdateObservationState {
583 snapshots_tx:
584 mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>,
585 resume_updates: watch::Sender<()>,
586 _maintain_remote_snapshot: Task<Option<()>>,
587}
588
589#[derive(Clone)]
590pub enum Event {
591 UpdatedEntries(UpdatedEntriesSet),
592 UpdatedGitRepositories(UpdatedGitRepositoriesSet),
593 DeletedEntry(ProjectEntryId),
594}
595
596const EMPTY_PATH: &str = "";
597
598impl EventEmitter<Event> for Worktree {}
599
600impl Worktree {
601 pub async fn local(
602 path: impl Into<Arc<Path>>,
603 visible: bool,
604 fs: Arc<dyn Fs>,
605 next_entry_id: Arc<AtomicUsize>,
606 cx: &mut AsyncApp,
607 ) -> Result<Entity<Self>> {
608 let abs_path = path.into();
609 let metadata = fs
610 .metadata(&abs_path)
611 .await
612 .context("failed to stat worktree path")?;
613
614 let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| {
615 log::error!(
616 "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}"
617 );
618 true
619 });
620
621 let root_file_handle = fs.open_handle(&abs_path).await.log_err();
622
623 cx.new(move |cx: &mut Context<Worktree>| {
624 let mut snapshot = LocalSnapshot {
625 ignores_by_parent_abs_path: Default::default(),
626 git_repositories: Default::default(),
627 snapshot: Snapshot::new(
628 cx.entity_id().as_u64(),
629 abs_path
630 .file_name()
631 .map_or(String::new(), |f| f.to_string_lossy().to_string()),
632 abs_path.clone(),
633 ),
634 root_file_handle,
635 };
636
637 let worktree_id = snapshot.id();
638 let settings_location = Some(SettingsLocation {
639 worktree_id,
640 path: Path::new(EMPTY_PATH),
641 });
642
643 let settings = WorktreeSettings::get(settings_location, cx).clone();
644 cx.observe_global::<SettingsStore>(move |this, cx| {
645 if let Self::Local(this) = this {
646 let settings = WorktreeSettings::get(settings_location, cx).clone();
647 if this.settings != settings {
648 this.settings = settings;
649 this.restart_background_scanners(cx);
650 }
651 }
652 })
653 .detach();
654
655 let share_private_files = false;
656 if let Some(metadata) = metadata {
657 let mut entry = Entry::new(
658 Arc::from(Path::new("")),
659 &metadata,
660 &next_entry_id,
661 snapshot.root_char_bag,
662 None,
663 );
664 if !metadata.is_dir {
665 entry.is_private = !share_private_files
666 && settings.is_path_private(abs_path.file_name().unwrap().as_ref());
667 }
668 snapshot.insert_entry(entry, fs.as_ref());
669 }
670
671 let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
672 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
673 let mut worktree = LocalWorktree {
674 share_private_files,
675 next_entry_id,
676 snapshot,
677 is_scanning: watch::channel_with(true),
678 update_observer: None,
679 scan_requests_tx,
680 path_prefixes_to_scan_tx,
681 _background_scanner_tasks: Vec::new(),
682 fs,
683 fs_case_sensitive,
684 visible,
685 settings,
686 };
687 worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
688 Worktree::Local(worktree)
689 })
690 }
691
692 pub fn remote(
693 project_id: u64,
694 replica_id: ReplicaId,
695 worktree: proto::WorktreeMetadata,
696 client: AnyProtoClient,
697 cx: &mut App,
698 ) -> Entity<Self> {
699 cx.new(|cx: &mut Context<Self>| {
700 let snapshot = Snapshot::new(
701 worktree.id,
702 worktree.root_name,
703 Arc::<Path>::from_proto(worktree.abs_path),
704 );
705
706 let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new())));
707 let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded();
708 let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
709
710 let worktree_id = snapshot.id();
711 let settings_location = Some(SettingsLocation {
712 worktree_id,
713 path: Path::new(EMPTY_PATH),
714 });
715
716 let settings = WorktreeSettings::get(settings_location, cx).clone();
717 let worktree = RemoteWorktree {
718 client,
719 project_id,
720 replica_id,
721 snapshot,
722 file_scan_inclusions: settings.file_scan_inclusions.clone(),
723 background_snapshot: background_snapshot.clone(),
724 updates_tx: Some(background_updates_tx),
725 update_observer: None,
726 snapshot_subscriptions: Default::default(),
727 visible: worktree.visible,
728 disconnected: false,
729 };
730
731 // Apply updates to a separate snapshot in a background task, then
732 // send them to a foreground task which updates the model.
733 cx.background_executor()
734 .spawn(async move {
735 while let Some(update) = background_updates_rx.next().await {
736 {
737 let mut lock = background_snapshot.lock();
738 if let Err(error) = lock
739 .0
740 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
741 {
742 log::error!("error applying worktree update: {}", error);
743 }
744 lock.1.push(update);
745 }
746 snapshot_updated_tx.send(()).await.ok();
747 }
748 })
749 .detach();
750
751 // On the foreground task, update to the latest snapshot and notify
752 // any update observer of all updates that led to that snapshot.
753 cx.spawn(|this, mut cx| async move {
754 while (snapshot_updated_rx.recv().await).is_some() {
755 this.update(&mut cx, |this, cx| {
756 let this = this.as_remote_mut().unwrap();
757 {
758 let mut lock = this.background_snapshot.lock();
759 this.snapshot = lock.0.clone();
760 if let Some(tx) = &this.update_observer {
761 for update in lock.1.drain(..) {
762 tx.unbounded_send(update).ok();
763 }
764 }
765 };
766 cx.emit(Event::UpdatedEntries(Arc::default()));
767 cx.notify();
768 while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
769 if this.observed_snapshot(*scan_id) {
770 let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
771 let _ = tx.send(());
772 } else {
773 break;
774 }
775 }
776 })?;
777 }
778 anyhow::Ok(())
779 })
780 .detach();
781
782 Worktree::Remote(worktree)
783 })
784 }
785
786 pub fn as_local(&self) -> Option<&LocalWorktree> {
787 if let Worktree::Local(worktree) = self {
788 Some(worktree)
789 } else {
790 None
791 }
792 }
793
794 pub fn as_remote(&self) -> Option<&RemoteWorktree> {
795 if let Worktree::Remote(worktree) = self {
796 Some(worktree)
797 } else {
798 None
799 }
800 }
801
802 pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> {
803 if let Worktree::Local(worktree) = self {
804 Some(worktree)
805 } else {
806 None
807 }
808 }
809
810 pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> {
811 if let Worktree::Remote(worktree) = self {
812 Some(worktree)
813 } else {
814 None
815 }
816 }
817
818 pub fn is_local(&self) -> bool {
819 matches!(self, Worktree::Local(_))
820 }
821
822 pub fn is_remote(&self) -> bool {
823 !self.is_local()
824 }
825
826 pub fn settings_location(&self, _: &Context<Self>) -> SettingsLocation<'static> {
827 SettingsLocation {
828 worktree_id: self.id(),
829 path: Path::new(EMPTY_PATH),
830 }
831 }
832
833 pub fn snapshot(&self) -> Snapshot {
834 match self {
835 Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(),
836 Worktree::Remote(worktree) => worktree.snapshot.clone(),
837 }
838 }
839
840 pub fn scan_id(&self) -> usize {
841 match self {
842 Worktree::Local(worktree) => worktree.snapshot.scan_id,
843 Worktree::Remote(worktree) => worktree.snapshot.scan_id,
844 }
845 }
846
847 pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
848 proto::WorktreeMetadata {
849 id: self.id().to_proto(),
850 root_name: self.root_name().to_string(),
851 visible: self.is_visible(),
852 abs_path: self.abs_path().to_proto(),
853 }
854 }
855
856 pub fn completed_scan_id(&self) -> usize {
857 match self {
858 Worktree::Local(worktree) => worktree.snapshot.completed_scan_id,
859 Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id,
860 }
861 }
862
863 pub fn is_visible(&self) -> bool {
864 match self {
865 Worktree::Local(worktree) => worktree.visible,
866 Worktree::Remote(worktree) => worktree.visible,
867 }
868 }
869
870 pub fn replica_id(&self) -> ReplicaId {
871 match self {
872 Worktree::Local(_) => 0,
873 Worktree::Remote(worktree) => worktree.replica_id,
874 }
875 }
876
877 pub fn abs_path(&self) -> Arc<Path> {
878 match self {
879 Worktree::Local(worktree) => worktree.abs_path.clone().into(),
880 Worktree::Remote(worktree) => worktree.abs_path.clone().into(),
881 }
882 }
883
884 pub fn root_file(&self, cx: &Context<Self>) -> Option<Arc<File>> {
885 let entry = self.root_entry()?;
886 Some(File::for_entry(entry.clone(), cx.entity()))
887 }
888
889 pub fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
890 where
891 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
892 Fut: 'static + Send + Future<Output = bool>,
893 {
894 match self {
895 Worktree::Local(this) => this.observe_updates(project_id, cx, callback),
896 Worktree::Remote(this) => this.observe_updates(project_id, cx, callback),
897 }
898 }
899
900 pub fn stop_observing_updates(&mut self) {
901 match self {
902 Worktree::Local(this) => {
903 this.update_observer.take();
904 }
905 Worktree::Remote(this) => {
906 this.update_observer.take();
907 }
908 }
909 }
910
911 #[cfg(any(test, feature = "test-support"))]
912 pub fn has_update_observer(&self) -> bool {
913 match self {
914 Worktree::Local(this) => this.update_observer.is_some(),
915 Worktree::Remote(this) => this.update_observer.is_some(),
916 }
917 }
918
919 pub fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
920 match self {
921 Worktree::Local(this) => this.load_file(path, cx),
922 Worktree::Remote(_) => {
923 Task::ready(Err(anyhow!("remote worktrees can't yet load files")))
924 }
925 }
926 }
927
928 pub fn load_staged_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
929 match self {
930 Worktree::Local(this) => {
931 let path = Arc::from(path);
932 let snapshot = this.snapshot();
933 cx.background_executor().spawn(async move {
934 if let Some(repo) = snapshot.repository_for_path(&path) {
935 if let Some(repo_path) = repo.relativize(&path).log_err() {
936 if let Some(git_repo) =
937 snapshot.git_repositories.get(&repo.work_directory_id)
938 {
939 return Ok(git_repo.repo_ptr.load_index_text(&repo_path));
940 }
941 }
942 }
943 Err(anyhow!("No repository found for {path:?}"))
944 })
945 }
946 Worktree::Remote(_) => {
947 Task::ready(Err(anyhow!("remote worktrees can't yet load staged files")))
948 }
949 }
950 }
951
952 pub fn load_committed_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
953 match self {
954 Worktree::Local(this) => {
955 let path = Arc::from(path);
956 let snapshot = this.snapshot();
957 cx.background_executor().spawn(async move {
958 if let Some(repo) = snapshot.repository_for_path(&path) {
959 if let Some(repo_path) = repo.relativize(&path).log_err() {
960 if let Some(git_repo) =
961 snapshot.git_repositories.get(&repo.work_directory_id)
962 {
963 return Ok(git_repo.repo_ptr.load_committed_text(&repo_path));
964 }
965 }
966 }
967 Err(anyhow!("No repository found for {path:?}"))
968 })
969 }
970 Worktree::Remote(_) => Task::ready(Err(anyhow!(
971 "remote worktrees can't yet load committed files"
972 ))),
973 }
974 }
975
976 pub fn load_binary_file(
977 &self,
978 path: &Path,
979 cx: &Context<Worktree>,
980 ) -> Task<Result<LoadedBinaryFile>> {
981 match self {
982 Worktree::Local(this) => this.load_binary_file(path, cx),
983 Worktree::Remote(_) => {
984 Task::ready(Err(anyhow!("remote worktrees can't yet load binary files")))
985 }
986 }
987 }
988
989 pub fn write_file(
990 &self,
991 path: &Path,
992 text: Rope,
993 line_ending: LineEnding,
994 cx: &Context<Worktree>,
995 ) -> Task<Result<Arc<File>>> {
996 match self {
997 Worktree::Local(this) => this.write_file(path, text, line_ending, cx),
998 Worktree::Remote(_) => {
999 Task::ready(Err(anyhow!("remote worktree can't yet write files")))
1000 }
1001 }
1002 }
1003
1004 pub fn create_entry(
1005 &mut self,
1006 path: impl Into<Arc<Path>>,
1007 is_directory: bool,
1008 cx: &Context<Worktree>,
1009 ) -> Task<Result<CreatedEntry>> {
1010 let path: Arc<Path> = path.into();
1011 let worktree_id = self.id();
1012 match self {
1013 Worktree::Local(this) => this.create_entry(path, is_directory, cx),
1014 Worktree::Remote(this) => {
1015 let project_id = this.project_id;
1016 let request = this.client.request(proto::CreateProjectEntry {
1017 worktree_id: worktree_id.to_proto(),
1018 project_id,
1019 path: path.as_ref().to_proto(),
1020 is_directory,
1021 });
1022 cx.spawn(move |this, mut cx| async move {
1023 let response = request.await?;
1024 match response.entry {
1025 Some(entry) => this
1026 .update(&mut cx, |worktree, cx| {
1027 worktree.as_remote_mut().unwrap().insert_entry(
1028 entry,
1029 response.worktree_scan_id as usize,
1030 cx,
1031 )
1032 })?
1033 .await
1034 .map(CreatedEntry::Included),
1035 None => {
1036 let abs_path = this.update(&mut cx, |worktree, _| {
1037 worktree
1038 .absolutize(&path)
1039 .with_context(|| format!("absolutizing {path:?}"))
1040 })??;
1041 Ok(CreatedEntry::Excluded { abs_path })
1042 }
1043 }
1044 })
1045 }
1046 }
1047 }
1048
1049 pub fn delete_entry(
1050 &mut self,
1051 entry_id: ProjectEntryId,
1052 trash: bool,
1053 cx: &mut Context<Worktree>,
1054 ) -> Option<Task<Result<()>>> {
1055 let task = match self {
1056 Worktree::Local(this) => this.delete_entry(entry_id, trash, cx),
1057 Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx),
1058 }?;
1059
1060 let entry = match self {
1061 Worktree::Local(ref this) => this.entry_for_id(entry_id),
1062 Worktree::Remote(ref this) => this.entry_for_id(entry_id),
1063 }?;
1064
1065 let mut ids = vec![entry_id];
1066 let path = &*entry.path;
1067
1068 self.get_children_ids_recursive(path, &mut ids);
1069
1070 for id in ids {
1071 cx.emit(Event::DeletedEntry(id));
1072 }
1073 Some(task)
1074 }
1075
1076 fn get_children_ids_recursive(&self, path: &Path, ids: &mut Vec<ProjectEntryId>) {
1077 let children_iter = self.child_entries(path);
1078 for child in children_iter {
1079 ids.push(child.id);
1080 self.get_children_ids_recursive(&child.path, ids);
1081 }
1082 }
1083
1084 pub fn rename_entry(
1085 &mut self,
1086 entry_id: ProjectEntryId,
1087 new_path: impl Into<Arc<Path>>,
1088 cx: &Context<Self>,
1089 ) -> Task<Result<CreatedEntry>> {
1090 let new_path = new_path.into();
1091 match self {
1092 Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
1093 Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
1094 }
1095 }
1096
1097 pub fn copy_entry(
1098 &mut self,
1099 entry_id: ProjectEntryId,
1100 relative_worktree_source_path: Option<PathBuf>,
1101 new_path: impl Into<Arc<Path>>,
1102 cx: &Context<Self>,
1103 ) -> Task<Result<Option<Entry>>> {
1104 let new_path: Arc<Path> = new_path.into();
1105 match self {
1106 Worktree::Local(this) => {
1107 this.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
1108 }
1109 Worktree::Remote(this) => {
1110 let relative_worktree_source_path = relative_worktree_source_path
1111 .map(|relative_worktree_source_path| relative_worktree_source_path.to_proto());
1112 let response = this.client.request(proto::CopyProjectEntry {
1113 project_id: this.project_id,
1114 entry_id: entry_id.to_proto(),
1115 relative_worktree_source_path,
1116 new_path: new_path.to_proto(),
1117 });
1118 cx.spawn(move |this, mut cx| async move {
1119 let response = response.await?;
1120 match response.entry {
1121 Some(entry) => this
1122 .update(&mut cx, |worktree, cx| {
1123 worktree.as_remote_mut().unwrap().insert_entry(
1124 entry,
1125 response.worktree_scan_id as usize,
1126 cx,
1127 )
1128 })?
1129 .await
1130 .map(Some),
1131 None => Ok(None),
1132 }
1133 })
1134 }
1135 }
1136 }
1137
1138 pub fn copy_external_entries(
1139 &mut self,
1140 target_directory: PathBuf,
1141 paths: Vec<Arc<Path>>,
1142 overwrite_existing_files: bool,
1143 cx: &Context<Worktree>,
1144 ) -> Task<Result<Vec<ProjectEntryId>>> {
1145 match self {
1146 Worktree::Local(this) => {
1147 this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx)
1148 }
1149 _ => Task::ready(Err(anyhow!(
1150 "Copying external entries is not supported for remote worktrees"
1151 ))),
1152 }
1153 }
1154
1155 pub fn expand_entry(
1156 &mut self,
1157 entry_id: ProjectEntryId,
1158 cx: &Context<Worktree>,
1159 ) -> Option<Task<Result<()>>> {
1160 match self {
1161 Worktree::Local(this) => this.expand_entry(entry_id, cx),
1162 Worktree::Remote(this) => {
1163 let response = this.client.request(proto::ExpandProjectEntry {
1164 project_id: this.project_id,
1165 entry_id: entry_id.to_proto(),
1166 });
1167 Some(cx.spawn(move |this, mut cx| async move {
1168 let response = response.await?;
1169 this.update(&mut cx, |this, _| {
1170 this.as_remote_mut()
1171 .unwrap()
1172 .wait_for_snapshot(response.worktree_scan_id as usize)
1173 })?
1174 .await?;
1175 Ok(())
1176 }))
1177 }
1178 }
1179 }
1180
1181 pub fn expand_all_for_entry(
1182 &mut self,
1183 entry_id: ProjectEntryId,
1184 cx: &Context<Worktree>,
1185 ) -> Option<Task<Result<()>>> {
1186 match self {
1187 Worktree::Local(this) => this.expand_all_for_entry(entry_id, cx),
1188 Worktree::Remote(this) => {
1189 let response = this.client.request(proto::ExpandAllForProjectEntry {
1190 project_id: this.project_id,
1191 entry_id: entry_id.to_proto(),
1192 });
1193 Some(cx.spawn(move |this, mut cx| async move {
1194 let response = response.await?;
1195 this.update(&mut cx, |this, _| {
1196 this.as_remote_mut()
1197 .unwrap()
1198 .wait_for_snapshot(response.worktree_scan_id as usize)
1199 })?
1200 .await?;
1201 Ok(())
1202 }))
1203 }
1204 }
1205 }
1206
1207 pub async fn handle_create_entry(
1208 this: Entity<Self>,
1209 request: proto::CreateProjectEntry,
1210 mut cx: AsyncApp,
1211 ) -> Result<proto::ProjectEntryResponse> {
1212 let (scan_id, entry) = this.update(&mut cx, |this, cx| {
1213 (
1214 this.scan_id(),
1215 this.create_entry(
1216 Arc::<Path>::from_proto(request.path),
1217 request.is_directory,
1218 cx,
1219 ),
1220 )
1221 })?;
1222 Ok(proto::ProjectEntryResponse {
1223 entry: match &entry.await? {
1224 CreatedEntry::Included(entry) => Some(entry.into()),
1225 CreatedEntry::Excluded { .. } => None,
1226 },
1227 worktree_scan_id: scan_id as u64,
1228 })
1229 }
1230
1231 pub async fn handle_delete_entry(
1232 this: Entity<Self>,
1233 request: proto::DeleteProjectEntry,
1234 mut cx: AsyncApp,
1235 ) -> Result<proto::ProjectEntryResponse> {
1236 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1237 (
1238 this.scan_id(),
1239 this.delete_entry(
1240 ProjectEntryId::from_proto(request.entry_id),
1241 request.use_trash,
1242 cx,
1243 ),
1244 )
1245 })?;
1246 task.ok_or_else(|| anyhow!("invalid entry"))?.await?;
1247 Ok(proto::ProjectEntryResponse {
1248 entry: None,
1249 worktree_scan_id: scan_id as u64,
1250 })
1251 }
1252
1253 pub async fn handle_expand_entry(
1254 this: Entity<Self>,
1255 request: proto::ExpandProjectEntry,
1256 mut cx: AsyncApp,
1257 ) -> Result<proto::ExpandProjectEntryResponse> {
1258 let task = this.update(&mut cx, |this, cx| {
1259 this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1260 })?;
1261 task.ok_or_else(|| anyhow!("no such entry"))?.await?;
1262 let scan_id = this.read_with(&cx, |this, _| this.scan_id())?;
1263 Ok(proto::ExpandProjectEntryResponse {
1264 worktree_scan_id: scan_id as u64,
1265 })
1266 }
1267
1268 pub async fn handle_expand_all_for_entry(
1269 this: Entity<Self>,
1270 request: proto::ExpandAllForProjectEntry,
1271 mut cx: AsyncApp,
1272 ) -> Result<proto::ExpandAllForProjectEntryResponse> {
1273 let task = this.update(&mut cx, |this, cx| {
1274 this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1275 })?;
1276 task.ok_or_else(|| anyhow!("no such entry"))?.await?;
1277 let scan_id = this.read_with(&cx, |this, _| this.scan_id())?;
1278 Ok(proto::ExpandAllForProjectEntryResponse {
1279 worktree_scan_id: scan_id as u64,
1280 })
1281 }
1282
1283 pub async fn handle_rename_entry(
1284 this: Entity<Self>,
1285 request: proto::RenameProjectEntry,
1286 mut cx: AsyncApp,
1287 ) -> Result<proto::ProjectEntryResponse> {
1288 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1289 (
1290 this.scan_id(),
1291 this.rename_entry(
1292 ProjectEntryId::from_proto(request.entry_id),
1293 Arc::<Path>::from_proto(request.new_path),
1294 cx,
1295 ),
1296 )
1297 })?;
1298 Ok(proto::ProjectEntryResponse {
1299 entry: match &task.await? {
1300 CreatedEntry::Included(entry) => Some(entry.into()),
1301 CreatedEntry::Excluded { .. } => None,
1302 },
1303 worktree_scan_id: scan_id as u64,
1304 })
1305 }
1306
1307 pub async fn handle_copy_entry(
1308 this: Entity<Self>,
1309 request: proto::CopyProjectEntry,
1310 mut cx: AsyncApp,
1311 ) -> Result<proto::ProjectEntryResponse> {
1312 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1313 let relative_worktree_source_path = request
1314 .relative_worktree_source_path
1315 .map(PathBuf::from_proto);
1316 (
1317 this.scan_id(),
1318 this.copy_entry(
1319 ProjectEntryId::from_proto(request.entry_id),
1320 relative_worktree_source_path,
1321 PathBuf::from_proto(request.new_path),
1322 cx,
1323 ),
1324 )
1325 })?;
1326 Ok(proto::ProjectEntryResponse {
1327 entry: task.await?.as_ref().map(|e| e.into()),
1328 worktree_scan_id: scan_id as u64,
1329 })
1330 }
1331}
1332
1333impl LocalWorktree {
1334 pub fn fs(&self) -> &Arc<dyn Fs> {
1335 &self.fs
1336 }
1337
1338 pub fn contains_abs_path(&self, path: &Path) -> bool {
1339 let path = SanitizedPath::from(path);
1340 path.starts_with(&self.abs_path)
1341 }
1342
1343 pub fn is_path_private(&self, path: &Path) -> bool {
1344 !self.share_private_files && self.settings.is_path_private(path)
1345 }
1346
1347 fn restart_background_scanners(&mut self, cx: &Context<Worktree>) {
1348 let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
1349 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
1350 self.scan_requests_tx = scan_requests_tx;
1351 self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
1352
1353 self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
1354 let always_included_entries = mem::take(&mut self.snapshot.always_included_entries);
1355 log::debug!(
1356 "refreshing entries for the following always included paths: {:?}",
1357 always_included_entries
1358 );
1359
1360 // Cleans up old always included entries to ensure they get updated properly. Otherwise,
1361 // nested always included entries may not get updated and will result in out-of-date info.
1362 self.refresh_entries_for_paths(always_included_entries);
1363 }
1364
1365 fn start_background_scanner(
1366 &mut self,
1367 scan_requests_rx: channel::Receiver<ScanRequest>,
1368 path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
1369 cx: &Context<Worktree>,
1370 ) {
1371 let snapshot = self.snapshot();
1372 let share_private_files = self.share_private_files;
1373 let next_entry_id = self.next_entry_id.clone();
1374 let fs = self.fs.clone();
1375 let git_hosting_provider_registry = GitHostingProviderRegistry::try_global(cx);
1376 let settings = self.settings.clone();
1377 let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
1378 let background_scanner = cx.background_executor().spawn({
1379 let abs_path = snapshot.abs_path.as_path().to_path_buf();
1380 let background = cx.background_executor().clone();
1381 async move {
1382 let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await;
1383 let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| {
1384 log::error!("Failed to determine whether filesystem is case sensitive: {e:#}");
1385 true
1386 });
1387
1388 let mut scanner = BackgroundScanner {
1389 fs,
1390 fs_case_sensitive,
1391 status_updates_tx: scan_states_tx,
1392 executor: background,
1393 scan_requests_rx,
1394 path_prefixes_to_scan_rx,
1395 next_entry_id,
1396 state: Mutex::new(BackgroundScannerState {
1397 prev_snapshot: snapshot.snapshot.clone(),
1398 snapshot,
1399 scanned_dirs: Default::default(),
1400 path_prefixes_to_scan: Default::default(),
1401 paths_to_scan: Default::default(),
1402 removed_entries: Default::default(),
1403 changed_paths: Default::default(),
1404 git_hosting_provider_registry,
1405 }),
1406 phase: BackgroundScannerPhase::InitialScan,
1407 share_private_files,
1408 settings,
1409 watcher,
1410 };
1411
1412 scanner
1413 .run(Box::pin(
1414 events.map(|events| events.into_iter().map(Into::into).collect()),
1415 ))
1416 .await;
1417 }
1418 });
1419 let scan_state_updater = cx.spawn(|this, mut cx| async move {
1420 while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
1421 this.update(&mut cx, |this, cx| {
1422 let this = this.as_local_mut().unwrap();
1423 match state {
1424 ScanState::Started => {
1425 *this.is_scanning.0.borrow_mut() = true;
1426 }
1427 ScanState::Updated {
1428 snapshot,
1429 changes,
1430 barrier,
1431 scanning,
1432 } => {
1433 *this.is_scanning.0.borrow_mut() = scanning;
1434 this.set_snapshot(snapshot, changes, cx);
1435 drop(barrier);
1436 }
1437 ScanState::RootUpdated { new_path } => {
1438 this.update_abs_path_and_refresh(new_path, cx);
1439 }
1440 }
1441 cx.notify();
1442 })
1443 .ok();
1444 }
1445 });
1446 self._background_scanner_tasks = vec![background_scanner, scan_state_updater];
1447 self.is_scanning = watch::channel_with(true);
1448 }
1449
1450 fn set_snapshot(
1451 &mut self,
1452 new_snapshot: LocalSnapshot,
1453 entry_changes: UpdatedEntriesSet,
1454 cx: &mut Context<Worktree>,
1455 ) {
1456 let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot);
1457 self.snapshot = new_snapshot;
1458
1459 if let Some(share) = self.update_observer.as_mut() {
1460 share
1461 .snapshots_tx
1462 .unbounded_send((
1463 self.snapshot.clone(),
1464 entry_changes.clone(),
1465 repo_changes.clone(),
1466 ))
1467 .ok();
1468 }
1469
1470 if !entry_changes.is_empty() {
1471 cx.emit(Event::UpdatedEntries(entry_changes));
1472 }
1473 if !repo_changes.is_empty() {
1474 cx.emit(Event::UpdatedGitRepositories(repo_changes));
1475 }
1476 }
1477
1478 fn changed_repos(
1479 &self,
1480 old_snapshot: &LocalSnapshot,
1481 new_snapshot: &LocalSnapshot,
1482 ) -> UpdatedGitRepositoriesSet {
1483 let mut changes = Vec::new();
1484 let mut old_repos = old_snapshot.git_repositories.iter().peekable();
1485 let mut new_repos = new_snapshot.git_repositories.iter().peekable();
1486
1487 loop {
1488 match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
1489 (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
1490 match Ord::cmp(&new_entry_id, &old_entry_id) {
1491 Ordering::Less => {
1492 if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
1493 changes.push((
1494 entry.path.clone(),
1495 GitRepositoryChange {
1496 old_repository: None,
1497 },
1498 ));
1499 }
1500 new_repos.next();
1501 }
1502 Ordering::Equal => {
1503 if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id
1504 || new_repo.status_scan_id != old_repo.status_scan_id
1505 {
1506 if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
1507 let old_repo = old_snapshot
1508 .repositories
1509 .get(&PathKey(entry.path.clone()), &())
1510 .cloned();
1511 changes.push((
1512 entry.path.clone(),
1513 GitRepositoryChange {
1514 old_repository: old_repo,
1515 },
1516 ));
1517 }
1518 }
1519 new_repos.next();
1520 old_repos.next();
1521 }
1522 Ordering::Greater => {
1523 if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) {
1524 let old_repo = old_snapshot
1525 .repositories
1526 .get(&PathKey(entry.path.clone()), &())
1527 .cloned();
1528 changes.push((
1529 entry.path.clone(),
1530 GitRepositoryChange {
1531 old_repository: old_repo,
1532 },
1533 ));
1534 }
1535 old_repos.next();
1536 }
1537 }
1538 }
1539 (Some((entry_id, _)), None) => {
1540 if let Some(entry) = new_snapshot.entry_for_id(entry_id) {
1541 changes.push((
1542 entry.path.clone(),
1543 GitRepositoryChange {
1544 old_repository: None,
1545 },
1546 ));
1547 }
1548 new_repos.next();
1549 }
1550 (None, Some((entry_id, _))) => {
1551 if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
1552 let old_repo = old_snapshot
1553 .repositories
1554 .get(&PathKey(entry.path.clone()), &())
1555 .cloned();
1556 changes.push((
1557 entry.path.clone(),
1558 GitRepositoryChange {
1559 old_repository: old_repo,
1560 },
1561 ));
1562 }
1563 old_repos.next();
1564 }
1565 (None, None) => break,
1566 }
1567 }
1568
1569 fn clone<T: Clone, U: Clone>(value: &(&T, &U)) -> (T, U) {
1570 (value.0.clone(), value.1.clone())
1571 }
1572
1573 changes.into()
1574 }
1575
1576 pub fn scan_complete(&self) -> impl Future<Output = ()> {
1577 let mut is_scanning_rx = self.is_scanning.1.clone();
1578 async move {
1579 let mut is_scanning = *is_scanning_rx.borrow();
1580 while is_scanning {
1581 if let Some(value) = is_scanning_rx.recv().await {
1582 is_scanning = value;
1583 } else {
1584 break;
1585 }
1586 }
1587 }
1588 }
1589
1590 pub fn snapshot(&self) -> LocalSnapshot {
1591 self.snapshot.clone()
1592 }
1593
1594 pub fn settings(&self) -> WorktreeSettings {
1595 self.settings.clone()
1596 }
1597
1598 pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
1599 self.git_repositories.get(&repo.work_directory_id)
1600 }
1601
1602 fn load_binary_file(
1603 &self,
1604 path: &Path,
1605 cx: &Context<Worktree>,
1606 ) -> Task<Result<LoadedBinaryFile>> {
1607 let path = Arc::from(path);
1608 let abs_path = self.absolutize(&path);
1609 let fs = self.fs.clone();
1610 let entry = self.refresh_entry(path.clone(), None, cx);
1611 let is_private = self.is_path_private(path.as_ref());
1612
1613 let worktree = cx.weak_entity();
1614 cx.background_executor().spawn(async move {
1615 let abs_path = abs_path?;
1616 let content = fs.load_bytes(&abs_path).await?;
1617
1618 let worktree = worktree
1619 .upgrade()
1620 .ok_or_else(|| anyhow!("worktree was dropped"))?;
1621 let file = match entry.await? {
1622 Some(entry) => File::for_entry(entry, worktree),
1623 None => {
1624 let metadata = fs
1625 .metadata(&abs_path)
1626 .await
1627 .with_context(|| {
1628 format!("Loading metadata for excluded file {abs_path:?}")
1629 })?
1630 .with_context(|| {
1631 format!("Excluded file {abs_path:?} got removed during loading")
1632 })?;
1633 Arc::new(File {
1634 entry_id: None,
1635 worktree,
1636 path,
1637 disk_state: DiskState::Present {
1638 mtime: metadata.mtime,
1639 },
1640 is_local: true,
1641 is_private,
1642 })
1643 }
1644 };
1645
1646 Ok(LoadedBinaryFile { file, content })
1647 })
1648 }
1649
1650 fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
1651 let path = Arc::from(path);
1652 let abs_path = self.absolutize(&path);
1653 let fs = self.fs.clone();
1654 let entry = self.refresh_entry(path.clone(), None, cx);
1655 let is_private = self.is_path_private(path.as_ref());
1656
1657 cx.spawn(|this, _cx| async move {
1658 let abs_path = abs_path?;
1659 let text = fs.load(&abs_path).await?;
1660
1661 let worktree = this
1662 .upgrade()
1663 .ok_or_else(|| anyhow!("worktree was dropped"))?;
1664 let file = match entry.await? {
1665 Some(entry) => File::for_entry(entry, worktree),
1666 None => {
1667 let metadata = fs
1668 .metadata(&abs_path)
1669 .await
1670 .with_context(|| {
1671 format!("Loading metadata for excluded file {abs_path:?}")
1672 })?
1673 .with_context(|| {
1674 format!("Excluded file {abs_path:?} got removed during loading")
1675 })?;
1676 Arc::new(File {
1677 entry_id: None,
1678 worktree,
1679 path,
1680 disk_state: DiskState::Present {
1681 mtime: metadata.mtime,
1682 },
1683 is_local: true,
1684 is_private,
1685 })
1686 }
1687 };
1688
1689 Ok(LoadedFile { file, text })
1690 })
1691 }
1692
1693 /// Find the lowest path in the worktree's datastructures that is an ancestor
1694 fn lowest_ancestor(&self, path: &Path) -> PathBuf {
1695 let mut lowest_ancestor = None;
1696 for path in path.ancestors() {
1697 if self.entry_for_path(path).is_some() {
1698 lowest_ancestor = Some(path.to_path_buf());
1699 break;
1700 }
1701 }
1702
1703 lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
1704 }
1705
1706 fn create_entry(
1707 &self,
1708 path: impl Into<Arc<Path>>,
1709 is_dir: bool,
1710 cx: &Context<Worktree>,
1711 ) -> Task<Result<CreatedEntry>> {
1712 let path = path.into();
1713 let abs_path = match self.absolutize(&path) {
1714 Ok(path) => path,
1715 Err(e) => return Task::ready(Err(e.context(format!("absolutizing path {path:?}")))),
1716 };
1717 let path_excluded = self.settings.is_path_excluded(&abs_path);
1718 let fs = self.fs.clone();
1719 let task_abs_path = abs_path.clone();
1720 let write = cx.background_executor().spawn(async move {
1721 if is_dir {
1722 fs.create_dir(&task_abs_path)
1723 .await
1724 .with_context(|| format!("creating directory {task_abs_path:?}"))
1725 } else {
1726 fs.save(&task_abs_path, &Rope::default(), LineEnding::default())
1727 .await
1728 .with_context(|| format!("creating file {task_abs_path:?}"))
1729 }
1730 });
1731
1732 let lowest_ancestor = self.lowest_ancestor(&path);
1733 cx.spawn(|this, mut cx| async move {
1734 write.await?;
1735 if path_excluded {
1736 return Ok(CreatedEntry::Excluded { abs_path });
1737 }
1738
1739 let (result, refreshes) = this.update(&mut cx, |this, cx| {
1740 let mut refreshes = Vec::new();
1741 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
1742 for refresh_path in refresh_paths.ancestors() {
1743 if refresh_path == Path::new("") {
1744 continue;
1745 }
1746 let refresh_full_path = lowest_ancestor.join(refresh_path);
1747
1748 refreshes.push(this.as_local_mut().unwrap().refresh_entry(
1749 refresh_full_path.into(),
1750 None,
1751 cx,
1752 ));
1753 }
1754 (
1755 this.as_local_mut().unwrap().refresh_entry(path, None, cx),
1756 refreshes,
1757 )
1758 })?;
1759 for refresh in refreshes {
1760 refresh.await.log_err();
1761 }
1762
1763 Ok(result
1764 .await?
1765 .map(CreatedEntry::Included)
1766 .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
1767 })
1768 }
1769
1770 fn write_file(
1771 &self,
1772 path: impl Into<Arc<Path>>,
1773 text: Rope,
1774 line_ending: LineEnding,
1775 cx: &Context<Worktree>,
1776 ) -> Task<Result<Arc<File>>> {
1777 let path = path.into();
1778 let fs = self.fs.clone();
1779 let is_private = self.is_path_private(&path);
1780 let Ok(abs_path) = self.absolutize(&path) else {
1781 return Task::ready(Err(anyhow!("invalid path {path:?}")));
1782 };
1783
1784 let write = cx.background_executor().spawn({
1785 let fs = fs.clone();
1786 let abs_path = abs_path.clone();
1787 async move { fs.save(&abs_path, &text, line_ending).await }
1788 });
1789
1790 cx.spawn(move |this, mut cx| async move {
1791 write.await?;
1792 let entry = this
1793 .update(&mut cx, |this, cx| {
1794 this.as_local_mut()
1795 .unwrap()
1796 .refresh_entry(path.clone(), None, cx)
1797 })?
1798 .await?;
1799 let worktree = this.upgrade().ok_or_else(|| anyhow!("worktree dropped"))?;
1800 if let Some(entry) = entry {
1801 Ok(File::for_entry(entry, worktree))
1802 } else {
1803 let metadata = fs
1804 .metadata(&abs_path)
1805 .await
1806 .with_context(|| {
1807 format!("Fetching metadata after saving the excluded buffer {abs_path:?}")
1808 })?
1809 .with_context(|| {
1810 format!("Excluded buffer {path:?} got removed during saving")
1811 })?;
1812 Ok(Arc::new(File {
1813 worktree,
1814 path,
1815 disk_state: DiskState::Present {
1816 mtime: metadata.mtime,
1817 },
1818 entry_id: None,
1819 is_local: true,
1820 is_private,
1821 }))
1822 }
1823 })
1824 }
1825
1826 fn delete_entry(
1827 &self,
1828 entry_id: ProjectEntryId,
1829 trash: bool,
1830 cx: &Context<Worktree>,
1831 ) -> Option<Task<Result<()>>> {
1832 let entry = self.entry_for_id(entry_id)?.clone();
1833 let abs_path = self.absolutize(&entry.path);
1834 let fs = self.fs.clone();
1835
1836 let delete = cx.background_executor().spawn(async move {
1837 if entry.is_file() {
1838 if trash {
1839 fs.trash_file(&abs_path?, Default::default()).await?;
1840 } else {
1841 fs.remove_file(&abs_path?, Default::default()).await?;
1842 }
1843 } else if trash {
1844 fs.trash_dir(
1845 &abs_path?,
1846 RemoveOptions {
1847 recursive: true,
1848 ignore_if_not_exists: false,
1849 },
1850 )
1851 .await?;
1852 } else {
1853 fs.remove_dir(
1854 &abs_path?,
1855 RemoveOptions {
1856 recursive: true,
1857 ignore_if_not_exists: false,
1858 },
1859 )
1860 .await?;
1861 }
1862 anyhow::Ok(entry.path)
1863 });
1864
1865 Some(cx.spawn(|this, mut cx| async move {
1866 let path = delete.await?;
1867 this.update(&mut cx, |this, _| {
1868 this.as_local_mut()
1869 .unwrap()
1870 .refresh_entries_for_paths(vec![path])
1871 })?
1872 .recv()
1873 .await;
1874 Ok(())
1875 }))
1876 }
1877
1878 /// Rename an entry.
1879 ///
1880 /// `new_path` is the new relative path to the worktree root.
1881 /// If the root entry is renamed then `new_path` is the new root name instead.
1882 fn rename_entry(
1883 &self,
1884 entry_id: ProjectEntryId,
1885 new_path: impl Into<Arc<Path>>,
1886 cx: &Context<Worktree>,
1887 ) -> Task<Result<CreatedEntry>> {
1888 let old_path = match self.entry_for_id(entry_id) {
1889 Some(entry) => entry.path.clone(),
1890 None => return Task::ready(Err(anyhow!("no entry to rename for id {entry_id:?}"))),
1891 };
1892 let new_path = new_path.into();
1893 let abs_old_path = self.absolutize(&old_path);
1894
1895 let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id);
1896 let abs_new_path = if is_root_entry {
1897 let Some(root_parent_path) = self.abs_path().parent() else {
1898 return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path)));
1899 };
1900 root_parent_path.join(&new_path)
1901 } else {
1902 let Ok(absolutize_path) = self.absolutize(&new_path) else {
1903 return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
1904 };
1905 absolutize_path
1906 };
1907 let abs_path = abs_new_path.clone();
1908 let fs = self.fs.clone();
1909 let case_sensitive = self.fs_case_sensitive;
1910 let rename = cx.background_executor().spawn(async move {
1911 let abs_old_path = abs_old_path?;
1912 let abs_new_path = abs_new_path;
1913
1914 let abs_old_path_lower = abs_old_path.to_str().map(|p| p.to_lowercase());
1915 let abs_new_path_lower = abs_new_path.to_str().map(|p| p.to_lowercase());
1916
1917 // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`)
1918 // we want to overwrite, because otherwise we run into a file-already-exists error.
1919 let overwrite = !case_sensitive
1920 && abs_old_path != abs_new_path
1921 && abs_old_path_lower == abs_new_path_lower;
1922
1923 fs.rename(
1924 &abs_old_path,
1925 &abs_new_path,
1926 fs::RenameOptions {
1927 overwrite,
1928 ..Default::default()
1929 },
1930 )
1931 .await
1932 .with_context(|| format!("Renaming {abs_old_path:?} into {abs_new_path:?}"))
1933 });
1934
1935 cx.spawn(|this, mut cx| async move {
1936 rename.await?;
1937 Ok(this
1938 .update(&mut cx, |this, cx| {
1939 let local = this.as_local_mut().unwrap();
1940 if is_root_entry {
1941 // We eagerly update `abs_path` and refresh this worktree.
1942 // Otherwise, the FS watcher would do it on the `RootUpdated` event,
1943 // but with a noticeable delay, so we handle it proactively.
1944 local.update_abs_path_and_refresh(
1945 Some(SanitizedPath::from(abs_path.clone())),
1946 cx,
1947 );
1948 Task::ready(Ok(this.root_entry().cloned()))
1949 } else {
1950 local.refresh_entry(new_path.clone(), Some(old_path), cx)
1951 }
1952 })?
1953 .await?
1954 .map(CreatedEntry::Included)
1955 .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
1956 })
1957 }
1958
1959 fn copy_entry(
1960 &self,
1961 entry_id: ProjectEntryId,
1962 relative_worktree_source_path: Option<PathBuf>,
1963 new_path: impl Into<Arc<Path>>,
1964 cx: &Context<Worktree>,
1965 ) -> Task<Result<Option<Entry>>> {
1966 let old_path = match self.entry_for_id(entry_id) {
1967 Some(entry) => entry.path.clone(),
1968 None => return Task::ready(Ok(None)),
1969 };
1970 let new_path = new_path.into();
1971 let abs_old_path =
1972 if let Some(relative_worktree_source_path) = relative_worktree_source_path {
1973 Ok(self.abs_path().join(relative_worktree_source_path))
1974 } else {
1975 self.absolutize(&old_path)
1976 };
1977 let abs_new_path = self.absolutize(&new_path);
1978 let fs = self.fs.clone();
1979 let copy = cx.background_executor().spawn(async move {
1980 copy_recursive(
1981 fs.as_ref(),
1982 &abs_old_path?,
1983 &abs_new_path?,
1984 Default::default(),
1985 )
1986 .await
1987 });
1988
1989 cx.spawn(|this, mut cx| async move {
1990 copy.await?;
1991 this.update(&mut cx, |this, cx| {
1992 this.as_local_mut()
1993 .unwrap()
1994 .refresh_entry(new_path.clone(), None, cx)
1995 })?
1996 .await
1997 })
1998 }
1999
2000 pub fn copy_external_entries(
2001 &self,
2002 target_directory: PathBuf,
2003 paths: Vec<Arc<Path>>,
2004 overwrite_existing_files: bool,
2005 cx: &Context<Worktree>,
2006 ) -> Task<Result<Vec<ProjectEntryId>>> {
2007 let worktree_path = self.abs_path().clone();
2008 let fs = self.fs.clone();
2009 let paths = paths
2010 .into_iter()
2011 .filter_map(|source| {
2012 let file_name = source.file_name()?;
2013 let mut target = target_directory.clone();
2014 target.push(file_name);
2015
2016 // Do not allow copying the same file to itself.
2017 if source.as_ref() != target.as_path() {
2018 Some((source, target))
2019 } else {
2020 None
2021 }
2022 })
2023 .collect::<Vec<_>>();
2024
2025 let paths_to_refresh = paths
2026 .iter()
2027 .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into()))
2028 .collect::<Vec<_>>();
2029
2030 cx.spawn(|this, cx| async move {
2031 cx.background_executor()
2032 .spawn(async move {
2033 for (source, target) in paths {
2034 copy_recursive(
2035 fs.as_ref(),
2036 &source,
2037 &target,
2038 fs::CopyOptions {
2039 overwrite: overwrite_existing_files,
2040 ..Default::default()
2041 },
2042 )
2043 .await
2044 .with_context(|| {
2045 anyhow!("Failed to copy file from {source:?} to {target:?}")
2046 })?;
2047 }
2048 Ok::<(), anyhow::Error>(())
2049 })
2050 .await
2051 .log_err();
2052 let mut refresh = cx.read_entity(
2053 &this.upgrade().with_context(|| "Dropped worktree")?,
2054 |this, _| {
2055 Ok::<postage::barrier::Receiver, anyhow::Error>(
2056 this.as_local()
2057 .with_context(|| "Worktree is not local")?
2058 .refresh_entries_for_paths(paths_to_refresh.clone()),
2059 )
2060 },
2061 )??;
2062
2063 cx.background_executor()
2064 .spawn(async move {
2065 refresh.next().await;
2066 Ok::<(), anyhow::Error>(())
2067 })
2068 .await
2069 .log_err();
2070
2071 let this = this.upgrade().with_context(|| "Dropped worktree")?;
2072 cx.read_entity(&this, |this, _| {
2073 paths_to_refresh
2074 .iter()
2075 .filter_map(|path| Some(this.entry_for_path(path)?.id))
2076 .collect()
2077 })
2078 })
2079 }
2080
2081 fn expand_entry(
2082 &self,
2083 entry_id: ProjectEntryId,
2084 cx: &Context<Worktree>,
2085 ) -> Option<Task<Result<()>>> {
2086 let path = self.entry_for_id(entry_id)?.path.clone();
2087 let mut refresh = self.refresh_entries_for_paths(vec![path]);
2088 Some(cx.background_executor().spawn(async move {
2089 refresh.next().await;
2090 Ok(())
2091 }))
2092 }
2093
2094 fn expand_all_for_entry(
2095 &self,
2096 entry_id: ProjectEntryId,
2097 cx: &Context<Worktree>,
2098 ) -> Option<Task<Result<()>>> {
2099 let path = self.entry_for_id(entry_id).unwrap().path.clone();
2100 let mut rx = self.add_path_prefix_to_scan(path.clone());
2101 Some(cx.background_executor().spawn(async move {
2102 rx.next().await;
2103 Ok(())
2104 }))
2105 }
2106
2107 fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
2108 let (tx, rx) = barrier::channel();
2109 self.scan_requests_tx
2110 .try_send(ScanRequest {
2111 relative_paths: paths,
2112 done: smallvec![tx],
2113 })
2114 .ok();
2115 rx
2116 }
2117
2118 pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
2119 let (tx, rx) = barrier::channel();
2120 self.path_prefixes_to_scan_tx
2121 .try_send(PathPrefixScanRequest {
2122 path: path_prefix,
2123 done: smallvec![tx],
2124 })
2125 .ok();
2126 rx
2127 }
2128
2129 fn refresh_entry(
2130 &self,
2131 path: Arc<Path>,
2132 old_path: Option<Arc<Path>>,
2133 cx: &Context<Worktree>,
2134 ) -> Task<Result<Option<Entry>>> {
2135 if self.settings.is_path_excluded(&path) {
2136 return Task::ready(Ok(None));
2137 }
2138 let paths = if let Some(old_path) = old_path.as_ref() {
2139 vec![old_path.clone(), path.clone()]
2140 } else {
2141 vec![path.clone()]
2142 };
2143 let t0 = Instant::now();
2144 let mut refresh = self.refresh_entries_for_paths(paths);
2145 cx.spawn(move |this, mut cx| async move {
2146 refresh.recv().await;
2147 log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed());
2148 let new_entry = this.update(&mut cx, |this, _| {
2149 this.entry_for_path(path)
2150 .cloned()
2151 .ok_or_else(|| anyhow!("failed to read path after update"))
2152 })??;
2153 Ok(Some(new_entry))
2154 })
2155 }
2156
2157 fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
2158 where
2159 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
2160 Fut: Send + Future<Output = bool>,
2161 {
2162 if let Some(observer) = self.update_observer.as_mut() {
2163 *observer.resume_updates.borrow_mut() = ();
2164 return;
2165 }
2166
2167 let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>();
2168 let (snapshots_tx, mut snapshots_rx) =
2169 mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>();
2170 snapshots_tx
2171 .unbounded_send((self.snapshot(), Arc::default(), Arc::default()))
2172 .ok();
2173
2174 let worktree_id = cx.entity_id().as_u64();
2175 let _maintain_remote_snapshot = cx.background_executor().spawn(async move {
2176 let mut is_first = true;
2177 while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await {
2178 let update;
2179 if is_first {
2180 update = snapshot.build_initial_update(project_id, worktree_id);
2181 is_first = false;
2182 } else {
2183 update =
2184 snapshot.build_update(project_id, worktree_id, entry_changes, repo_changes);
2185 }
2186
2187 for update in proto::split_worktree_update(update) {
2188 let _ = resume_updates_rx.try_recv();
2189 loop {
2190 let result = callback(update.clone());
2191 if result.await {
2192 break;
2193 } else {
2194 log::info!("waiting to resume updates");
2195 if resume_updates_rx.next().await.is_none() {
2196 return Some(());
2197 }
2198 }
2199 }
2200 }
2201 }
2202 Some(())
2203 });
2204
2205 self.update_observer = Some(UpdateObservationState {
2206 snapshots_tx,
2207 resume_updates: resume_updates_tx,
2208 _maintain_remote_snapshot,
2209 });
2210 }
2211
2212 pub fn share_private_files(&mut self, cx: &Context<Worktree>) {
2213 self.share_private_files = true;
2214 self.restart_background_scanners(cx);
2215 }
2216
2217 fn update_abs_path_and_refresh(
2218 &mut self,
2219 new_path: Option<SanitizedPath>,
2220 cx: &Context<Worktree>,
2221 ) {
2222 if let Some(new_path) = new_path {
2223 self.snapshot.git_repositories = Default::default();
2224 self.snapshot.ignores_by_parent_abs_path = Default::default();
2225 let root_name = new_path
2226 .as_path()
2227 .file_name()
2228 .map_or(String::new(), |f| f.to_string_lossy().to_string());
2229 self.snapshot.update_abs_path(new_path, root_name);
2230 }
2231 self.restart_background_scanners(cx);
2232 }
2233}
2234
2235impl RemoteWorktree {
2236 pub fn project_id(&self) -> u64 {
2237 self.project_id
2238 }
2239
2240 pub fn client(&self) -> AnyProtoClient {
2241 self.client.clone()
2242 }
2243
2244 pub fn disconnected_from_host(&mut self) {
2245 self.updates_tx.take();
2246 self.snapshot_subscriptions.clear();
2247 self.disconnected = true;
2248 }
2249
2250 pub fn update_from_remote(&self, update: proto::UpdateWorktree) {
2251 if let Some(updates_tx) = &self.updates_tx {
2252 updates_tx
2253 .unbounded_send(update)
2254 .expect("consumer runs to completion");
2255 }
2256 }
2257
2258 fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
2259 where
2260 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
2261 Fut: 'static + Send + Future<Output = bool>,
2262 {
2263 let (tx, mut rx) = mpsc::unbounded();
2264 let initial_update = self
2265 .snapshot
2266 .build_initial_update(project_id, self.id().to_proto());
2267 self.update_observer = Some(tx);
2268 cx.spawn(|this, mut cx| async move {
2269 let mut update = initial_update;
2270 'outer: loop {
2271 // SSH projects use a special project ID of 0, and we need to
2272 // remap it to the correct one here.
2273 update.project_id = project_id;
2274
2275 for chunk in split_worktree_update(update) {
2276 if !callback(chunk).await {
2277 break 'outer;
2278 }
2279 }
2280
2281 if let Some(next_update) = rx.next().await {
2282 update = next_update;
2283 } else {
2284 break;
2285 }
2286 }
2287 this.update(&mut cx, |this, _| {
2288 let this = this.as_remote_mut().unwrap();
2289 this.update_observer.take();
2290 })
2291 })
2292 .detach();
2293 }
2294
2295 fn observed_snapshot(&self, scan_id: usize) -> bool {
2296 self.completed_scan_id >= scan_id
2297 }
2298
2299 pub fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
2300 let (tx, rx) = oneshot::channel();
2301 if self.observed_snapshot(scan_id) {
2302 let _ = tx.send(());
2303 } else if self.disconnected {
2304 drop(tx);
2305 } else {
2306 match self
2307 .snapshot_subscriptions
2308 .binary_search_by_key(&scan_id, |probe| probe.0)
2309 {
2310 Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
2311 }
2312 }
2313
2314 async move {
2315 rx.await?;
2316 Ok(())
2317 }
2318 }
2319
2320 fn insert_entry(
2321 &mut self,
2322 entry: proto::Entry,
2323 scan_id: usize,
2324 cx: &Context<Worktree>,
2325 ) -> Task<Result<Entry>> {
2326 let wait_for_snapshot = self.wait_for_snapshot(scan_id);
2327 cx.spawn(|this, mut cx| async move {
2328 wait_for_snapshot.await?;
2329 this.update(&mut cx, |worktree, _| {
2330 let worktree = worktree.as_remote_mut().unwrap();
2331 let snapshot = &mut worktree.background_snapshot.lock().0;
2332 let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions);
2333 worktree.snapshot = snapshot.clone();
2334 entry
2335 })?
2336 })
2337 }
2338
2339 fn delete_entry(
2340 &self,
2341 entry_id: ProjectEntryId,
2342 trash: bool,
2343 cx: &Context<Worktree>,
2344 ) -> Option<Task<Result<()>>> {
2345 let response = self.client.request(proto::DeleteProjectEntry {
2346 project_id: self.project_id,
2347 entry_id: entry_id.to_proto(),
2348 use_trash: trash,
2349 });
2350 Some(cx.spawn(move |this, mut cx| async move {
2351 let response = response.await?;
2352 let scan_id = response.worktree_scan_id as usize;
2353
2354 this.update(&mut cx, move |this, _| {
2355 this.as_remote_mut().unwrap().wait_for_snapshot(scan_id)
2356 })?
2357 .await?;
2358
2359 this.update(&mut cx, |this, _| {
2360 let this = this.as_remote_mut().unwrap();
2361 let snapshot = &mut this.background_snapshot.lock().0;
2362 snapshot.delete_entry(entry_id);
2363 this.snapshot = snapshot.clone();
2364 })
2365 }))
2366 }
2367
2368 fn rename_entry(
2369 &self,
2370 entry_id: ProjectEntryId,
2371 new_path: impl Into<Arc<Path>>,
2372 cx: &Context<Worktree>,
2373 ) -> Task<Result<CreatedEntry>> {
2374 let new_path: Arc<Path> = new_path.into();
2375 let response = self.client.request(proto::RenameProjectEntry {
2376 project_id: self.project_id,
2377 entry_id: entry_id.to_proto(),
2378 new_path: new_path.as_ref().to_proto(),
2379 });
2380 cx.spawn(move |this, mut cx| async move {
2381 let response = response.await?;
2382 match response.entry {
2383 Some(entry) => this
2384 .update(&mut cx, |this, cx| {
2385 this.as_remote_mut().unwrap().insert_entry(
2386 entry,
2387 response.worktree_scan_id as usize,
2388 cx,
2389 )
2390 })?
2391 .await
2392 .map(CreatedEntry::Included),
2393 None => {
2394 let abs_path = this.update(&mut cx, |worktree, _| {
2395 worktree
2396 .absolutize(&new_path)
2397 .with_context(|| format!("absolutizing {new_path:?}"))
2398 })??;
2399 Ok(CreatedEntry::Excluded { abs_path })
2400 }
2401 }
2402 })
2403 }
2404}
2405
2406impl Snapshot {
2407 pub fn new(id: u64, root_name: String, abs_path: Arc<Path>) -> Self {
2408 Snapshot {
2409 id: WorktreeId::from_usize(id as usize),
2410 abs_path: abs_path.into(),
2411 root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
2412 root_name,
2413 always_included_entries: Default::default(),
2414 entries_by_path: Default::default(),
2415 entries_by_id: Default::default(),
2416 repositories: Default::default(),
2417 scan_id: 1,
2418 completed_scan_id: 0,
2419 }
2420 }
2421
2422 pub fn id(&self) -> WorktreeId {
2423 self.id
2424 }
2425
2426 // TODO:
2427 // Consider the following:
2428 //
2429 // ```rust
2430 // let abs_path: Arc<Path> = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project"
2431 // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs");
2432 // // The caller perform some actions here:
2433 // some_non_trimmed_path.strip_prefix(abs_path); // This fails
2434 // some_non_trimmed_path.starts_with(abs_path); // This fails too
2435 // ```
2436 //
2437 // This is definitely a bug, but it's not clear if we should handle it here or not.
2438 pub fn abs_path(&self) -> &Arc<Path> {
2439 self.abs_path.as_path()
2440 }
2441
2442 fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
2443 let mut updated_entries = self
2444 .entries_by_path
2445 .iter()
2446 .map(proto::Entry::from)
2447 .collect::<Vec<_>>();
2448 updated_entries.sort_unstable_by_key(|e| e.id);
2449
2450 let mut updated_repositories = self
2451 .repositories
2452 .iter()
2453 .map(|repository| repository.initial_update())
2454 .collect::<Vec<_>>();
2455 updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
2456
2457 proto::UpdateWorktree {
2458 project_id,
2459 worktree_id,
2460 abs_path: self.abs_path().to_proto(),
2461 root_name: self.root_name().to_string(),
2462 updated_entries,
2463 removed_entries: Vec::new(),
2464 scan_id: self.scan_id as u64,
2465 is_last_update: self.completed_scan_id == self.scan_id,
2466 updated_repositories,
2467 removed_repositories: Vec::new(),
2468 }
2469 }
2470
2471 pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
2472 if path
2473 .components()
2474 .any(|component| !matches!(component, std::path::Component::Normal(_)))
2475 {
2476 return Err(anyhow!("invalid path"));
2477 }
2478 if path.file_name().is_some() {
2479 Ok(self.abs_path.as_path().join(path))
2480 } else {
2481 Ok(self.abs_path.as_path().to_path_buf())
2482 }
2483 }
2484
2485 pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
2486 self.entries_by_id.get(&entry_id, &()).is_some()
2487 }
2488
2489 fn insert_entry(
2490 &mut self,
2491 entry: proto::Entry,
2492 always_included_paths: &PathMatcher,
2493 ) -> Result<Entry> {
2494 let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
2495 let old_entry = self.entries_by_id.insert_or_replace(
2496 PathEntry {
2497 id: entry.id,
2498 path: entry.path.clone(),
2499 is_ignored: entry.is_ignored,
2500 scan_id: 0,
2501 },
2502 &(),
2503 );
2504 if let Some(old_entry) = old_entry {
2505 self.entries_by_path.remove(&PathKey(old_entry.path), &());
2506 }
2507 self.entries_by_path.insert_or_replace(entry.clone(), &());
2508 Ok(entry)
2509 }
2510
2511 fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<Path>> {
2512 let removed_entry = self.entries_by_id.remove(&entry_id, &())?;
2513 self.entries_by_path = {
2514 let mut cursor = self.entries_by_path.cursor::<TraversalProgress>(&());
2515 let mut new_entries_by_path =
2516 cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left, &());
2517 while let Some(entry) = cursor.item() {
2518 if entry.path.starts_with(&removed_entry.path) {
2519 self.entries_by_id.remove(&entry.id, &());
2520 cursor.next(&());
2521 } else {
2522 break;
2523 }
2524 }
2525 new_entries_by_path.append(cursor.suffix(&()), &());
2526 new_entries_by_path
2527 };
2528
2529 Some(removed_entry.path)
2530 }
2531
2532 pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<FileStatus> {
2533 let path = path.as_ref();
2534 self.repository_for_path(path).and_then(|repo| {
2535 let repo_path = repo.relativize(path).unwrap();
2536 repo.statuses_by_path
2537 .get(&PathKey(repo_path.0), &())
2538 .map(|entry| entry.status)
2539 })
2540 }
2541
2542 fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
2543 self.abs_path = abs_path;
2544 if root_name != self.root_name {
2545 self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
2546 self.root_name = root_name;
2547 }
2548 }
2549
2550 pub(crate) fn apply_remote_update(
2551 &mut self,
2552 mut update: proto::UpdateWorktree,
2553 always_included_paths: &PathMatcher,
2554 ) -> Result<()> {
2555 log::trace!(
2556 "applying remote worktree update. {} entries updated, {} removed",
2557 update.updated_entries.len(),
2558 update.removed_entries.len()
2559 );
2560 self.update_abs_path(
2561 SanitizedPath::from(PathBuf::from_proto(update.abs_path)),
2562 update.root_name,
2563 );
2564
2565 let mut entries_by_path_edits = Vec::new();
2566 let mut entries_by_id_edits = Vec::new();
2567
2568 for entry_id in update.removed_entries {
2569 let entry_id = ProjectEntryId::from_proto(entry_id);
2570 entries_by_id_edits.push(Edit::Remove(entry_id));
2571 if let Some(entry) = self.entry_for_id(entry_id) {
2572 entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
2573 }
2574 }
2575
2576 for entry in update.updated_entries {
2577 let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
2578 if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) {
2579 entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
2580 }
2581 if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) {
2582 if old_entry.id != entry.id {
2583 entries_by_id_edits.push(Edit::Remove(old_entry.id));
2584 }
2585 }
2586 entries_by_id_edits.push(Edit::Insert(PathEntry {
2587 id: entry.id,
2588 path: entry.path.clone(),
2589 is_ignored: entry.is_ignored,
2590 scan_id: 0,
2591 }));
2592 entries_by_path_edits.push(Edit::Insert(entry));
2593 }
2594
2595 self.entries_by_path.edit(entries_by_path_edits, &());
2596 self.entries_by_id.edit(entries_by_id_edits, &());
2597
2598 update.removed_repositories.sort_unstable();
2599 self.repositories.retain(&(), |entry: &RepositoryEntry| {
2600 update
2601 .removed_repositories
2602 .binary_search(&entry.work_directory_id.to_proto())
2603 .is_err()
2604 });
2605
2606 for repository in update.updated_repositories {
2607 let work_directory_id = ProjectEntryId::from_proto(repository.work_directory_id);
2608 if let Some(work_dir_entry) = self.entry_for_id(work_directory_id) {
2609 let conflicted_paths = TreeSet::from_ordered_entries(
2610 repository
2611 .current_merge_conflicts
2612 .into_iter()
2613 .map(|path| RepoPath(Path::new(&path).into())),
2614 );
2615
2616 if self
2617 .repositories
2618 .contains(&PathKey(work_dir_entry.path.clone()), &())
2619 {
2620 let edits = repository
2621 .removed_statuses
2622 .into_iter()
2623 .map(|path| Edit::Remove(PathKey(FromProto::from_proto(path))))
2624 .chain(repository.updated_statuses.into_iter().filter_map(
2625 |updated_status| {
2626 Some(Edit::Insert(updated_status.try_into().log_err()?))
2627 },
2628 ))
2629 .collect::<Vec<_>>();
2630
2631 self.repositories
2632 .update(&PathKey(work_dir_entry.path.clone()), &(), |repo| {
2633 repo.branch = repository.branch.map(Into::into);
2634 repo.statuses_by_path.edit(edits, &());
2635 repo.current_merge_conflicts = conflicted_paths
2636 });
2637 } else {
2638 let statuses = SumTree::from_iter(
2639 repository
2640 .updated_statuses
2641 .into_iter()
2642 .filter_map(|updated_status| updated_status.try_into().log_err()),
2643 &(),
2644 );
2645
2646 self.repositories.insert_or_replace(
2647 RepositoryEntry {
2648 work_directory_id,
2649 // When syncing repository entries from a peer, we don't need
2650 // the location_in_repo field, since git operations don't happen locally
2651 // anyway.
2652 work_directory: WorkDirectory::InProject {
2653 relative_path: work_dir_entry.path.clone(),
2654 },
2655 branch: repository.branch.map(Into::into),
2656 statuses_by_path: statuses,
2657 current_merge_conflicts: conflicted_paths,
2658 },
2659 &(),
2660 );
2661 }
2662 } else {
2663 log::error!(
2664 "no work directory entry for repository {:?}",
2665 repository.work_directory_id
2666 )
2667 }
2668 }
2669
2670 self.scan_id = update.scan_id as usize;
2671 if update.is_last_update {
2672 self.completed_scan_id = update.scan_id as usize;
2673 }
2674
2675 Ok(())
2676 }
2677
2678 pub fn entry_count(&self) -> usize {
2679 self.entries_by_path.summary().count
2680 }
2681
2682 pub fn visible_entry_count(&self) -> usize {
2683 self.entries_by_path.summary().non_ignored_count
2684 }
2685
2686 pub fn dir_count(&self) -> usize {
2687 let summary = self.entries_by_path.summary();
2688 summary.count - summary.file_count
2689 }
2690
2691 pub fn visible_dir_count(&self) -> usize {
2692 let summary = self.entries_by_path.summary();
2693 summary.non_ignored_count - summary.non_ignored_file_count
2694 }
2695
2696 pub fn file_count(&self) -> usize {
2697 self.entries_by_path.summary().file_count
2698 }
2699
2700 pub fn visible_file_count(&self) -> usize {
2701 self.entries_by_path.summary().non_ignored_file_count
2702 }
2703
2704 fn traverse_from_offset(
2705 &self,
2706 include_files: bool,
2707 include_dirs: bool,
2708 include_ignored: bool,
2709 start_offset: usize,
2710 ) -> Traversal {
2711 let mut cursor = self.entries_by_path.cursor(&());
2712 cursor.seek(
2713 &TraversalTarget::Count {
2714 count: start_offset,
2715 include_files,
2716 include_dirs,
2717 include_ignored,
2718 },
2719 Bias::Right,
2720 &(),
2721 );
2722 Traversal {
2723 snapshot: self,
2724 cursor,
2725 include_files,
2726 include_dirs,
2727 include_ignored,
2728 }
2729 }
2730
2731 pub fn traverse_from_path(
2732 &self,
2733 include_files: bool,
2734 include_dirs: bool,
2735 include_ignored: bool,
2736 path: &Path,
2737 ) -> Traversal {
2738 Traversal::new(self, include_files, include_dirs, include_ignored, path)
2739 }
2740
2741 pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
2742 self.traverse_from_offset(true, false, include_ignored, start)
2743 }
2744
2745 pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal {
2746 self.traverse_from_offset(false, true, include_ignored, start)
2747 }
2748
2749 pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal {
2750 self.traverse_from_offset(true, true, include_ignored, start)
2751 }
2752
2753 #[cfg(any(feature = "test-support", test))]
2754 pub fn git_status(&self, work_dir: &Path) -> Option<Vec<StatusEntry>> {
2755 self.repositories
2756 .get(&PathKey(work_dir.into()), &())
2757 .map(|repo| repo.status().collect())
2758 }
2759
2760 pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
2761 &self.repositories
2762 }
2763
2764 /// Get the repository whose work directory corresponds to the given path.
2765 pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
2766 self.repositories.get(&work_directory, &()).cloned()
2767 }
2768
2769 /// Get the repository whose work directory contains the given path.
2770 #[track_caller]
2771 pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
2772 self.repositories
2773 .iter()
2774 .filter(|repo| repo.work_directory.directory_contains(path))
2775 .last()
2776 }
2777
2778 /// Given an ordered iterator of entries, returns an iterator of those entries,
2779 /// along with their containing git repository.
2780 #[track_caller]
2781 pub fn entries_with_repositories<'a>(
2782 &'a self,
2783 entries: impl 'a + Iterator<Item = &'a Entry>,
2784 ) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
2785 let mut containing_repos = Vec::<&RepositoryEntry>::new();
2786 let mut repositories = self.repositories().iter().peekable();
2787 entries.map(move |entry| {
2788 while let Some(repository) = containing_repos.last() {
2789 if repository.directory_contains(&entry.path) {
2790 break;
2791 } else {
2792 containing_repos.pop();
2793 }
2794 }
2795 while let Some(repository) = repositories.peek() {
2796 if repository.directory_contains(&entry.path) {
2797 containing_repos.push(repositories.next().unwrap());
2798 } else {
2799 break;
2800 }
2801 }
2802 let repo = containing_repos.last().copied();
2803 (entry, repo)
2804 })
2805 }
2806
2807 pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
2808 let empty_path = Path::new("");
2809 self.entries_by_path
2810 .cursor::<()>(&())
2811 .filter(move |entry| entry.path.as_ref() != empty_path)
2812 .map(|entry| &entry.path)
2813 }
2814
2815 pub fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> {
2816 let options = ChildEntriesOptions {
2817 include_files: true,
2818 include_dirs: true,
2819 include_ignored: true,
2820 };
2821 self.child_entries_with_options(parent_path, options)
2822 }
2823
2824 pub fn child_entries_with_options<'a>(
2825 &'a self,
2826 parent_path: &'a Path,
2827 options: ChildEntriesOptions,
2828 ) -> ChildEntriesIter<'a> {
2829 let mut cursor = self.entries_by_path.cursor(&());
2830 cursor.seek(&TraversalTarget::path(parent_path), Bias::Right, &());
2831 let traversal = Traversal {
2832 snapshot: self,
2833 cursor,
2834 include_files: options.include_files,
2835 include_dirs: options.include_dirs,
2836 include_ignored: options.include_ignored,
2837 };
2838 ChildEntriesIter {
2839 traversal,
2840 parent_path,
2841 }
2842 }
2843
2844 pub fn root_entry(&self) -> Option<&Entry> {
2845 self.entry_for_path("")
2846 }
2847
2848 /// TODO: what's the difference between `root_dir` and `abs_path`?
2849 /// is there any? if so, document it.
2850 pub fn root_dir(&self) -> Option<Arc<Path>> {
2851 self.root_entry()
2852 .filter(|entry| entry.is_dir())
2853 .map(|_| self.abs_path().clone())
2854 }
2855
2856 pub fn root_name(&self) -> &str {
2857 &self.root_name
2858 }
2859
2860 pub fn root_git_entry(&self) -> Option<RepositoryEntry> {
2861 self.repositories
2862 .get(&PathKey(Path::new("").into()), &())
2863 .map(|entry| entry.to_owned())
2864 }
2865
2866 pub fn git_entry(&self, work_directory_path: Arc<Path>) -> Option<RepositoryEntry> {
2867 self.repositories
2868 .get(&PathKey(work_directory_path), &())
2869 .map(|entry| entry.to_owned())
2870 }
2871
2872 pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
2873 self.repositories.iter()
2874 }
2875
2876 pub fn scan_id(&self) -> usize {
2877 self.scan_id
2878 }
2879
2880 pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
2881 let path = path.as_ref();
2882 debug_assert!(path.is_relative());
2883 self.traverse_from_path(true, true, true, path)
2884 .entry()
2885 .and_then(|entry| {
2886 if entry.path.as_ref() == path {
2887 Some(entry)
2888 } else {
2889 None
2890 }
2891 })
2892 }
2893
2894 pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
2895 let entry = self.entries_by_id.get(&id, &())?;
2896 self.entry_for_path(&entry.path)
2897 }
2898
2899 pub fn inode_for_path(&self, path: impl AsRef<Path>) -> Option<u64> {
2900 self.entry_for_path(path.as_ref()).map(|e| e.inode)
2901 }
2902}
2903
2904impl LocalSnapshot {
2905 pub fn local_repo_for_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
2906 let repository_entry = self.repository_for_path(path)?;
2907 let work_directory_id = repository_entry.work_directory_id();
2908 self.git_repositories.get(&work_directory_id)
2909 }
2910
2911 fn build_update(
2912 &self,
2913 project_id: u64,
2914 worktree_id: u64,
2915 entry_changes: UpdatedEntriesSet,
2916 repo_changes: UpdatedGitRepositoriesSet,
2917 ) -> proto::UpdateWorktree {
2918 let mut updated_entries = Vec::new();
2919 let mut removed_entries = Vec::new();
2920 let mut updated_repositories = Vec::new();
2921 let mut removed_repositories = Vec::new();
2922
2923 for (_, entry_id, path_change) in entry_changes.iter() {
2924 if let PathChange::Removed = path_change {
2925 removed_entries.push(entry_id.0 as u64);
2926 } else if let Some(entry) = self.entry_for_id(*entry_id) {
2927 updated_entries.push(proto::Entry::from(entry));
2928 }
2929 }
2930
2931 for (work_dir_path, change) in repo_changes.iter() {
2932 let new_repo = self.repositories.get(&PathKey(work_dir_path.clone()), &());
2933 match (&change.old_repository, new_repo) {
2934 (Some(old_repo), Some(new_repo)) => {
2935 updated_repositories.push(new_repo.build_update(old_repo));
2936 }
2937 (None, Some(new_repo)) => {
2938 updated_repositories.push(new_repo.initial_update());
2939 }
2940 (Some(old_repo), None) => {
2941 removed_repositories.push(old_repo.work_directory_id.to_proto());
2942 }
2943 _ => {}
2944 }
2945 }
2946
2947 removed_entries.sort_unstable();
2948 updated_entries.sort_unstable_by_key(|e| e.id);
2949 removed_repositories.sort_unstable();
2950 updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
2951
2952 // TODO - optimize, knowing that removed_entries are sorted.
2953 removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err());
2954
2955 proto::UpdateWorktree {
2956 project_id,
2957 worktree_id,
2958 abs_path: self.abs_path().to_proto(),
2959 root_name: self.root_name().to_string(),
2960 updated_entries,
2961 removed_entries,
2962 scan_id: self.scan_id as u64,
2963 is_last_update: self.completed_scan_id == self.scan_id,
2964 updated_repositories,
2965 removed_repositories,
2966 }
2967 }
2968
2969 fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
2970 if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
2971 let abs_path = self.abs_path.as_path().join(&entry.path);
2972 match smol::block_on(build_gitignore(&abs_path, fs)) {
2973 Ok(ignore) => {
2974 self.ignores_by_parent_abs_path
2975 .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
2976 }
2977 Err(error) => {
2978 log::error!(
2979 "error loading .gitignore file {:?} - {:?}",
2980 &entry.path,
2981 error
2982 );
2983 }
2984 }
2985 }
2986
2987 if entry.kind == EntryKind::PendingDir {
2988 if let Some(existing_entry) =
2989 self.entries_by_path.get(&PathKey(entry.path.clone()), &())
2990 {
2991 entry.kind = existing_entry.kind;
2992 }
2993 }
2994
2995 let scan_id = self.scan_id;
2996 let removed = self.entries_by_path.insert_or_replace(entry.clone(), &());
2997 if let Some(removed) = removed {
2998 if removed.id != entry.id {
2999 self.entries_by_id.remove(&removed.id, &());
3000 }
3001 }
3002 self.entries_by_id.insert_or_replace(
3003 PathEntry {
3004 id: entry.id,
3005 path: entry.path.clone(),
3006 is_ignored: entry.is_ignored,
3007 scan_id,
3008 },
3009 &(),
3010 );
3011
3012 entry
3013 }
3014
3015 fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
3016 let mut inodes = TreeSet::default();
3017 for ancestor in path.ancestors().skip(1) {
3018 if let Some(entry) = self.entry_for_path(ancestor) {
3019 inodes.insert(entry.inode);
3020 }
3021 }
3022 inodes
3023 }
3024
3025 fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
3026 let mut new_ignores = Vec::new();
3027 for (index, ancestor) in abs_path.ancestors().enumerate() {
3028 if index > 0 {
3029 if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
3030 new_ignores.push((ancestor, Some(ignore.clone())));
3031 } else {
3032 new_ignores.push((ancestor, None));
3033 }
3034 }
3035 if ancestor.join(*DOT_GIT).exists() {
3036 break;
3037 }
3038 }
3039
3040 let mut ignore_stack = IgnoreStack::none();
3041 for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
3042 if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
3043 ignore_stack = IgnoreStack::all();
3044 break;
3045 } else if let Some(ignore) = ignore {
3046 ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
3047 }
3048 }
3049
3050 if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
3051 ignore_stack = IgnoreStack::all();
3052 }
3053
3054 ignore_stack
3055 }
3056
3057 #[cfg(test)]
3058 pub(crate) fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
3059 self.entries_by_path
3060 .cursor::<()>(&())
3061 .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
3062 }
3063
3064 #[cfg(test)]
3065 pub fn check_invariants(&self, git_state: bool) {
3066 use pretty_assertions::assert_eq;
3067
3068 assert_eq!(
3069 self.entries_by_path
3070 .cursor::<()>(&())
3071 .map(|e| (&e.path, e.id))
3072 .collect::<Vec<_>>(),
3073 self.entries_by_id
3074 .cursor::<()>(&())
3075 .map(|e| (&e.path, e.id))
3076 .collect::<collections::BTreeSet<_>>()
3077 .into_iter()
3078 .collect::<Vec<_>>(),
3079 "entries_by_path and entries_by_id are inconsistent"
3080 );
3081
3082 let mut files = self.files(true, 0);
3083 let mut visible_files = self.files(false, 0);
3084 for entry in self.entries_by_path.cursor::<()>(&()) {
3085 if entry.is_file() {
3086 assert_eq!(files.next().unwrap().inode, entry.inode);
3087 if (!entry.is_ignored && !entry.is_external) || entry.is_always_included {
3088 assert_eq!(visible_files.next().unwrap().inode, entry.inode);
3089 }
3090 }
3091 }
3092
3093 assert!(files.next().is_none());
3094 assert!(visible_files.next().is_none());
3095
3096 let mut bfs_paths = Vec::new();
3097 let mut stack = self
3098 .root_entry()
3099 .map(|e| e.path.as_ref())
3100 .into_iter()
3101 .collect::<Vec<_>>();
3102 while let Some(path) = stack.pop() {
3103 bfs_paths.push(path);
3104 let ix = stack.len();
3105 for child_entry in self.child_entries(path) {
3106 stack.insert(ix, &child_entry.path);
3107 }
3108 }
3109
3110 let dfs_paths_via_iter = self
3111 .entries_by_path
3112 .cursor::<()>(&())
3113 .map(|e| e.path.as_ref())
3114 .collect::<Vec<_>>();
3115 assert_eq!(bfs_paths, dfs_paths_via_iter);
3116
3117 let dfs_paths_via_traversal = self
3118 .entries(true, 0)
3119 .map(|e| e.path.as_ref())
3120 .collect::<Vec<_>>();
3121 assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
3122
3123 if git_state {
3124 for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
3125 let ignore_parent_path = ignore_parent_abs_path
3126 .strip_prefix(self.abs_path.as_path())
3127 .unwrap();
3128 assert!(self.entry_for_path(ignore_parent_path).is_some());
3129 assert!(self
3130 .entry_for_path(ignore_parent_path.join(*GITIGNORE))
3131 .is_some());
3132 }
3133 }
3134 }
3135
3136 #[cfg(test)]
3137 fn check_git_invariants(&self) {
3138 let dotgit_paths = self
3139 .git_repositories
3140 .iter()
3141 .map(|repo| repo.1.dot_git_dir_abs_path.clone())
3142 .collect::<HashSet<_>>();
3143 let work_dir_paths = self
3144 .repositories
3145 .iter()
3146 .map(|repo| repo.work_directory.path_key())
3147 .collect::<HashSet<_>>();
3148 assert_eq!(dotgit_paths.len(), work_dir_paths.len());
3149 assert_eq!(self.repositories.iter().count(), work_dir_paths.len());
3150 assert_eq!(self.git_repositories.iter().count(), work_dir_paths.len());
3151 for entry in self.repositories.iter() {
3152 self.git_repositories.get(&entry.work_directory_id).unwrap();
3153 }
3154 }
3155
3156 #[cfg(test)]
3157 pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
3158 let mut paths = Vec::new();
3159 for entry in self.entries_by_path.cursor::<()>(&()) {
3160 if include_ignored || !entry.is_ignored {
3161 paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored));
3162 }
3163 }
3164 paths.sort_by(|a, b| a.0.cmp(b.0));
3165 paths
3166 }
3167}
3168
3169impl BackgroundScannerState {
3170 fn should_scan_directory(&self, entry: &Entry) -> bool {
3171 (!entry.is_external && (!entry.is_ignored || entry.is_always_included))
3172 || entry.path.file_name() == Some(*DOT_GIT)
3173 || entry.path.file_name() == Some(local_settings_folder_relative_path().as_os_str())
3174 || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
3175 || self
3176 .paths_to_scan
3177 .iter()
3178 .any(|p| p.starts_with(&entry.path))
3179 || self
3180 .path_prefixes_to_scan
3181 .iter()
3182 .any(|p| entry.path.starts_with(p))
3183 }
3184
3185 fn enqueue_scan_dir(&self, abs_path: Arc<Path>, entry: &Entry, scan_job_tx: &Sender<ScanJob>) {
3186 let path = entry.path.clone();
3187 let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
3188 let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
3189
3190 if !ancestor_inodes.contains(&entry.inode) {
3191 ancestor_inodes.insert(entry.inode);
3192 scan_job_tx
3193 .try_send(ScanJob {
3194 abs_path,
3195 path,
3196 ignore_stack,
3197 scan_queue: scan_job_tx.clone(),
3198 ancestor_inodes,
3199 is_external: entry.is_external,
3200 })
3201 .unwrap();
3202 }
3203 }
3204
3205 fn reuse_entry_id(&mut self, entry: &mut Entry) {
3206 if let Some(mtime) = entry.mtime {
3207 // If an entry with the same inode was removed from the worktree during this scan,
3208 // then it *might* represent the same file or directory. But the OS might also have
3209 // re-used the inode for a completely different file or directory.
3210 //
3211 // Conditionally reuse the old entry's id:
3212 // * if the mtime is the same, the file was probably been renamed.
3213 // * if the path is the same, the file may just have been updated
3214 if let Some(removed_entry) = self.removed_entries.remove(&entry.inode) {
3215 if removed_entry.mtime == Some(mtime) || removed_entry.path == entry.path {
3216 entry.id = removed_entry.id;
3217 }
3218 } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) {
3219 entry.id = existing_entry.id;
3220 }
3221 }
3222 }
3223
3224 fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry {
3225 self.reuse_entry_id(&mut entry);
3226 let entry = self.snapshot.insert_entry(entry, fs);
3227 if entry.path.file_name() == Some(&DOT_GIT) {
3228 self.insert_git_repository(entry.path.clone(), fs, watcher);
3229 }
3230
3231 #[cfg(test)]
3232 self.snapshot.check_invariants(false);
3233
3234 entry
3235 }
3236
3237 fn populate_dir(
3238 &mut self,
3239 parent_path: &Arc<Path>,
3240 entries: impl IntoIterator<Item = Entry>,
3241 ignore: Option<Arc<Gitignore>>,
3242 ) {
3243 let mut parent_entry = if let Some(parent_entry) = self
3244 .snapshot
3245 .entries_by_path
3246 .get(&PathKey(parent_path.clone()), &())
3247 {
3248 parent_entry.clone()
3249 } else {
3250 log::warn!(
3251 "populating a directory {:?} that has been removed",
3252 parent_path
3253 );
3254 return;
3255 };
3256
3257 match parent_entry.kind {
3258 EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir,
3259 EntryKind::Dir => {}
3260 _ => return,
3261 }
3262
3263 if let Some(ignore) = ignore {
3264 let abs_parent_path = self.snapshot.abs_path.as_path().join(parent_path).into();
3265 self.snapshot
3266 .ignores_by_parent_abs_path
3267 .insert(abs_parent_path, (ignore, false));
3268 }
3269
3270 let parent_entry_id = parent_entry.id;
3271 self.scanned_dirs.insert(parent_entry_id);
3272 let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
3273 let mut entries_by_id_edits = Vec::new();
3274
3275 for entry in entries {
3276 entries_by_id_edits.push(Edit::Insert(PathEntry {
3277 id: entry.id,
3278 path: entry.path.clone(),
3279 is_ignored: entry.is_ignored,
3280 scan_id: self.snapshot.scan_id,
3281 }));
3282 entries_by_path_edits.push(Edit::Insert(entry));
3283 }
3284
3285 self.snapshot
3286 .entries_by_path
3287 .edit(entries_by_path_edits, &());
3288 self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
3289
3290 if let Err(ix) = self.changed_paths.binary_search(parent_path) {
3291 self.changed_paths.insert(ix, parent_path.clone());
3292 }
3293
3294 #[cfg(test)]
3295 self.snapshot.check_invariants(false);
3296 }
3297
3298 fn remove_path(&mut self, path: &Path) {
3299 let mut new_entries;
3300 let removed_entries;
3301 {
3302 let mut cursor = self
3303 .snapshot
3304 .entries_by_path
3305 .cursor::<TraversalProgress>(&());
3306 new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left, &());
3307 removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left, &());
3308 new_entries.append(cursor.suffix(&()), &());
3309 }
3310 self.snapshot.entries_by_path = new_entries;
3311
3312 let mut removed_ids = Vec::with_capacity(removed_entries.summary().count);
3313 for entry in removed_entries.cursor::<()>(&()) {
3314 match self.removed_entries.entry(entry.inode) {
3315 hash_map::Entry::Occupied(mut e) => {
3316 let prev_removed_entry = e.get_mut();
3317 if entry.id > prev_removed_entry.id {
3318 *prev_removed_entry = entry.clone();
3319 }
3320 }
3321 hash_map::Entry::Vacant(e) => {
3322 e.insert(entry.clone());
3323 }
3324 }
3325
3326 if entry.path.file_name() == Some(&GITIGNORE) {
3327 let abs_parent_path = self
3328 .snapshot
3329 .abs_path
3330 .as_path()
3331 .join(entry.path.parent().unwrap());
3332 if let Some((_, needs_update)) = self
3333 .snapshot
3334 .ignores_by_parent_abs_path
3335 .get_mut(abs_parent_path.as_path())
3336 {
3337 *needs_update = true;
3338 }
3339 }
3340
3341 if let Err(ix) = removed_ids.binary_search(&entry.id) {
3342 removed_ids.insert(ix, entry.id);
3343 }
3344 }
3345
3346 self.snapshot.entries_by_id.edit(
3347 removed_ids.iter().map(|&id| Edit::Remove(id)).collect(),
3348 &(),
3349 );
3350 self.snapshot
3351 .git_repositories
3352 .retain(|id, _| removed_ids.binary_search(id).is_err());
3353 self.snapshot.repositories.retain(&(), |repository| {
3354 !repository.work_directory.path_key().0.starts_with(path)
3355 });
3356
3357 #[cfg(test)]
3358 self.snapshot.check_invariants(false);
3359 }
3360
3361 fn insert_git_repository(
3362 &mut self,
3363 dot_git_path: Arc<Path>,
3364 fs: &dyn Fs,
3365 watcher: &dyn Watcher,
3366 ) -> Option<LocalRepositoryEntry> {
3367 let work_dir_path: Arc<Path> = match dot_git_path.parent() {
3368 Some(parent_dir) => {
3369 // Guard against repositories inside the repository metadata
3370 if parent_dir.iter().any(|component| component == *DOT_GIT) {
3371 log::info!(
3372 "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
3373 );
3374 return None;
3375 };
3376 log::info!(
3377 "building git repository, `.git` path in the worktree: {dot_git_path:?}"
3378 );
3379
3380 parent_dir.into()
3381 }
3382 None => {
3383 // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself,
3384 // no files inside that directory are tracked by git, so no need to build the repo around it
3385 log::info!(
3386 "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
3387 );
3388 return None;
3389 }
3390 };
3391
3392 self.insert_git_repository_for_path(
3393 WorkDirectory::InProject {
3394 relative_path: work_dir_path,
3395 },
3396 dot_git_path,
3397 fs,
3398 watcher,
3399 )
3400 }
3401
3402 fn insert_git_repository_for_path(
3403 &mut self,
3404 work_directory: WorkDirectory,
3405 dot_git_path: Arc<Path>,
3406 fs: &dyn Fs,
3407 watcher: &dyn Watcher,
3408 ) -> Option<LocalRepositoryEntry> {
3409 let work_dir_id = self
3410 .snapshot
3411 .entry_for_path(work_directory.path_key().0)
3412 .map(|entry| entry.id)?;
3413
3414 if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
3415 return None;
3416 }
3417
3418 let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
3419
3420 let t0 = Instant::now();
3421 let repository = fs.open_repo(&dot_git_abs_path)?;
3422
3423 let repository_path = repository.path();
3424 watcher.add(&repository_path).log_err()?;
3425
3426 let actual_dot_git_dir_abs_path = repository.main_repository_path();
3427 let dot_git_worktree_abs_path = if actual_dot_git_dir_abs_path == dot_git_abs_path {
3428 None
3429 } else {
3430 // The two paths could be different because we opened a git worktree.
3431 // When that happens:
3432 //
3433 // * `dot_git_abs_path` is a file that points to the worktree-subdirectory in the actual
3434 // .git directory.
3435 //
3436 // * `repository_path` is the worktree-subdirectory.
3437 //
3438 // * `actual_dot_git_dir_abs_path` is the path to the actual .git directory. In git
3439 // documentation this is called the "commondir".
3440 watcher.add(&dot_git_abs_path).log_err()?;
3441 Some(Arc::from(dot_git_abs_path))
3442 };
3443
3444 log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
3445
3446 if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
3447 git_hosting_providers::register_additional_providers(
3448 git_hosting_provider_registry,
3449 repository.clone(),
3450 );
3451 }
3452
3453 self.snapshot.repositories.insert_or_replace(
3454 RepositoryEntry {
3455 work_directory_id: work_dir_id,
3456 work_directory: work_directory.clone(),
3457 branch: repository.branch_name().map(Into::into),
3458 statuses_by_path: Default::default(),
3459 current_merge_conflicts: Default::default(),
3460 },
3461 &(),
3462 );
3463
3464 let local_repository = LocalRepositoryEntry {
3465 work_directory_id: work_dir_id,
3466 work_directory: work_directory.clone(),
3467 git_dir_scan_id: 0,
3468 status_scan_id: 0,
3469 repo_ptr: repository.clone(),
3470 dot_git_dir_abs_path: actual_dot_git_dir_abs_path.into(),
3471 dot_git_worktree_abs_path,
3472 current_merge_head_shas: Default::default(),
3473 };
3474
3475 self.snapshot
3476 .git_repositories
3477 .insert(work_dir_id, local_repository.clone());
3478
3479 Some(local_repository)
3480 }
3481}
3482
3483async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool {
3484 if path.file_name() == Some(&*DOT_GIT) {
3485 return true;
3486 }
3487
3488 // If we're in a bare repository, we are not inside a `.git` folder. In a
3489 // bare repository, the root folder contains what would normally be in the
3490 // `.git` folder.
3491 let head_metadata = fs.metadata(&path.join("HEAD")).await;
3492 if !matches!(head_metadata, Ok(Some(_))) {
3493 return false;
3494 }
3495 let config_metadata = fs.metadata(&path.join("config")).await;
3496 matches!(config_metadata, Ok(Some(_)))
3497}
3498
3499async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
3500 let contents = fs.load(abs_path).await?;
3501 let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
3502 let mut builder = GitignoreBuilder::new(parent);
3503 for line in contents.lines() {
3504 builder.add_line(Some(abs_path.into()), line)?;
3505 }
3506 Ok(builder.build()?)
3507}
3508
3509impl Deref for Worktree {
3510 type Target = Snapshot;
3511
3512 fn deref(&self) -> &Self::Target {
3513 match self {
3514 Worktree::Local(worktree) => &worktree.snapshot,
3515 Worktree::Remote(worktree) => &worktree.snapshot,
3516 }
3517 }
3518}
3519
3520impl Deref for LocalWorktree {
3521 type Target = LocalSnapshot;
3522
3523 fn deref(&self) -> &Self::Target {
3524 &self.snapshot
3525 }
3526}
3527
3528impl Deref for RemoteWorktree {
3529 type Target = Snapshot;
3530
3531 fn deref(&self) -> &Self::Target {
3532 &self.snapshot
3533 }
3534}
3535
3536impl fmt::Debug for LocalWorktree {
3537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3538 self.snapshot.fmt(f)
3539 }
3540}
3541
3542impl fmt::Debug for Snapshot {
3543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3544 struct EntriesById<'a>(&'a SumTree<PathEntry>);
3545 struct EntriesByPath<'a>(&'a SumTree<Entry>);
3546
3547 impl<'a> fmt::Debug for EntriesByPath<'a> {
3548 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3549 f.debug_map()
3550 .entries(self.0.iter().map(|entry| (&entry.path, entry.id)))
3551 .finish()
3552 }
3553 }
3554
3555 impl<'a> fmt::Debug for EntriesById<'a> {
3556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3557 f.debug_list().entries(self.0.iter()).finish()
3558 }
3559 }
3560
3561 f.debug_struct("Snapshot")
3562 .field("id", &self.id)
3563 .field("root_name", &self.root_name)
3564 .field("entries_by_path", &EntriesByPath(&self.entries_by_path))
3565 .field("entries_by_id", &EntriesById(&self.entries_by_id))
3566 .finish()
3567 }
3568}
3569
3570#[derive(Clone, PartialEq)]
3571pub struct File {
3572 pub worktree: Entity<Worktree>,
3573 pub path: Arc<Path>,
3574 pub disk_state: DiskState,
3575 pub entry_id: Option<ProjectEntryId>,
3576 pub is_local: bool,
3577 pub is_private: bool,
3578}
3579
3580impl language::File for File {
3581 fn as_local(&self) -> Option<&dyn language::LocalFile> {
3582 if self.is_local {
3583 Some(self)
3584 } else {
3585 None
3586 }
3587 }
3588
3589 fn disk_state(&self) -> DiskState {
3590 self.disk_state
3591 }
3592
3593 fn path(&self) -> &Arc<Path> {
3594 &self.path
3595 }
3596
3597 fn full_path(&self, cx: &App) -> PathBuf {
3598 let mut full_path = PathBuf::new();
3599 let worktree = self.worktree.read(cx);
3600
3601 if worktree.is_visible() {
3602 full_path.push(worktree.root_name());
3603 } else {
3604 let path = worktree.abs_path();
3605
3606 if worktree.is_local() && path.starts_with(home_dir().as_path()) {
3607 full_path.push("~");
3608 full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
3609 } else {
3610 full_path.push(path)
3611 }
3612 }
3613
3614 if self.path.components().next().is_some() {
3615 full_path.push(&self.path);
3616 }
3617
3618 full_path
3619 }
3620
3621 /// Returns the last component of this handle's absolute path. If this handle refers to the root
3622 /// of its worktree, then this method will return the name of the worktree itself.
3623 fn file_name<'a>(&'a self, cx: &'a App) -> &'a OsStr {
3624 self.path
3625 .file_name()
3626 .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
3627 }
3628
3629 fn worktree_id(&self, cx: &App) -> WorktreeId {
3630 self.worktree.read(cx).id()
3631 }
3632
3633 fn as_any(&self) -> &dyn Any {
3634 self
3635 }
3636
3637 fn to_proto(&self, cx: &App) -> rpc::proto::File {
3638 rpc::proto::File {
3639 worktree_id: self.worktree.read(cx).id().to_proto(),
3640 entry_id: self.entry_id.map(|id| id.to_proto()),
3641 path: self.path.as_ref().to_proto(),
3642 mtime: self.disk_state.mtime().map(|time| time.into()),
3643 is_deleted: self.disk_state == DiskState::Deleted,
3644 }
3645 }
3646
3647 fn is_private(&self) -> bool {
3648 self.is_private
3649 }
3650}
3651
3652impl language::LocalFile for File {
3653 fn abs_path(&self, cx: &App) -> PathBuf {
3654 let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
3655 if self.path.as_ref() == Path::new("") {
3656 worktree_path.as_path().to_path_buf()
3657 } else {
3658 worktree_path.as_path().join(&self.path)
3659 }
3660 }
3661
3662 fn load(&self, cx: &App) -> Task<Result<String>> {
3663 let worktree = self.worktree.read(cx).as_local().unwrap();
3664 let abs_path = worktree.absolutize(&self.path);
3665 let fs = worktree.fs.clone();
3666 cx.background_executor()
3667 .spawn(async move { fs.load(&abs_path?).await })
3668 }
3669
3670 fn load_bytes(&self, cx: &App) -> Task<Result<Vec<u8>>> {
3671 let worktree = self.worktree.read(cx).as_local().unwrap();
3672 let abs_path = worktree.absolutize(&self.path);
3673 let fs = worktree.fs.clone();
3674 cx.background_executor()
3675 .spawn(async move { fs.load_bytes(&abs_path?).await })
3676 }
3677}
3678
3679impl File {
3680 pub fn for_entry(entry: Entry, worktree: Entity<Worktree>) -> Arc<Self> {
3681 Arc::new(Self {
3682 worktree,
3683 path: entry.path.clone(),
3684 disk_state: if let Some(mtime) = entry.mtime {
3685 DiskState::Present { mtime }
3686 } else {
3687 DiskState::New
3688 },
3689 entry_id: Some(entry.id),
3690 is_local: true,
3691 is_private: entry.is_private,
3692 })
3693 }
3694
3695 pub fn from_proto(
3696 proto: rpc::proto::File,
3697 worktree: Entity<Worktree>,
3698 cx: &App,
3699 ) -> Result<Self> {
3700 let worktree_id = worktree
3701 .read(cx)
3702 .as_remote()
3703 .ok_or_else(|| anyhow!("not remote"))?
3704 .id();
3705
3706 if worktree_id.to_proto() != proto.worktree_id {
3707 return Err(anyhow!("worktree id does not match file"));
3708 }
3709
3710 let disk_state = if proto.is_deleted {
3711 DiskState::Deleted
3712 } else {
3713 if let Some(mtime) = proto.mtime.map(&Into::into) {
3714 DiskState::Present { mtime }
3715 } else {
3716 DiskState::New
3717 }
3718 };
3719
3720 Ok(Self {
3721 worktree,
3722 path: Arc::<Path>::from_proto(proto.path),
3723 disk_state,
3724 entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
3725 is_local: false,
3726 is_private: false,
3727 })
3728 }
3729
3730 pub fn from_dyn(file: Option<&Arc<dyn language::File>>) -> Option<&Self> {
3731 file.and_then(|f| f.as_any().downcast_ref())
3732 }
3733
3734 pub fn worktree_id(&self, cx: &App) -> WorktreeId {
3735 self.worktree.read(cx).id()
3736 }
3737
3738 pub fn project_entry_id(&self, _: &App) -> Option<ProjectEntryId> {
3739 match self.disk_state {
3740 DiskState::Deleted => None,
3741 _ => self.entry_id,
3742 }
3743 }
3744}
3745
3746#[derive(Clone, Debug, PartialEq, Eq)]
3747pub struct Entry {
3748 pub id: ProjectEntryId,
3749 pub kind: EntryKind,
3750 pub path: Arc<Path>,
3751 pub inode: u64,
3752 pub mtime: Option<MTime>,
3753
3754 pub canonical_path: Option<Box<Path>>,
3755 /// Whether this entry is ignored by Git.
3756 ///
3757 /// We only scan ignored entries once the directory is expanded and
3758 /// exclude them from searches.
3759 pub is_ignored: bool,
3760
3761 /// Whether this entry is always included in searches.
3762 ///
3763 /// This is used for entries that are always included in searches, even
3764 /// if they are ignored by git. Overridden by file_scan_exclusions.
3765 pub is_always_included: bool,
3766
3767 /// Whether this entry's canonical path is outside of the worktree.
3768 /// This means the entry is only accessible from the worktree root via a
3769 /// symlink.
3770 ///
3771 /// We only scan entries outside of the worktree once the symlinked
3772 /// directory is expanded. External entries are treated like gitignored
3773 /// entries in that they are not included in searches.
3774 pub is_external: bool,
3775
3776 /// Whether this entry is considered to be a `.env` file.
3777 pub is_private: bool,
3778 /// The entry's size on disk, in bytes.
3779 pub size: u64,
3780 pub char_bag: CharBag,
3781 pub is_fifo: bool,
3782}
3783
3784#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3785pub enum EntryKind {
3786 UnloadedDir,
3787 PendingDir,
3788 Dir,
3789 File,
3790}
3791
3792#[derive(Clone, Copy, Debug, PartialEq)]
3793pub enum PathChange {
3794 /// A filesystem entry was was created.
3795 Added,
3796 /// A filesystem entry was removed.
3797 Removed,
3798 /// A filesystem entry was updated.
3799 Updated,
3800 /// A filesystem entry was either updated or added. We don't know
3801 /// whether or not it already existed, because the path had not
3802 /// been loaded before the event.
3803 AddedOrUpdated,
3804 /// A filesystem entry was found during the initial scan of the worktree.
3805 Loaded,
3806}
3807
3808#[derive(Debug)]
3809pub struct GitRepositoryChange {
3810 /// The previous state of the repository, if it already existed.
3811 pub old_repository: Option<RepositoryEntry>,
3812}
3813
3814pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
3815pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, GitRepositoryChange)]>;
3816
3817#[derive(Clone, Debug, PartialEq, Eq)]
3818pub struct StatusEntry {
3819 pub repo_path: RepoPath,
3820 pub status: FileStatus,
3821}
3822
3823impl StatusEntry {
3824 pub fn is_staged(&self) -> Option<bool> {
3825 self.status.is_staged()
3826 }
3827
3828 fn to_proto(&self) -> proto::StatusEntry {
3829 let simple_status = match self.status {
3830 FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
3831 FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
3832 FileStatus::Tracked(TrackedStatus {
3833 index_status,
3834 worktree_status,
3835 }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
3836 worktree_status
3837 } else {
3838 index_status
3839 }),
3840 };
3841
3842 proto::StatusEntry {
3843 repo_path: self.repo_path.as_ref().to_proto(),
3844 simple_status,
3845 status: Some(status_to_proto(self.status)),
3846 }
3847 }
3848}
3849
3850impl TryFrom<proto::StatusEntry> for StatusEntry {
3851 type Error = anyhow::Error;
3852
3853 fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
3854 let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
3855 let status = status_from_proto(value.simple_status, value.status)?;
3856 Ok(Self { repo_path, status })
3857 }
3858}
3859
3860#[derive(Clone, Debug)]
3861struct PathProgress<'a> {
3862 max_path: &'a Path,
3863}
3864
3865#[derive(Clone, Debug)]
3866pub struct PathSummary<S> {
3867 max_path: Arc<Path>,
3868 item_summary: S,
3869}
3870
3871impl<S: Summary> Summary for PathSummary<S> {
3872 type Context = S::Context;
3873
3874 fn zero(cx: &Self::Context) -> Self {
3875 Self {
3876 max_path: Path::new("").into(),
3877 item_summary: S::zero(cx),
3878 }
3879 }
3880
3881 fn add_summary(&mut self, rhs: &Self, cx: &Self::Context) {
3882 self.max_path = rhs.max_path.clone();
3883 self.item_summary.add_summary(&rhs.item_summary, cx);
3884 }
3885}
3886
3887impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathProgress<'a> {
3888 fn zero(_: &<PathSummary<S> as Summary>::Context) -> Self {
3889 Self {
3890 max_path: Path::new(""),
3891 }
3892 }
3893
3894 fn add_summary(
3895 &mut self,
3896 summary: &'a PathSummary<S>,
3897 _: &<PathSummary<S> as Summary>::Context,
3898 ) {
3899 self.max_path = summary.max_path.as_ref()
3900 }
3901}
3902
3903impl sum_tree::Item for RepositoryEntry {
3904 type Summary = PathSummary<Unit>;
3905
3906 fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
3907 PathSummary {
3908 max_path: self.work_directory.path_key().0,
3909 item_summary: Unit,
3910 }
3911 }
3912}
3913
3914impl sum_tree::KeyedItem for RepositoryEntry {
3915 type Key = PathKey;
3916
3917 fn key(&self) -> Self::Key {
3918 self.work_directory.path_key()
3919 }
3920}
3921
3922impl sum_tree::Item for StatusEntry {
3923 type Summary = PathSummary<GitSummary>;
3924
3925 fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
3926 PathSummary {
3927 max_path: self.repo_path.0.clone(),
3928 item_summary: self.status.summary(),
3929 }
3930 }
3931}
3932
3933impl sum_tree::KeyedItem for StatusEntry {
3934 type Key = PathKey;
3935
3936 fn key(&self) -> Self::Key {
3937 PathKey(self.repo_path.0.clone())
3938 }
3939}
3940
3941impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
3942 fn zero(_cx: &()) -> Self {
3943 Default::default()
3944 }
3945
3946 fn add_summary(&mut self, summary: &'a PathSummary<GitSummary>, _: &()) {
3947 *self += summary.item_summary
3948 }
3949}
3950
3951impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathKey {
3952 fn zero(_: &S::Context) -> Self {
3953 Default::default()
3954 }
3955
3956 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: &S::Context) {
3957 self.0 = summary.max_path.clone();
3958 }
3959}
3960
3961impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for TraversalProgress<'a> {
3962 fn zero(_cx: &S::Context) -> Self {
3963 Default::default()
3964 }
3965
3966 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: &S::Context) {
3967 self.max_path = summary.max_path.as_ref();
3968 }
3969}
3970
3971impl Entry {
3972 fn new(
3973 path: Arc<Path>,
3974 metadata: &fs::Metadata,
3975 next_entry_id: &AtomicUsize,
3976 root_char_bag: CharBag,
3977 canonical_path: Option<Box<Path>>,
3978 ) -> Self {
3979 let char_bag = char_bag_for_path(root_char_bag, &path);
3980 Self {
3981 id: ProjectEntryId::new(next_entry_id),
3982 kind: if metadata.is_dir {
3983 EntryKind::PendingDir
3984 } else {
3985 EntryKind::File
3986 },
3987 path,
3988 inode: metadata.inode,
3989 mtime: Some(metadata.mtime),
3990 size: metadata.len,
3991 canonical_path,
3992 is_ignored: false,
3993 is_always_included: false,
3994 is_external: false,
3995 is_private: false,
3996 char_bag,
3997 is_fifo: metadata.is_fifo,
3998 }
3999 }
4000
4001 pub fn is_created(&self) -> bool {
4002 self.mtime.is_some()
4003 }
4004
4005 pub fn is_dir(&self) -> bool {
4006 self.kind.is_dir()
4007 }
4008
4009 pub fn is_file(&self) -> bool {
4010 self.kind.is_file()
4011 }
4012}
4013
4014impl EntryKind {
4015 pub fn is_dir(&self) -> bool {
4016 matches!(
4017 self,
4018 EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir
4019 )
4020 }
4021
4022 pub fn is_unloaded(&self) -> bool {
4023 matches!(self, EntryKind::UnloadedDir)
4024 }
4025
4026 pub fn is_file(&self) -> bool {
4027 matches!(self, EntryKind::File)
4028 }
4029}
4030
4031impl sum_tree::Item for Entry {
4032 type Summary = EntrySummary;
4033
4034 fn summary(&self, _cx: &()) -> Self::Summary {
4035 let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included
4036 {
4037 0
4038 } else {
4039 1
4040 };
4041 let file_count;
4042 let non_ignored_file_count;
4043 if self.is_file() {
4044 file_count = 1;
4045 non_ignored_file_count = non_ignored_count;
4046 } else {
4047 file_count = 0;
4048 non_ignored_file_count = 0;
4049 }
4050
4051 EntrySummary {
4052 max_path: self.path.clone(),
4053 count: 1,
4054 non_ignored_count,
4055 file_count,
4056 non_ignored_file_count,
4057 }
4058 }
4059}
4060
4061impl sum_tree::KeyedItem for Entry {
4062 type Key = PathKey;
4063
4064 fn key(&self) -> Self::Key {
4065 PathKey(self.path.clone())
4066 }
4067}
4068
4069#[derive(Clone, Debug)]
4070pub struct EntrySummary {
4071 max_path: Arc<Path>,
4072 count: usize,
4073 non_ignored_count: usize,
4074 file_count: usize,
4075 non_ignored_file_count: usize,
4076}
4077
4078impl Default for EntrySummary {
4079 fn default() -> Self {
4080 Self {
4081 max_path: Arc::from(Path::new("")),
4082 count: 0,
4083 non_ignored_count: 0,
4084 file_count: 0,
4085 non_ignored_file_count: 0,
4086 }
4087 }
4088}
4089
4090impl sum_tree::Summary for EntrySummary {
4091 type Context = ();
4092
4093 fn zero(_cx: &()) -> Self {
4094 Default::default()
4095 }
4096
4097 fn add_summary(&mut self, rhs: &Self, _: &()) {
4098 self.max_path = rhs.max_path.clone();
4099 self.count += rhs.count;
4100 self.non_ignored_count += rhs.non_ignored_count;
4101 self.file_count += rhs.file_count;
4102 self.non_ignored_file_count += rhs.non_ignored_file_count;
4103 }
4104}
4105
4106#[derive(Clone, Debug)]
4107struct PathEntry {
4108 id: ProjectEntryId,
4109 path: Arc<Path>,
4110 is_ignored: bool,
4111 scan_id: usize,
4112}
4113
4114impl sum_tree::Item for PathEntry {
4115 type Summary = PathEntrySummary;
4116
4117 fn summary(&self, _cx: &()) -> Self::Summary {
4118 PathEntrySummary { max_id: self.id }
4119 }
4120}
4121
4122impl sum_tree::KeyedItem for PathEntry {
4123 type Key = ProjectEntryId;
4124
4125 fn key(&self) -> Self::Key {
4126 self.id
4127 }
4128}
4129
4130#[derive(Clone, Debug, Default)]
4131struct PathEntrySummary {
4132 max_id: ProjectEntryId,
4133}
4134
4135impl sum_tree::Summary for PathEntrySummary {
4136 type Context = ();
4137
4138 fn zero(_cx: &Self::Context) -> Self {
4139 Default::default()
4140 }
4141
4142 fn add_summary(&mut self, summary: &Self, _: &Self::Context) {
4143 self.max_id = summary.max_id;
4144 }
4145}
4146
4147impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
4148 fn zero(_cx: &()) -> Self {
4149 Default::default()
4150 }
4151
4152 fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
4153 *self = summary.max_id;
4154 }
4155}
4156
4157#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
4158pub struct PathKey(Arc<Path>);
4159
4160impl Default for PathKey {
4161 fn default() -> Self {
4162 Self(Path::new("").into())
4163 }
4164}
4165
4166impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
4167 fn zero(_cx: &()) -> Self {
4168 Default::default()
4169 }
4170
4171 fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
4172 self.0 = summary.max_path.clone();
4173 }
4174}
4175
4176struct BackgroundScanner {
4177 state: Mutex<BackgroundScannerState>,
4178 fs: Arc<dyn Fs>,
4179 fs_case_sensitive: bool,
4180 status_updates_tx: UnboundedSender<ScanState>,
4181 executor: BackgroundExecutor,
4182 scan_requests_rx: channel::Receiver<ScanRequest>,
4183 path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
4184 next_entry_id: Arc<AtomicUsize>,
4185 phase: BackgroundScannerPhase,
4186 watcher: Arc<dyn Watcher>,
4187 settings: WorktreeSettings,
4188 share_private_files: bool,
4189}
4190
4191#[derive(PartialEq)]
4192enum BackgroundScannerPhase {
4193 InitialScan,
4194 EventsReceivedDuringInitialScan,
4195 Events,
4196}
4197
4198impl BackgroundScanner {
4199 async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>) {
4200 use futures::FutureExt as _;
4201
4202 // If the worktree root does not contain a git repository, then find
4203 // the git repository in an ancestor directory. Find any gitignore files
4204 // in ancestor directories.
4205 let root_abs_path = self.state.lock().snapshot.abs_path.clone();
4206 for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
4207 if index != 0 {
4208 if let Ok(ignore) =
4209 build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
4210 {
4211 self.state
4212 .lock()
4213 .snapshot
4214 .ignores_by_parent_abs_path
4215 .insert(ancestor.into(), (ignore.into(), false));
4216 }
4217 }
4218
4219 let ancestor_dot_git = ancestor.join(*DOT_GIT);
4220 // Check whether the directory or file called `.git` exists (in the
4221 // case of worktrees it's a file.)
4222 if self
4223 .fs
4224 .metadata(&ancestor_dot_git)
4225 .await
4226 .is_ok_and(|metadata| metadata.is_some())
4227 {
4228 if index != 0 {
4229 // We canonicalize, since the FS events use the canonicalized path.
4230 if let Some(ancestor_dot_git) =
4231 self.fs.canonicalize(&ancestor_dot_git).await.log_err()
4232 {
4233 // We associate the external git repo with our root folder and
4234 // also mark where in the git repo the root folder is located.
4235 self.state.lock().insert_git_repository_for_path(
4236 WorkDirectory::AboveProject {
4237 absolute_path: ancestor.into(),
4238 location_in_repo: root_abs_path
4239 .as_path()
4240 .strip_prefix(ancestor)
4241 .unwrap()
4242 .into(),
4243 },
4244 ancestor_dot_git.into(),
4245 self.fs.as_ref(),
4246 self.watcher.as_ref(),
4247 );
4248 };
4249 }
4250
4251 // Reached root of git repository.
4252 break;
4253 }
4254 }
4255
4256 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4257 {
4258 let mut state = self.state.lock();
4259 state.snapshot.scan_id += 1;
4260 if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
4261 let ignore_stack = state
4262 .snapshot
4263 .ignore_stack_for_abs_path(root_abs_path.as_path(), true);
4264 if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
4265 root_entry.is_ignored = true;
4266 state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
4267 }
4268 state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx);
4269 }
4270 };
4271
4272 // Perform an initial scan of the directory.
4273 drop(scan_job_tx);
4274 self.scan_dirs(true, scan_job_rx).await;
4275 {
4276 let mut state = self.state.lock();
4277 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4278 }
4279
4280 self.send_status_update(false, SmallVec::new());
4281
4282 // Process any any FS events that occurred while performing the initial scan.
4283 // For these events, update events cannot be as precise, because we didn't
4284 // have the previous state loaded yet.
4285 self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
4286 if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) {
4287 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
4288 paths.extend(more_paths);
4289 }
4290 self.process_events(paths.into_iter().map(Into::into).collect())
4291 .await;
4292 }
4293
4294 // Continue processing events until the worktree is dropped.
4295 self.phase = BackgroundScannerPhase::Events;
4296
4297 loop {
4298 select_biased! {
4299 // Process any path refresh requests from the worktree. Prioritize
4300 // these before handling changes reported by the filesystem.
4301 request = self.next_scan_request().fuse() => {
4302 let Ok(request) = request else { break };
4303 if !self.process_scan_request(request, false).await {
4304 return;
4305 }
4306 }
4307
4308 path_prefix_request = self.path_prefixes_to_scan_rx.recv().fuse() => {
4309 let Ok(request) = path_prefix_request else { break };
4310 log::trace!("adding path prefix {:?}", request.path);
4311
4312 let did_scan = self.forcibly_load_paths(&[request.path.clone()]).await;
4313 if did_scan {
4314 let abs_path =
4315 {
4316 let mut state = self.state.lock();
4317 state.path_prefixes_to_scan.insert(request.path.clone());
4318 state.snapshot.abs_path.as_path().join(&request.path)
4319 };
4320
4321 if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
4322 self.process_events(vec![abs_path]).await;
4323 }
4324 }
4325 self.send_status_update(false, request.done);
4326 }
4327
4328 paths = fs_events_rx.next().fuse() => {
4329 let Some(mut paths) = paths else { break };
4330 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
4331 paths.extend(more_paths);
4332 }
4333 self.process_events(paths.into_iter().map(Into::into).collect()).await;
4334 }
4335 }
4336 }
4337 }
4338
4339 async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
4340 log::debug!("rescanning paths {:?}", request.relative_paths);
4341
4342 request.relative_paths.sort_unstable();
4343 self.forcibly_load_paths(&request.relative_paths).await;
4344
4345 let root_path = self.state.lock().snapshot.abs_path.clone();
4346 let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
4347 Ok(path) => SanitizedPath::from(path),
4348 Err(err) => {
4349 log::error!("failed to canonicalize root path: {}", err);
4350 return true;
4351 }
4352 };
4353 let abs_paths = request
4354 .relative_paths
4355 .iter()
4356 .map(|path| {
4357 if path.file_name().is_some() {
4358 root_canonical_path.as_path().join(path).to_path_buf()
4359 } else {
4360 root_canonical_path.as_path().to_path_buf()
4361 }
4362 })
4363 .collect::<Vec<_>>();
4364
4365 {
4366 let mut state = self.state.lock();
4367 let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id;
4368 state.snapshot.scan_id += 1;
4369 if is_idle {
4370 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4371 }
4372 }
4373
4374 self.reload_entries_for_paths(
4375 root_path,
4376 root_canonical_path,
4377 &request.relative_paths,
4378 abs_paths,
4379 None,
4380 )
4381 .await;
4382
4383 self.send_status_update(scanning, request.done)
4384 }
4385
4386 async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
4387 let root_path = self.state.lock().snapshot.abs_path.clone();
4388 let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
4389 Ok(path) => SanitizedPath::from(path),
4390 Err(err) => {
4391 let new_path = self
4392 .state
4393 .lock()
4394 .snapshot
4395 .root_file_handle
4396 .clone()
4397 .and_then(|handle| handle.current_path(&self.fs).log_err())
4398 .map(SanitizedPath::from)
4399 .filter(|new_path| *new_path != root_path);
4400
4401 if let Some(new_path) = new_path.as_ref() {
4402 log::info!(
4403 "root renamed from {} to {}",
4404 root_path.as_path().display(),
4405 new_path.as_path().display()
4406 )
4407 } else {
4408 log::warn!("root path could not be canonicalized: {}", err);
4409 }
4410 self.status_updates_tx
4411 .unbounded_send(ScanState::RootUpdated { new_path })
4412 .ok();
4413 return;
4414 }
4415 };
4416
4417 // Certain directories may have FS changes, but do not lead to git data changes that Zed cares about.
4418 // Ignore these, to avoid Zed unnecessarily rescanning git metadata.
4419 let skipped_files_in_dot_git = HashSet::from_iter([*COMMIT_MESSAGE, *INDEX_LOCK]);
4420 let skipped_dirs_in_dot_git = [*FSMONITOR_DAEMON];
4421
4422 let mut relative_paths = Vec::with_capacity(abs_paths.len());
4423 let mut dot_git_abs_paths = Vec::new();
4424 abs_paths.sort_unstable();
4425 abs_paths.dedup_by(|a, b| a.starts_with(b));
4426 abs_paths.retain(|abs_path| {
4427 let abs_path = SanitizedPath::from(abs_path);
4428
4429 let snapshot = &self.state.lock().snapshot;
4430 {
4431 let mut is_git_related = false;
4432
4433 let dot_git_paths = abs_path.as_path().ancestors().find_map(|ancestor| {
4434 if smol::block_on(is_git_dir(ancestor, self.fs.as_ref())) {
4435 let path_in_git_dir = abs_path.as_path().strip_prefix(ancestor).expect("stripping off the ancestor");
4436 Some((ancestor.to_owned(), path_in_git_dir.to_owned()))
4437 } else {
4438 None
4439 }
4440 });
4441
4442 if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths {
4443 if skipped_files_in_dot_git.contains(path_in_git_dir.as_os_str()) || skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| path_in_git_dir.starts_with(skipped_git_subdir)) {
4444 log::debug!("ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories");
4445 return false;
4446 }
4447
4448 is_git_related = true;
4449 if !dot_git_abs_paths.contains(&dot_git_abs_path) {
4450 dot_git_abs_paths.push(dot_git_abs_path);
4451 }
4452 }
4453
4454 let relative_path: Arc<Path> =
4455 if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
4456 path.into()
4457 } else {
4458 if is_git_related {
4459 log::debug!(
4460 "ignoring event {abs_path:?}, since it's in git dir outside of root path {root_canonical_path:?}",
4461 );
4462 } else {
4463 log::error!(
4464 "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
4465 );
4466 }
4467 return false;
4468 };
4469
4470 if abs_path.0.file_name() == Some(*GITIGNORE) {
4471 for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
4472 if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) {
4473 dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf());
4474 }
4475 }
4476 }
4477
4478 let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
4479 snapshot
4480 .entry_for_path(parent)
4481 .map_or(false, |entry| entry.kind == EntryKind::Dir)
4482 });
4483 if !parent_dir_is_loaded {
4484 log::debug!("ignoring event {relative_path:?} within unloaded directory");
4485 return false;
4486 }
4487
4488 if self.settings.is_path_excluded(&relative_path) {
4489 if !is_git_related {
4490 log::debug!("ignoring FS event for excluded path {relative_path:?}");
4491 }
4492 return false;
4493 }
4494
4495 relative_paths.push(relative_path);
4496 true
4497 }
4498 });
4499
4500 if relative_paths.is_empty() && dot_git_abs_paths.is_empty() {
4501 return;
4502 }
4503
4504 self.state.lock().snapshot.scan_id += 1;
4505
4506 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4507 log::debug!("received fs events {:?}", relative_paths);
4508 self.reload_entries_for_paths(
4509 root_path,
4510 root_canonical_path,
4511 &relative_paths,
4512 abs_paths,
4513 Some(scan_job_tx.clone()),
4514 )
4515 .await;
4516
4517 self.update_ignore_statuses(scan_job_tx).await;
4518 self.scan_dirs(false, scan_job_rx).await;
4519
4520 if !dot_git_abs_paths.is_empty() {
4521 self.update_git_repositories(dot_git_abs_paths).await;
4522 }
4523
4524 {
4525 let mut state = self.state.lock();
4526 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4527 for (_, entry) in mem::take(&mut state.removed_entries) {
4528 state.scanned_dirs.remove(&entry.id);
4529 }
4530 }
4531
4532 #[cfg(test)]
4533 self.state.lock().snapshot.check_git_invariants();
4534
4535 self.send_status_update(false, SmallVec::new());
4536 }
4537
4538 async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
4539 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4540 {
4541 let mut state = self.state.lock();
4542 let root_path = state.snapshot.abs_path.clone();
4543 for path in paths {
4544 for ancestor in path.ancestors() {
4545 if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
4546 if entry.kind == EntryKind::UnloadedDir {
4547 let abs_path = root_path.as_path().join(ancestor);
4548 state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
4549 state.paths_to_scan.insert(path.clone());
4550 break;
4551 }
4552 }
4553 }
4554 }
4555 drop(scan_job_tx);
4556 }
4557 while let Ok(job) = scan_job_rx.recv().await {
4558 self.scan_dir(&job).await.log_err();
4559 }
4560
4561 !mem::take(&mut self.state.lock().paths_to_scan).is_empty()
4562 }
4563
4564 async fn scan_dirs(
4565 &self,
4566 enable_progress_updates: bool,
4567 scan_jobs_rx: channel::Receiver<ScanJob>,
4568 ) {
4569 use futures::FutureExt as _;
4570
4571 if self
4572 .status_updates_tx
4573 .unbounded_send(ScanState::Started)
4574 .is_err()
4575 {
4576 return;
4577 }
4578
4579 let progress_update_count = AtomicUsize::new(0);
4580 self.executor
4581 .scoped(|scope| {
4582 for _ in 0..self.executor.num_cpus() {
4583 scope.spawn(async {
4584 let mut last_progress_update_count = 0;
4585 let progress_update_timer = self.progress_timer(enable_progress_updates).fuse();
4586 futures::pin_mut!(progress_update_timer);
4587
4588 loop {
4589 select_biased! {
4590 // Process any path refresh requests before moving on to process
4591 // the scan queue, so that user operations are prioritized.
4592 request = self.next_scan_request().fuse() => {
4593 let Ok(request) = request else { break };
4594 if !self.process_scan_request(request, true).await {
4595 return;
4596 }
4597 }
4598
4599 // Send periodic progress updates to the worktree. Use an atomic counter
4600 // to ensure that only one of the workers sends a progress update after
4601 // the update interval elapses.
4602 _ = progress_update_timer => {
4603 match progress_update_count.compare_exchange(
4604 last_progress_update_count,
4605 last_progress_update_count + 1,
4606 SeqCst,
4607 SeqCst
4608 ) {
4609 Ok(_) => {
4610 last_progress_update_count += 1;
4611 self.send_status_update(true, SmallVec::new());
4612 }
4613 Err(count) => {
4614 last_progress_update_count = count;
4615 }
4616 }
4617 progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse());
4618 }
4619
4620 // Recursively load directories from the file system.
4621 job = scan_jobs_rx.recv().fuse() => {
4622 let Ok(job) = job else { break };
4623 if let Err(err) = self.scan_dir(&job).await {
4624 if job.path.as_ref() != Path::new("") {
4625 log::error!("error scanning directory {:?}: {}", job.abs_path, err);
4626 }
4627 }
4628 }
4629 }
4630 }
4631 })
4632 }
4633 })
4634 .await;
4635 }
4636
4637 fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
4638 let mut state = self.state.lock();
4639 if state.changed_paths.is_empty() && scanning {
4640 return true;
4641 }
4642
4643 let new_snapshot = state.snapshot.clone();
4644 let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
4645 let changes = self.build_change_set(&old_snapshot, &new_snapshot, &state.changed_paths);
4646 state.changed_paths.clear();
4647
4648 self.status_updates_tx
4649 .unbounded_send(ScanState::Updated {
4650 snapshot: new_snapshot,
4651 changes,
4652 scanning,
4653 barrier,
4654 })
4655 .is_ok()
4656 }
4657
4658 async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
4659 let root_abs_path;
4660 let root_char_bag;
4661 {
4662 let snapshot = &self.state.lock().snapshot;
4663 if self.settings.is_path_excluded(&job.path) {
4664 log::error!("skipping excluded directory {:?}", job.path);
4665 return Ok(());
4666 }
4667 log::debug!("scanning directory {:?}", job.path);
4668 root_abs_path = snapshot.abs_path().clone();
4669 root_char_bag = snapshot.root_char_bag;
4670 }
4671
4672 let next_entry_id = self.next_entry_id.clone();
4673 let mut ignore_stack = job.ignore_stack.clone();
4674 let mut new_ignore = None;
4675 let mut root_canonical_path = None;
4676 let mut new_entries: Vec<Entry> = Vec::new();
4677 let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
4678 let mut child_paths = self
4679 .fs
4680 .read_dir(&job.abs_path)
4681 .await?
4682 .filter_map(|entry| async {
4683 match entry {
4684 Ok(entry) => Some(entry),
4685 Err(error) => {
4686 log::error!("error processing entry {:?}", error);
4687 None
4688 }
4689 }
4690 })
4691 .collect::<Vec<_>>()
4692 .await;
4693
4694 // Ensure that .git and .gitignore are processed first.
4695 swap_to_front(&mut child_paths, *GITIGNORE);
4696 swap_to_front(&mut child_paths, *DOT_GIT);
4697
4698 for child_abs_path in child_paths {
4699 let child_abs_path: Arc<Path> = child_abs_path.into();
4700 let child_name = child_abs_path.file_name().unwrap();
4701 let child_path: Arc<Path> = job.path.join(child_name).into();
4702
4703 if child_name == *DOT_GIT {
4704 let repo = self.state.lock().insert_git_repository(
4705 child_path.clone(),
4706 self.fs.as_ref(),
4707 self.watcher.as_ref(),
4708 );
4709
4710 if let Some(local_repo) = repo {
4711 self.update_git_statuses(UpdateGitStatusesJob {
4712 local_repository: local_repo,
4713 });
4714 }
4715 } else if child_name == *GITIGNORE {
4716 match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
4717 Ok(ignore) => {
4718 let ignore = Arc::new(ignore);
4719 ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
4720 new_ignore = Some(ignore);
4721 }
4722 Err(error) => {
4723 log::error!(
4724 "error loading .gitignore file {:?} - {:?}",
4725 child_name,
4726 error
4727 );
4728 }
4729 }
4730 }
4731
4732 if self.settings.is_path_excluded(&child_path) {
4733 log::debug!("skipping excluded child entry {child_path:?}");
4734 self.state.lock().remove_path(&child_path);
4735 continue;
4736 }
4737
4738 let child_metadata = match self.fs.metadata(&child_abs_path).await {
4739 Ok(Some(metadata)) => metadata,
4740 Ok(None) => continue,
4741 Err(err) => {
4742 log::error!("error processing {child_abs_path:?}: {err:?}");
4743 continue;
4744 }
4745 };
4746
4747 let mut child_entry = Entry::new(
4748 child_path.clone(),
4749 &child_metadata,
4750 &next_entry_id,
4751 root_char_bag,
4752 None,
4753 );
4754
4755 if job.is_external {
4756 child_entry.is_external = true;
4757 } else if child_metadata.is_symlink {
4758 let canonical_path = match self.fs.canonicalize(&child_abs_path).await {
4759 Ok(path) => path,
4760 Err(err) => {
4761 log::error!(
4762 "error reading target of symlink {:?}: {:?}",
4763 child_abs_path,
4764 err
4765 );
4766 continue;
4767 }
4768 };
4769
4770 // lazily canonicalize the root path in order to determine if
4771 // symlinks point outside of the worktree.
4772 let root_canonical_path = match &root_canonical_path {
4773 Some(path) => path,
4774 None => match self.fs.canonicalize(&root_abs_path).await {
4775 Ok(path) => root_canonical_path.insert(path),
4776 Err(err) => {
4777 log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err);
4778 continue;
4779 }
4780 },
4781 };
4782
4783 if !canonical_path.starts_with(root_canonical_path) {
4784 child_entry.is_external = true;
4785 }
4786
4787 child_entry.canonical_path = Some(canonical_path.into());
4788 }
4789
4790 if child_entry.is_dir() {
4791 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
4792 child_entry.is_always_included = self.settings.is_path_always_included(&child_path);
4793
4794 // Avoid recursing until crash in the case of a recursive symlink
4795 if job.ancestor_inodes.contains(&child_entry.inode) {
4796 new_jobs.push(None);
4797 } else {
4798 let mut ancestor_inodes = job.ancestor_inodes.clone();
4799 ancestor_inodes.insert(child_entry.inode);
4800
4801 new_jobs.push(Some(ScanJob {
4802 abs_path: child_abs_path.clone(),
4803 path: child_path,
4804 is_external: child_entry.is_external,
4805 ignore_stack: if child_entry.is_ignored {
4806 IgnoreStack::all()
4807 } else {
4808 ignore_stack.clone()
4809 },
4810 ancestor_inodes,
4811 scan_queue: job.scan_queue.clone(),
4812 }));
4813 }
4814 } else {
4815 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
4816 child_entry.is_always_included = self.settings.is_path_always_included(&child_path);
4817 }
4818
4819 {
4820 let relative_path = job.path.join(child_name);
4821 if self.is_path_private(&relative_path) {
4822 log::debug!("detected private file: {relative_path:?}");
4823 child_entry.is_private = true;
4824 }
4825 }
4826
4827 new_entries.push(child_entry);
4828 }
4829
4830 let mut state = self.state.lock();
4831
4832 // Identify any subdirectories that should not be scanned.
4833 let mut job_ix = 0;
4834 for entry in &mut new_entries {
4835 state.reuse_entry_id(entry);
4836 if entry.is_dir() {
4837 if state.should_scan_directory(entry) {
4838 job_ix += 1;
4839 } else {
4840 log::debug!("defer scanning directory {:?}", entry.path);
4841 entry.kind = EntryKind::UnloadedDir;
4842 new_jobs.remove(job_ix);
4843 }
4844 }
4845 if entry.is_always_included {
4846 state
4847 .snapshot
4848 .always_included_entries
4849 .push(entry.path.clone());
4850 }
4851 }
4852
4853 state.populate_dir(&job.path, new_entries, new_ignore);
4854 self.watcher.add(job.abs_path.as_ref()).log_err();
4855
4856 for new_job in new_jobs.into_iter().flatten() {
4857 job.scan_queue
4858 .try_send(new_job)
4859 .expect("channel is unbounded");
4860 }
4861
4862 Ok(())
4863 }
4864
4865 /// All list arguments should be sorted before calling this function
4866 async fn reload_entries_for_paths(
4867 &self,
4868 root_abs_path: SanitizedPath,
4869 root_canonical_path: SanitizedPath,
4870 relative_paths: &[Arc<Path>],
4871 abs_paths: Vec<PathBuf>,
4872 scan_queue_tx: Option<Sender<ScanJob>>,
4873 ) {
4874 // grab metadata for all requested paths
4875 let metadata = futures::future::join_all(
4876 abs_paths
4877 .iter()
4878 .map(|abs_path| async move {
4879 let metadata = self.fs.metadata(abs_path).await?;
4880 if let Some(metadata) = metadata {
4881 let canonical_path = self.fs.canonicalize(abs_path).await?;
4882
4883 // If we're on a case-insensitive filesystem (default on macOS), we want
4884 // to only ignore metadata for non-symlink files if their absolute-path matches
4885 // the canonical-path.
4886 // Because if not, this might be a case-only-renaming (`mv test.txt TEST.TXT`)
4887 // and we want to ignore the metadata for the old path (`test.txt`) so it's
4888 // treated as removed.
4889 if !self.fs_case_sensitive && !metadata.is_symlink {
4890 let canonical_file_name = canonical_path.file_name();
4891 let file_name = abs_path.file_name();
4892 if canonical_file_name != file_name {
4893 return Ok(None);
4894 }
4895 }
4896
4897 anyhow::Ok(Some((metadata, SanitizedPath::from(canonical_path))))
4898 } else {
4899 Ok(None)
4900 }
4901 })
4902 .collect::<Vec<_>>(),
4903 )
4904 .await;
4905
4906 let mut state = self.state.lock();
4907 let doing_recursive_update = scan_queue_tx.is_some();
4908
4909 // Remove any entries for paths that no longer exist or are being recursively
4910 // refreshed. Do this before adding any new entries, so that renames can be
4911 // detected regardless of the order of the paths.
4912 for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
4913 if matches!(metadata, Ok(None)) || doing_recursive_update {
4914 log::trace!("remove path {:?}", path);
4915 state.remove_path(path);
4916 }
4917 }
4918
4919 // Group all relative paths by their git repository.
4920 let mut paths_by_git_repo = HashMap::default();
4921 for relative_path in relative_paths.iter() {
4922 let repository_data = state
4923 .snapshot
4924 .local_repo_for_path(relative_path)
4925 .zip(state.snapshot.repository_for_path(relative_path));
4926 if let Some((local_repo, entry)) = repository_data {
4927 if let Ok(repo_path) = local_repo.relativize(relative_path) {
4928 paths_by_git_repo
4929 .entry(local_repo.work_directory.clone())
4930 .or_insert_with(|| RepoPaths {
4931 entry: entry.clone(),
4932 repo: local_repo.repo_ptr.clone(),
4933 repo_paths: Default::default(),
4934 })
4935 .add_path(repo_path);
4936 }
4937 }
4938 }
4939
4940 for (work_directory, mut paths) in paths_by_git_repo {
4941 if let Ok(status) = paths.repo.status(&paths.repo_paths) {
4942 let mut changed_path_statuses = Vec::new();
4943 let statuses = paths.entry.statuses_by_path.clone();
4944 let mut cursor = statuses.cursor::<PathProgress>(&());
4945
4946 for (repo_path, status) in &*status.entries {
4947 paths.remove_repo_path(repo_path);
4948 if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left, &()) {
4949 if &cursor.item().unwrap().status == status {
4950 continue;
4951 }
4952 }
4953
4954 changed_path_statuses.push(Edit::Insert(StatusEntry {
4955 repo_path: repo_path.clone(),
4956 status: *status,
4957 }));
4958 }
4959
4960 let mut cursor = statuses.cursor::<PathProgress>(&());
4961 for path in paths.repo_paths {
4962 if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left, &()) {
4963 changed_path_statuses.push(Edit::Remove(PathKey(path.0)));
4964 }
4965 }
4966
4967 if !changed_path_statuses.is_empty() {
4968 let work_directory_id = state.snapshot.repositories.update(
4969 &work_directory.path_key(),
4970 &(),
4971 move |repository_entry| {
4972 repository_entry
4973 .statuses_by_path
4974 .edit(changed_path_statuses, &());
4975
4976 repository_entry.work_directory_id
4977 },
4978 );
4979
4980 if let Some(work_directory_id) = work_directory_id {
4981 let scan_id = state.snapshot.scan_id;
4982 state.snapshot.git_repositories.update(
4983 &work_directory_id,
4984 |local_repository_entry| {
4985 local_repository_entry.status_scan_id = scan_id;
4986 },
4987 );
4988 }
4989 }
4990 }
4991 }
4992
4993 for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) {
4994 let abs_path: Arc<Path> = root_abs_path.as_path().join(path).into();
4995 match metadata {
4996 Ok(Some((metadata, canonical_path))) => {
4997 let ignore_stack = state
4998 .snapshot
4999 .ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
5000 let is_external = !canonical_path.starts_with(&root_canonical_path);
5001 let mut fs_entry = Entry::new(
5002 path.clone(),
5003 &metadata,
5004 self.next_entry_id.as_ref(),
5005 state.snapshot.root_char_bag,
5006 if metadata.is_symlink {
5007 Some(canonical_path.as_path().to_path_buf().into())
5008 } else {
5009 None
5010 },
5011 );
5012
5013 let is_dir = fs_entry.is_dir();
5014 fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
5015 fs_entry.is_external = is_external;
5016 fs_entry.is_private = self.is_path_private(path);
5017 fs_entry.is_always_included = self.settings.is_path_always_included(path);
5018
5019 if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
5020 if state.should_scan_directory(&fs_entry)
5021 || (fs_entry.path.as_os_str().is_empty()
5022 && abs_path.file_name() == Some(*DOT_GIT))
5023 {
5024 state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx);
5025 } else {
5026 fs_entry.kind = EntryKind::UnloadedDir;
5027 }
5028 }
5029
5030 state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
5031 }
5032 Ok(None) => {
5033 self.remove_repo_path(path, &mut state.snapshot);
5034 }
5035 Err(err) => {
5036 log::error!("error reading file {abs_path:?} on event: {err:#}");
5037 }
5038 }
5039 }
5040
5041 util::extend_sorted(
5042 &mut state.changed_paths,
5043 relative_paths.iter().cloned(),
5044 usize::MAX,
5045 Ord::cmp,
5046 );
5047 }
5048
5049 fn remove_repo_path(&self, path: &Arc<Path>, snapshot: &mut LocalSnapshot) -> Option<()> {
5050 if !path
5051 .components()
5052 .any(|component| component.as_os_str() == *DOT_GIT)
5053 {
5054 if let Some(repository) = snapshot.repository(PathKey(path.clone())) {
5055 snapshot
5056 .git_repositories
5057 .remove(&repository.work_directory_id);
5058 snapshot
5059 .snapshot
5060 .repositories
5061 .remove(&repository.work_directory.path_key(), &());
5062 return Some(());
5063 }
5064 }
5065
5066 Some(())
5067 }
5068
5069 async fn update_ignore_statuses(&self, scan_job_tx: Sender<ScanJob>) {
5070 use futures::FutureExt as _;
5071
5072 let mut ignores_to_update = Vec::new();
5073 let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
5074 let prev_snapshot;
5075 {
5076 let snapshot = &mut self.state.lock().snapshot;
5077 let abs_path = snapshot.abs_path.clone();
5078 snapshot
5079 .ignores_by_parent_abs_path
5080 .retain(|parent_abs_path, (_, needs_update)| {
5081 if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) {
5082 if *needs_update {
5083 *needs_update = false;
5084 if snapshot.snapshot.entry_for_path(parent_path).is_some() {
5085 ignores_to_update.push(parent_abs_path.clone());
5086 }
5087 }
5088
5089 let ignore_path = parent_path.join(*GITIGNORE);
5090 if snapshot.snapshot.entry_for_path(ignore_path).is_none() {
5091 return false;
5092 }
5093 }
5094 true
5095 });
5096
5097 ignores_to_update.sort_unstable();
5098 let mut ignores_to_update = ignores_to_update.into_iter().peekable();
5099 while let Some(parent_abs_path) = ignores_to_update.next() {
5100 while ignores_to_update
5101 .peek()
5102 .map_or(false, |p| p.starts_with(&parent_abs_path))
5103 {
5104 ignores_to_update.next().unwrap();
5105 }
5106
5107 let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
5108 ignore_queue_tx
5109 .send_blocking(UpdateIgnoreStatusJob {
5110 abs_path: parent_abs_path,
5111 ignore_stack,
5112 ignore_queue: ignore_queue_tx.clone(),
5113 scan_queue: scan_job_tx.clone(),
5114 })
5115 .unwrap();
5116 }
5117
5118 prev_snapshot = snapshot.clone();
5119 }
5120 drop(ignore_queue_tx);
5121
5122 self.executor
5123 .scoped(|scope| {
5124 for _ in 0..self.executor.num_cpus() {
5125 scope.spawn(async {
5126 loop {
5127 select_biased! {
5128 // Process any path refresh requests before moving on to process
5129 // the queue of ignore statuses.
5130 request = self.next_scan_request().fuse() => {
5131 let Ok(request) = request else { break };
5132 if !self.process_scan_request(request, true).await {
5133 return;
5134 }
5135 }
5136
5137 // Recursively process directories whose ignores have changed.
5138 job = ignore_queue_rx.recv().fuse() => {
5139 let Ok(job) = job else { break };
5140 self.update_ignore_status(job, &prev_snapshot).await;
5141 }
5142 }
5143 }
5144 });
5145 }
5146 })
5147 .await;
5148 }
5149
5150 async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
5151 log::trace!("update ignore status {:?}", job.abs_path);
5152
5153 let mut ignore_stack = job.ignore_stack;
5154 if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
5155 ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
5156 }
5157
5158 let mut entries_by_id_edits = Vec::new();
5159 let mut entries_by_path_edits = Vec::new();
5160 let path = job
5161 .abs_path
5162 .strip_prefix(snapshot.abs_path.as_path())
5163 .unwrap();
5164
5165 for mut entry in snapshot.child_entries(path).cloned() {
5166 let was_ignored = entry.is_ignored;
5167 let abs_path: Arc<Path> = snapshot.abs_path().join(&entry.path).into();
5168 entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
5169
5170 if entry.is_dir() {
5171 let child_ignore_stack = if entry.is_ignored {
5172 IgnoreStack::all()
5173 } else {
5174 ignore_stack.clone()
5175 };
5176
5177 // Scan any directories that were previously ignored and weren't previously scanned.
5178 if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
5179 let state = self.state.lock();
5180 if state.should_scan_directory(&entry) {
5181 state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue);
5182 }
5183 }
5184
5185 job.ignore_queue
5186 .send(UpdateIgnoreStatusJob {
5187 abs_path: abs_path.clone(),
5188 ignore_stack: child_ignore_stack,
5189 ignore_queue: job.ignore_queue.clone(),
5190 scan_queue: job.scan_queue.clone(),
5191 })
5192 .await
5193 .unwrap();
5194 }
5195
5196 if entry.is_ignored != was_ignored {
5197 let mut path_entry = snapshot.entries_by_id.get(&entry.id, &()).unwrap().clone();
5198 path_entry.scan_id = snapshot.scan_id;
5199 path_entry.is_ignored = entry.is_ignored;
5200 entries_by_id_edits.push(Edit::Insert(path_entry));
5201 entries_by_path_edits.push(Edit::Insert(entry));
5202 }
5203 }
5204
5205 let state = &mut self.state.lock();
5206 for edit in &entries_by_path_edits {
5207 if let Edit::Insert(entry) = edit {
5208 if let Err(ix) = state.changed_paths.binary_search(&entry.path) {
5209 state.changed_paths.insert(ix, entry.path.clone());
5210 }
5211 }
5212 }
5213
5214 state
5215 .snapshot
5216 .entries_by_path
5217 .edit(entries_by_path_edits, &());
5218 state.snapshot.entries_by_id.edit(entries_by_id_edits, &());
5219 }
5220
5221 async fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) {
5222 log::debug!("reloading repositories: {dot_git_paths:?}");
5223
5224 let mut repo_updates = Vec::new();
5225 {
5226 let mut state = self.state.lock();
5227 let scan_id = state.snapshot.scan_id;
5228 for dot_git_dir in dot_git_paths {
5229 let existing_repository_entry =
5230 state
5231 .snapshot
5232 .git_repositories
5233 .iter()
5234 .find_map(|(_, repo)| {
5235 if repo.dot_git_dir_abs_path.as_ref() == &dot_git_dir
5236 || repo.dot_git_worktree_abs_path.as_deref() == Some(&dot_git_dir)
5237 {
5238 Some(repo.clone())
5239 } else {
5240 None
5241 }
5242 });
5243
5244 let local_repository = match existing_repository_entry {
5245 None => {
5246 let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path())
5247 else {
5248 return;
5249 };
5250 match state.insert_git_repository(
5251 relative.into(),
5252 self.fs.as_ref(),
5253 self.watcher.as_ref(),
5254 ) {
5255 Some(output) => output,
5256 None => continue,
5257 }
5258 }
5259 Some(local_repository) => {
5260 if local_repository.git_dir_scan_id == scan_id {
5261 continue;
5262 }
5263 let Some(work_dir) = state
5264 .snapshot
5265 .entry_for_id(local_repository.work_directory_id)
5266 .map(|entry| entry.path.clone())
5267 else {
5268 continue;
5269 };
5270
5271 let branch = local_repository.repo_ptr.branch_name();
5272 local_repository.repo_ptr.reload_index();
5273
5274 state.snapshot.git_repositories.update(
5275 &local_repository.work_directory_id,
5276 |entry| {
5277 entry.git_dir_scan_id = scan_id;
5278 entry.status_scan_id = scan_id;
5279 },
5280 );
5281 state.snapshot.snapshot.repositories.update(
5282 &PathKey(work_dir.clone()),
5283 &(),
5284 |entry| entry.branch = branch.map(Into::into),
5285 );
5286
5287 local_repository
5288 }
5289 };
5290
5291 repo_updates.push(UpdateGitStatusesJob { local_repository });
5292 }
5293
5294 // Remove any git repositories whose .git entry no longer exists.
5295 let snapshot = &mut state.snapshot;
5296 let mut ids_to_preserve = HashSet::default();
5297 for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
5298 let exists_in_snapshot = snapshot
5299 .entry_for_id(work_directory_id)
5300 .map_or(false, |entry| {
5301 snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
5302 });
5303
5304 if exists_in_snapshot
5305 || matches!(
5306 smol::block_on(self.fs.metadata(&entry.dot_git_dir_abs_path)),
5307 Ok(Some(_))
5308 )
5309 {
5310 ids_to_preserve.insert(work_directory_id);
5311 }
5312 }
5313
5314 snapshot
5315 .git_repositories
5316 .retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
5317 snapshot.repositories.retain(&(), |entry| {
5318 ids_to_preserve.contains(&entry.work_directory_id)
5319 });
5320 }
5321
5322 let (mut updates_done_tx, mut updates_done_rx) = barrier::channel();
5323 self.executor
5324 .scoped(|scope| {
5325 scope.spawn(async {
5326 for repo_update in repo_updates {
5327 self.update_git_statuses(repo_update);
5328 }
5329 updates_done_tx.blocking_send(()).ok();
5330 });
5331
5332 scope.spawn(async {
5333 loop {
5334 select_biased! {
5335 // Process any path refresh requests before moving on to process
5336 // the queue of git statuses.
5337 request = self.next_scan_request().fuse() => {
5338 let Ok(request) = request else { break };
5339 if !self.process_scan_request(request, true).await {
5340 return;
5341 }
5342 }
5343 _ = updates_done_rx.recv().fuse() => break,
5344 }
5345 }
5346 });
5347 })
5348 .await;
5349 }
5350
5351 /// Update the git statuses for a given batch of entries.
5352 fn update_git_statuses(&self, job: UpdateGitStatusesJob) {
5353 log::trace!(
5354 "updating git statuses for repo {:?}",
5355 job.local_repository.work_directory.display_name()
5356 );
5357 let t0 = Instant::now();
5358
5359 let Some(statuses) = job
5360 .local_repository
5361 .repo()
5362 .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
5363 .log_err()
5364 else {
5365 return;
5366 };
5367 log::trace!(
5368 "computed git statuses for repo {:?} in {:?}",
5369 job.local_repository.work_directory.display_name(),
5370 t0.elapsed()
5371 );
5372
5373 let t0 = Instant::now();
5374 let mut changed_paths = Vec::new();
5375 let snapshot = self.state.lock().snapshot.snapshot.clone();
5376
5377 let Some(mut repository) =
5378 snapshot.repository(job.local_repository.work_directory.path_key())
5379 else {
5380 // happens when a folder is deleted
5381 log::debug!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot");
5382 return;
5383 };
5384
5385 let merge_head_shas = job.local_repository.repo().merge_head_shas();
5386 if merge_head_shas != job.local_repository.current_merge_head_shas {
5387 mem::take(&mut repository.current_merge_conflicts);
5388 }
5389
5390 let mut new_entries_by_path = SumTree::new(&());
5391 for (repo_path, status) in statuses.entries.iter() {
5392 let project_path = repository.work_directory.unrelativize(repo_path);
5393
5394 new_entries_by_path.insert_or_replace(
5395 StatusEntry {
5396 repo_path: repo_path.clone(),
5397 status: *status,
5398 },
5399 &(),
5400 );
5401 if status.is_conflicted() {
5402 repository.current_merge_conflicts.insert(repo_path.clone());
5403 }
5404
5405 if let Some(path) = project_path {
5406 changed_paths.push(path);
5407 }
5408 }
5409
5410 repository.statuses_by_path = new_entries_by_path;
5411 let mut state = self.state.lock();
5412 state
5413 .snapshot
5414 .repositories
5415 .insert_or_replace(repository, &());
5416
5417 state
5418 .snapshot
5419 .git_repositories
5420 .update(&job.local_repository.work_directory_id, |entry| {
5421 entry.current_merge_head_shas = merge_head_shas;
5422 });
5423
5424 util::extend_sorted(
5425 &mut state.changed_paths,
5426 changed_paths,
5427 usize::MAX,
5428 Ord::cmp,
5429 );
5430
5431 log::trace!(
5432 "applied git status updates for repo {:?} in {:?}",
5433 job.local_repository.work_directory.display_name(),
5434 t0.elapsed(),
5435 );
5436 }
5437
5438 fn build_change_set(
5439 &self,
5440 old_snapshot: &Snapshot,
5441 new_snapshot: &Snapshot,
5442 event_paths: &[Arc<Path>],
5443 ) -> UpdatedEntriesSet {
5444 use BackgroundScannerPhase::*;
5445 use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated};
5446
5447 // Identify which paths have changed. Use the known set of changed
5448 // parent paths to optimize the search.
5449 let mut changes = Vec::new();
5450 let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>(&());
5451 let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>(&());
5452 let mut last_newly_loaded_dir_path = None;
5453 old_paths.next(&());
5454 new_paths.next(&());
5455 for path in event_paths {
5456 let path = PathKey(path.clone());
5457 if old_paths.item().map_or(false, |e| e.path < path.0) {
5458 old_paths.seek_forward(&path, Bias::Left, &());
5459 }
5460 if new_paths.item().map_or(false, |e| e.path < path.0) {
5461 new_paths.seek_forward(&path, Bias::Left, &());
5462 }
5463 loop {
5464 match (old_paths.item(), new_paths.item()) {
5465 (Some(old_entry), Some(new_entry)) => {
5466 if old_entry.path > path.0
5467 && new_entry.path > path.0
5468 && !old_entry.path.starts_with(&path.0)
5469 && !new_entry.path.starts_with(&path.0)
5470 {
5471 break;
5472 }
5473
5474 match Ord::cmp(&old_entry.path, &new_entry.path) {
5475 Ordering::Less => {
5476 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5477 old_paths.next(&());
5478 }
5479 Ordering::Equal => {
5480 if self.phase == EventsReceivedDuringInitialScan {
5481 if old_entry.id != new_entry.id {
5482 changes.push((
5483 old_entry.path.clone(),
5484 old_entry.id,
5485 Removed,
5486 ));
5487 }
5488 // If the worktree was not fully initialized when this event was generated,
5489 // we can't know whether this entry was added during the scan or whether
5490 // it was merely updated.
5491 changes.push((
5492 new_entry.path.clone(),
5493 new_entry.id,
5494 AddedOrUpdated,
5495 ));
5496 } else if old_entry.id != new_entry.id {
5497 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5498 changes.push((new_entry.path.clone(), new_entry.id, Added));
5499 } else if old_entry != new_entry {
5500 if old_entry.kind.is_unloaded() {
5501 last_newly_loaded_dir_path = Some(&new_entry.path);
5502 changes.push((
5503 new_entry.path.clone(),
5504 new_entry.id,
5505 Loaded,
5506 ));
5507 } else {
5508 changes.push((
5509 new_entry.path.clone(),
5510 new_entry.id,
5511 Updated,
5512 ));
5513 }
5514 }
5515 old_paths.next(&());
5516 new_paths.next(&());
5517 }
5518 Ordering::Greater => {
5519 let is_newly_loaded = self.phase == InitialScan
5520 || last_newly_loaded_dir_path
5521 .as_ref()
5522 .map_or(false, |dir| new_entry.path.starts_with(dir));
5523 changes.push((
5524 new_entry.path.clone(),
5525 new_entry.id,
5526 if is_newly_loaded { Loaded } else { Added },
5527 ));
5528 new_paths.next(&());
5529 }
5530 }
5531 }
5532 (Some(old_entry), None) => {
5533 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5534 old_paths.next(&());
5535 }
5536 (None, Some(new_entry)) => {
5537 let is_newly_loaded = self.phase == InitialScan
5538 || last_newly_loaded_dir_path
5539 .as_ref()
5540 .map_or(false, |dir| new_entry.path.starts_with(dir));
5541 changes.push((
5542 new_entry.path.clone(),
5543 new_entry.id,
5544 if is_newly_loaded { Loaded } else { Added },
5545 ));
5546 new_paths.next(&());
5547 }
5548 (None, None) => break,
5549 }
5550 }
5551 }
5552
5553 changes.into()
5554 }
5555
5556 async fn progress_timer(&self, running: bool) {
5557 if !running {
5558 return futures::future::pending().await;
5559 }
5560
5561 #[cfg(any(test, feature = "test-support"))]
5562 if self.fs.is_fake() {
5563 return self.executor.simulate_random_delay().await;
5564 }
5565
5566 smol::Timer::after(FS_WATCH_LATENCY).await;
5567 }
5568
5569 fn is_path_private(&self, path: &Path) -> bool {
5570 !self.share_private_files && self.settings.is_path_private(path)
5571 }
5572
5573 async fn next_scan_request(&self) -> Result<ScanRequest> {
5574 let mut request = self.scan_requests_rx.recv().await?;
5575 while let Ok(next_request) = self.scan_requests_rx.try_recv() {
5576 request.relative_paths.extend(next_request.relative_paths);
5577 request.done.extend(next_request.done);
5578 }
5579 Ok(request)
5580 }
5581}
5582
5583fn swap_to_front(child_paths: &mut Vec<PathBuf>, file: &OsStr) {
5584 let position = child_paths
5585 .iter()
5586 .position(|path| path.file_name().unwrap() == file);
5587 if let Some(position) = position {
5588 let temp = child_paths.remove(position);
5589 child_paths.insert(0, temp);
5590 }
5591}
5592
5593fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
5594 let mut result = root_char_bag;
5595 result.extend(
5596 path.to_string_lossy()
5597 .chars()
5598 .map(|c| c.to_ascii_lowercase()),
5599 );
5600 result
5601}
5602
5603#[derive(Debug)]
5604struct RepoPaths {
5605 repo: Arc<dyn GitRepository>,
5606 entry: RepositoryEntry,
5607 // sorted
5608 repo_paths: Vec<RepoPath>,
5609}
5610
5611impl RepoPaths {
5612 fn add_path(&mut self, repo_path: RepoPath) {
5613 match self.repo_paths.binary_search(&repo_path) {
5614 Ok(_) => {}
5615 Err(ix) => self.repo_paths.insert(ix, repo_path),
5616 }
5617 }
5618
5619 fn remove_repo_path(&mut self, repo_path: &RepoPath) {
5620 match self.repo_paths.binary_search(&repo_path) {
5621 Ok(ix) => {
5622 self.repo_paths.remove(ix);
5623 }
5624 Err(_) => {}
5625 }
5626 }
5627}
5628
5629struct ScanJob {
5630 abs_path: Arc<Path>,
5631 path: Arc<Path>,
5632 ignore_stack: Arc<IgnoreStack>,
5633 scan_queue: Sender<ScanJob>,
5634 ancestor_inodes: TreeSet<u64>,
5635 is_external: bool,
5636}
5637
5638struct UpdateIgnoreStatusJob {
5639 abs_path: Arc<Path>,
5640 ignore_stack: Arc<IgnoreStack>,
5641 ignore_queue: Sender<UpdateIgnoreStatusJob>,
5642 scan_queue: Sender<ScanJob>,
5643}
5644
5645struct UpdateGitStatusesJob {
5646 local_repository: LocalRepositoryEntry,
5647}
5648
5649pub trait WorktreeModelHandle {
5650 #[cfg(any(test, feature = "test-support"))]
5651 fn flush_fs_events<'a>(
5652 &self,
5653 cx: &'a mut gpui::TestAppContext,
5654 ) -> futures::future::LocalBoxFuture<'a, ()>;
5655
5656 #[cfg(any(test, feature = "test-support"))]
5657 fn flush_fs_events_in_root_git_repository<'a>(
5658 &self,
5659 cx: &'a mut gpui::TestAppContext,
5660 ) -> futures::future::LocalBoxFuture<'a, ()>;
5661}
5662
5663impl WorktreeModelHandle for Entity<Worktree> {
5664 // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
5665 // occurred before the worktree was constructed. These events can cause the worktree to perform
5666 // extra directory scans, and emit extra scan-state notifications.
5667 //
5668 // This function mutates the worktree's directory and waits for those mutations to be picked up,
5669 // to ensure that all redundant FS events have already been processed.
5670 #[cfg(any(test, feature = "test-support"))]
5671 fn flush_fs_events<'a>(
5672 &self,
5673 cx: &'a mut gpui::TestAppContext,
5674 ) -> futures::future::LocalBoxFuture<'a, ()> {
5675 let file_name = "fs-event-sentinel";
5676
5677 let tree = self.clone();
5678 let (fs, root_path) = self.update(cx, |tree, _| {
5679 let tree = tree.as_local().unwrap();
5680 (tree.fs.clone(), tree.abs_path().clone())
5681 });
5682
5683 async move {
5684 fs.create_file(&root_path.join(file_name), Default::default())
5685 .await
5686 .unwrap();
5687
5688 cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_some())
5689 .await;
5690
5691 fs.remove_file(&root_path.join(file_name), Default::default())
5692 .await
5693 .unwrap();
5694 cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_none())
5695 .await;
5696
5697 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5698 .await;
5699 }
5700 .boxed_local()
5701 }
5702
5703 // This function is similar to flush_fs_events, except that it waits for events to be flushed in
5704 // the .git folder of the root repository.
5705 // The reason for its existence is that a repository's .git folder might live *outside* of the
5706 // worktree and thus its FS events might go through a different path.
5707 // In order to flush those, we need to create artificial events in the .git folder and wait
5708 // for the repository to be reloaded.
5709 #[cfg(any(test, feature = "test-support"))]
5710 fn flush_fs_events_in_root_git_repository<'a>(
5711 &self,
5712 cx: &'a mut gpui::TestAppContext,
5713 ) -> futures::future::LocalBoxFuture<'a, ()> {
5714 let file_name = "fs-event-sentinel";
5715
5716 let tree = self.clone();
5717 let (fs, root_path, mut git_dir_scan_id) = self.update(cx, |tree, _| {
5718 let tree = tree.as_local().unwrap();
5719 let root_entry = tree.root_git_entry().unwrap();
5720 let local_repo_entry = tree.get_local_repo(&root_entry).unwrap();
5721 (
5722 tree.fs.clone(),
5723 local_repo_entry.dot_git_dir_abs_path.clone(),
5724 local_repo_entry.git_dir_scan_id,
5725 )
5726 });
5727
5728 let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| {
5729 let root_entry = tree.root_git_entry().unwrap();
5730 let local_repo_entry = tree
5731 .as_local()
5732 .unwrap()
5733 .get_local_repo(&root_entry)
5734 .unwrap();
5735
5736 if local_repo_entry.git_dir_scan_id > *git_dir_scan_id {
5737 *git_dir_scan_id = local_repo_entry.git_dir_scan_id;
5738 true
5739 } else {
5740 false
5741 }
5742 };
5743
5744 async move {
5745 fs.create_file(&root_path.join(file_name), Default::default())
5746 .await
5747 .unwrap();
5748
5749 cx.condition(&tree, |tree, _| {
5750 scan_id_increased(tree, &mut git_dir_scan_id)
5751 })
5752 .await;
5753
5754 fs.remove_file(&root_path.join(file_name), Default::default())
5755 .await
5756 .unwrap();
5757
5758 cx.condition(&tree, |tree, _| {
5759 scan_id_increased(tree, &mut git_dir_scan_id)
5760 })
5761 .await;
5762
5763 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5764 .await;
5765 }
5766 .boxed_local()
5767 }
5768}
5769
5770#[derive(Clone, Debug)]
5771struct TraversalProgress<'a> {
5772 max_path: &'a Path,
5773 count: usize,
5774 non_ignored_count: usize,
5775 file_count: usize,
5776 non_ignored_file_count: usize,
5777}
5778
5779impl<'a> TraversalProgress<'a> {
5780 fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
5781 match (include_files, include_dirs, include_ignored) {
5782 (true, true, true) => self.count,
5783 (true, true, false) => self.non_ignored_count,
5784 (true, false, true) => self.file_count,
5785 (true, false, false) => self.non_ignored_file_count,
5786 (false, true, true) => self.count - self.file_count,
5787 (false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
5788 (false, false, _) => 0,
5789 }
5790 }
5791}
5792
5793impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> {
5794 fn zero(_cx: &()) -> Self {
5795 Default::default()
5796 }
5797
5798 fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
5799 self.max_path = summary.max_path.as_ref();
5800 self.count += summary.count;
5801 self.non_ignored_count += summary.non_ignored_count;
5802 self.file_count += summary.file_count;
5803 self.non_ignored_file_count += summary.non_ignored_file_count;
5804 }
5805}
5806
5807impl<'a> Default for TraversalProgress<'a> {
5808 fn default() -> Self {
5809 Self {
5810 max_path: Path::new(""),
5811 count: 0,
5812 non_ignored_count: 0,
5813 file_count: 0,
5814 non_ignored_file_count: 0,
5815 }
5816 }
5817}
5818
5819#[derive(Debug, Clone, Copy)]
5820pub struct GitEntryRef<'a> {
5821 pub entry: &'a Entry,
5822 pub git_summary: GitSummary,
5823}
5824
5825impl<'a> GitEntryRef<'a> {
5826 pub fn to_owned(&self) -> GitEntry {
5827 GitEntry {
5828 entry: self.entry.clone(),
5829 git_summary: self.git_summary,
5830 }
5831 }
5832}
5833
5834impl<'a> Deref for GitEntryRef<'a> {
5835 type Target = Entry;
5836
5837 fn deref(&self) -> &Self::Target {
5838 &self.entry
5839 }
5840}
5841
5842impl<'a> AsRef<Entry> for GitEntryRef<'a> {
5843 fn as_ref(&self) -> &Entry {
5844 self.entry
5845 }
5846}
5847
5848#[derive(Debug, Clone, PartialEq, Eq)]
5849pub struct GitEntry {
5850 pub entry: Entry,
5851 pub git_summary: GitSummary,
5852}
5853
5854impl GitEntry {
5855 pub fn to_ref(&self) -> GitEntryRef {
5856 GitEntryRef {
5857 entry: &self.entry,
5858 git_summary: self.git_summary,
5859 }
5860 }
5861}
5862
5863impl Deref for GitEntry {
5864 type Target = Entry;
5865
5866 fn deref(&self) -> &Self::Target {
5867 &self.entry
5868 }
5869}
5870
5871impl AsRef<Entry> for GitEntry {
5872 fn as_ref(&self) -> &Entry {
5873 &self.entry
5874 }
5875}
5876
5877/// Walks the worktree entries and their associated git statuses.
5878pub struct GitTraversal<'a> {
5879 traversal: Traversal<'a>,
5880 current_entry_summary: Option<GitSummary>,
5881 repo_location: Option<(
5882 &'a RepositoryEntry,
5883 Cursor<'a, StatusEntry, PathProgress<'a>>,
5884 )>,
5885}
5886
5887impl<'a> GitTraversal<'a> {
5888 fn synchronize_statuses(&mut self, reset: bool) {
5889 self.current_entry_summary = None;
5890
5891 let Some(entry) = self.traversal.cursor.item() else {
5892 return;
5893 };
5894
5895 let Some(repo) = self.traversal.snapshot.repository_for_path(&entry.path) else {
5896 self.repo_location = None;
5897 return;
5898 };
5899
5900 // Update our state if we changed repositories.
5901 if reset || self.repo_location.as_ref().map(|(prev_repo, _)| prev_repo) != Some(&repo) {
5902 self.repo_location = Some((repo, repo.statuses_by_path.cursor::<PathProgress>(&())));
5903 }
5904
5905 let Some((repo, statuses)) = &mut self.repo_location else {
5906 return;
5907 };
5908
5909 let repo_path = repo.relativize(&entry.path).unwrap();
5910
5911 if entry.is_dir() {
5912 let mut statuses = statuses.clone();
5913 statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
5914 let summary =
5915 statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &());
5916
5917 self.current_entry_summary = Some(summary);
5918 } else if entry.is_file() {
5919 // For a file entry, park the cursor on the corresponding status
5920 if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
5921 // TODO: Investigate statuses.item() being None here.
5922 self.current_entry_summary = statuses.item().map(|item| item.status.into());
5923 } else {
5924 self.current_entry_summary = Some(GitSummary::UNCHANGED);
5925 }
5926 }
5927 }
5928
5929 pub fn advance(&mut self) -> bool {
5930 self.advance_by(1)
5931 }
5932
5933 pub fn advance_by(&mut self, count: usize) -> bool {
5934 let found = self.traversal.advance_by(count);
5935 self.synchronize_statuses(false);
5936 found
5937 }
5938
5939 pub fn advance_to_sibling(&mut self) -> bool {
5940 let found = self.traversal.advance_to_sibling();
5941 self.synchronize_statuses(false);
5942 found
5943 }
5944
5945 pub fn back_to_parent(&mut self) -> bool {
5946 let found = self.traversal.back_to_parent();
5947 self.synchronize_statuses(true);
5948 found
5949 }
5950
5951 pub fn start_offset(&self) -> usize {
5952 self.traversal.start_offset()
5953 }
5954
5955 pub fn end_offset(&self) -> usize {
5956 self.traversal.end_offset()
5957 }
5958
5959 pub fn entry(&self) -> Option<GitEntryRef<'a>> {
5960 let entry = self.traversal.cursor.item()?;
5961 let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED);
5962 Some(GitEntryRef { entry, git_summary })
5963 }
5964}
5965
5966impl<'a> Iterator for GitTraversal<'a> {
5967 type Item = GitEntryRef<'a>;
5968 fn next(&mut self) -> Option<Self::Item> {
5969 if let Some(item) = self.entry() {
5970 self.advance();
5971 Some(item)
5972 } else {
5973 None
5974 }
5975 }
5976}
5977
5978#[derive(Debug)]
5979pub struct Traversal<'a> {
5980 snapshot: &'a Snapshot,
5981 cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
5982 include_ignored: bool,
5983 include_files: bool,
5984 include_dirs: bool,
5985}
5986
5987impl<'a> Traversal<'a> {
5988 fn new(
5989 snapshot: &'a Snapshot,
5990 include_files: bool,
5991 include_dirs: bool,
5992 include_ignored: bool,
5993 start_path: &Path,
5994 ) -> Self {
5995 let mut cursor = snapshot.entries_by_path.cursor(&());
5996 cursor.seek(&TraversalTarget::path(start_path), Bias::Left, &());
5997 let mut traversal = Self {
5998 snapshot,
5999 cursor,
6000 include_files,
6001 include_dirs,
6002 include_ignored,
6003 };
6004 if traversal.end_offset() == traversal.start_offset() {
6005 traversal.next();
6006 }
6007 traversal
6008 }
6009
6010 pub fn with_git_statuses(self) -> GitTraversal<'a> {
6011 let mut this = GitTraversal {
6012 traversal: self,
6013 current_entry_summary: None,
6014 repo_location: None,
6015 };
6016 this.synchronize_statuses(true);
6017 this
6018 }
6019
6020 pub fn advance(&mut self) -> bool {
6021 self.advance_by(1)
6022 }
6023
6024 pub fn advance_by(&mut self, count: usize) -> bool {
6025 self.cursor.seek_forward(
6026 &TraversalTarget::Count {
6027 count: self.end_offset() + count,
6028 include_dirs: self.include_dirs,
6029 include_files: self.include_files,
6030 include_ignored: self.include_ignored,
6031 },
6032 Bias::Left,
6033 &(),
6034 )
6035 }
6036
6037 pub fn advance_to_sibling(&mut self) -> bool {
6038 while let Some(entry) = self.cursor.item() {
6039 self.cursor
6040 .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left, &());
6041 if let Some(entry) = self.cursor.item() {
6042 if (self.include_files || !entry.is_file())
6043 && (self.include_dirs || !entry.is_dir())
6044 && (self.include_ignored || !entry.is_ignored || entry.is_always_included)
6045 {
6046 return true;
6047 }
6048 }
6049 }
6050 false
6051 }
6052
6053 pub fn back_to_parent(&mut self) -> bool {
6054 let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else {
6055 return false;
6056 };
6057 self.cursor
6058 .seek(&TraversalTarget::path(parent_path), Bias::Left, &())
6059 }
6060
6061 pub fn entry(&self) -> Option<&'a Entry> {
6062 self.cursor.item()
6063 }
6064
6065 pub fn start_offset(&self) -> usize {
6066 self.cursor
6067 .start()
6068 .count(self.include_files, self.include_dirs, self.include_ignored)
6069 }
6070
6071 pub fn end_offset(&self) -> usize {
6072 self.cursor
6073 .end(&())
6074 .count(self.include_files, self.include_dirs, self.include_ignored)
6075 }
6076}
6077
6078impl<'a> Iterator for Traversal<'a> {
6079 type Item = &'a Entry;
6080
6081 fn next(&mut self) -> Option<Self::Item> {
6082 if let Some(item) = self.entry() {
6083 self.advance();
6084 Some(item)
6085 } else {
6086 None
6087 }
6088 }
6089}
6090
6091#[derive(Debug, Clone, Copy)]
6092enum PathTarget<'a> {
6093 Path(&'a Path),
6094 Successor(&'a Path),
6095}
6096
6097impl<'a> PathTarget<'a> {
6098 fn cmp_path(&self, other: &Path) -> Ordering {
6099 match self {
6100 PathTarget::Path(path) => path.cmp(&other),
6101 PathTarget::Successor(path) => {
6102 if other.starts_with(path) {
6103 Ordering::Greater
6104 } else {
6105 Ordering::Equal
6106 }
6107 }
6108 }
6109 }
6110}
6111
6112impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary<S>, PathProgress<'a>> for PathTarget<'b> {
6113 fn cmp(&self, cursor_location: &PathProgress<'a>, _: &S::Context) -> Ordering {
6114 self.cmp_path(&cursor_location.max_path)
6115 }
6116}
6117
6118impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> for PathTarget<'b> {
6119 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering {
6120 self.cmp_path(&cursor_location.max_path)
6121 }
6122}
6123
6124impl<'a, 'b> SeekTarget<'a, PathSummary<GitSummary>, (TraversalProgress<'a>, GitSummary)>
6125 for PathTarget<'b>
6126{
6127 fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering {
6128 self.cmp_path(&cursor_location.0.max_path)
6129 }
6130}
6131
6132#[derive(Debug)]
6133enum TraversalTarget<'a> {
6134 Path(PathTarget<'a>),
6135 Count {
6136 count: usize,
6137 include_files: bool,
6138 include_ignored: bool,
6139 include_dirs: bool,
6140 },
6141}
6142
6143impl<'a> TraversalTarget<'a> {
6144 fn path(path: &'a Path) -> Self {
6145 Self::Path(PathTarget::Path(path))
6146 }
6147
6148 fn successor(path: &'a Path) -> Self {
6149 Self::Path(PathTarget::Successor(path))
6150 }
6151
6152 fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering {
6153 match self {
6154 TraversalTarget::Path(path) => path.cmp_path(&progress.max_path),
6155 TraversalTarget::Count {
6156 count,
6157 include_files,
6158 include_dirs,
6159 include_ignored,
6160 } => Ord::cmp(
6161 count,
6162 &progress.count(*include_files, *include_dirs, *include_ignored),
6163 ),
6164 }
6165 }
6166}
6167
6168impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'b> {
6169 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering {
6170 self.cmp_progress(cursor_location)
6171 }
6172}
6173
6174impl<'a, 'b> SeekTarget<'a, PathSummary<Unit>, TraversalProgress<'a>> for TraversalTarget<'b> {
6175 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering {
6176 self.cmp_progress(cursor_location)
6177 }
6178}
6179
6180pub struct ChildEntriesOptions {
6181 pub include_files: bool,
6182 pub include_dirs: bool,
6183 pub include_ignored: bool,
6184}
6185
6186pub struct ChildEntriesIter<'a> {
6187 parent_path: &'a Path,
6188 traversal: Traversal<'a>,
6189}
6190
6191impl<'a> ChildEntriesIter<'a> {
6192 pub fn with_git_statuses(self) -> ChildEntriesGitIter<'a> {
6193 ChildEntriesGitIter {
6194 parent_path: self.parent_path,
6195 traversal: self.traversal.with_git_statuses(),
6196 }
6197 }
6198}
6199
6200pub struct ChildEntriesGitIter<'a> {
6201 parent_path: &'a Path,
6202 traversal: GitTraversal<'a>,
6203}
6204
6205impl<'a> Iterator for ChildEntriesIter<'a> {
6206 type Item = &'a Entry;
6207
6208 fn next(&mut self) -> Option<Self::Item> {
6209 if let Some(item) = self.traversal.entry() {
6210 if item.path.starts_with(self.parent_path) {
6211 self.traversal.advance_to_sibling();
6212 return Some(item);
6213 }
6214 }
6215 None
6216 }
6217}
6218
6219impl<'a> Iterator for ChildEntriesGitIter<'a> {
6220 type Item = GitEntryRef<'a>;
6221
6222 fn next(&mut self) -> Option<Self::Item> {
6223 if let Some(item) = self.traversal.entry() {
6224 if item.path.starts_with(self.parent_path) {
6225 self.traversal.advance_to_sibling();
6226 return Some(item);
6227 }
6228 }
6229 None
6230 }
6231}
6232
6233impl<'a> From<&'a Entry> for proto::Entry {
6234 fn from(entry: &'a Entry) -> Self {
6235 Self {
6236 id: entry.id.to_proto(),
6237 is_dir: entry.is_dir(),
6238 path: entry.path.as_ref().to_proto(),
6239 inode: entry.inode,
6240 mtime: entry.mtime.map(|time| time.into()),
6241 is_ignored: entry.is_ignored,
6242 is_external: entry.is_external,
6243 is_fifo: entry.is_fifo,
6244 size: Some(entry.size),
6245 canonical_path: entry
6246 .canonical_path
6247 .as_ref()
6248 .map(|path| path.as_ref().to_proto()),
6249 }
6250 }
6251}
6252
6253impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
6254 type Error = anyhow::Error;
6255
6256 fn try_from(
6257 (root_char_bag, always_included, entry): (&'a CharBag, &PathMatcher, proto::Entry),
6258 ) -> Result<Self> {
6259 let kind = if entry.is_dir {
6260 EntryKind::Dir
6261 } else {
6262 EntryKind::File
6263 };
6264
6265 let path = Arc::<Path>::from_proto(entry.path);
6266 let char_bag = char_bag_for_path(*root_char_bag, &path);
6267 let is_always_included = always_included.is_match(path.as_ref());
6268 Ok(Entry {
6269 id: ProjectEntryId::from_proto(entry.id),
6270 kind,
6271 path,
6272 inode: entry.inode,
6273 mtime: entry.mtime.map(|time| time.into()),
6274 size: entry.size.unwrap_or(0),
6275 canonical_path: entry
6276 .canonical_path
6277 .map(|path_string| Box::from(PathBuf::from_proto(path_string))),
6278 is_ignored: entry.is_ignored,
6279 is_always_included,
6280 is_external: entry.is_external,
6281 is_private: false,
6282 char_bag,
6283 is_fifo: entry.is_fifo,
6284 })
6285 }
6286}
6287
6288fn status_from_proto(
6289 simple_status: i32,
6290 status: Option<proto::GitFileStatus>,
6291) -> anyhow::Result<FileStatus> {
6292 use proto::git_file_status::Variant;
6293
6294 let Some(variant) = status.and_then(|status| status.variant) else {
6295 let code = proto::GitStatus::from_i32(simple_status)
6296 .ok_or_else(|| anyhow!("Invalid git status code: {simple_status}"))?;
6297 let result = match code {
6298 proto::GitStatus::Added => TrackedStatus {
6299 worktree_status: StatusCode::Added,
6300 index_status: StatusCode::Unmodified,
6301 }
6302 .into(),
6303 proto::GitStatus::Modified => TrackedStatus {
6304 worktree_status: StatusCode::Modified,
6305 index_status: StatusCode::Unmodified,
6306 }
6307 .into(),
6308 proto::GitStatus::Conflict => UnmergedStatus {
6309 first_head: UnmergedStatusCode::Updated,
6310 second_head: UnmergedStatusCode::Updated,
6311 }
6312 .into(),
6313 proto::GitStatus::Deleted => TrackedStatus {
6314 worktree_status: StatusCode::Deleted,
6315 index_status: StatusCode::Unmodified,
6316 }
6317 .into(),
6318 _ => return Err(anyhow!("Invalid code for simple status: {simple_status}")),
6319 };
6320 return Ok(result);
6321 };
6322
6323 let result = match variant {
6324 Variant::Untracked(_) => FileStatus::Untracked,
6325 Variant::Ignored(_) => FileStatus::Ignored,
6326 Variant::Unmerged(unmerged) => {
6327 let [first_head, second_head] =
6328 [unmerged.first_head, unmerged.second_head].map(|head| {
6329 let code = proto::GitStatus::from_i32(head)
6330 .ok_or_else(|| anyhow!("Invalid git status code: {head}"))?;
6331 let result = match code {
6332 proto::GitStatus::Added => UnmergedStatusCode::Added,
6333 proto::GitStatus::Updated => UnmergedStatusCode::Updated,
6334 proto::GitStatus::Deleted => UnmergedStatusCode::Deleted,
6335 _ => return Err(anyhow!("Invalid code for unmerged status: {code:?}")),
6336 };
6337 Ok(result)
6338 });
6339 let [first_head, second_head] = [first_head?, second_head?];
6340 UnmergedStatus {
6341 first_head,
6342 second_head,
6343 }
6344 .into()
6345 }
6346 Variant::Tracked(tracked) => {
6347 let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status]
6348 .map(|status| {
6349 let code = proto::GitStatus::from_i32(status)
6350 .ok_or_else(|| anyhow!("Invalid git status code: {status}"))?;
6351 let result = match code {
6352 proto::GitStatus::Modified => StatusCode::Modified,
6353 proto::GitStatus::TypeChanged => StatusCode::TypeChanged,
6354 proto::GitStatus::Added => StatusCode::Added,
6355 proto::GitStatus::Deleted => StatusCode::Deleted,
6356 proto::GitStatus::Renamed => StatusCode::Renamed,
6357 proto::GitStatus::Copied => StatusCode::Copied,
6358 proto::GitStatus::Unmodified => StatusCode::Unmodified,
6359 _ => return Err(anyhow!("Invalid code for tracked status: {code:?}")),
6360 };
6361 Ok(result)
6362 });
6363 let [index_status, worktree_status] = [index_status?, worktree_status?];
6364 TrackedStatus {
6365 index_status,
6366 worktree_status,
6367 }
6368 .into()
6369 }
6370 };
6371 Ok(result)
6372}
6373
6374fn status_to_proto(status: FileStatus) -> proto::GitFileStatus {
6375 use proto::git_file_status::{Tracked, Unmerged, Variant};
6376
6377 let variant = match status {
6378 FileStatus::Untracked => Variant::Untracked(Default::default()),
6379 FileStatus::Ignored => Variant::Ignored(Default::default()),
6380 FileStatus::Unmerged(UnmergedStatus {
6381 first_head,
6382 second_head,
6383 }) => Variant::Unmerged(Unmerged {
6384 first_head: unmerged_status_to_proto(first_head),
6385 second_head: unmerged_status_to_proto(second_head),
6386 }),
6387 FileStatus::Tracked(TrackedStatus {
6388 index_status,
6389 worktree_status,
6390 }) => Variant::Tracked(Tracked {
6391 index_status: tracked_status_to_proto(index_status),
6392 worktree_status: tracked_status_to_proto(worktree_status),
6393 }),
6394 };
6395 proto::GitFileStatus {
6396 variant: Some(variant),
6397 }
6398}
6399
6400fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 {
6401 match code {
6402 UnmergedStatusCode::Added => proto::GitStatus::Added as _,
6403 UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _,
6404 UnmergedStatusCode::Updated => proto::GitStatus::Updated as _,
6405 }
6406}
6407
6408fn tracked_status_to_proto(code: StatusCode) -> i32 {
6409 match code {
6410 StatusCode::Added => proto::GitStatus::Added as _,
6411 StatusCode::Deleted => proto::GitStatus::Deleted as _,
6412 StatusCode::Modified => proto::GitStatus::Modified as _,
6413 StatusCode::Renamed => proto::GitStatus::Renamed as _,
6414 StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _,
6415 StatusCode::Copied => proto::GitStatus::Copied as _,
6416 StatusCode::Unmodified => proto::GitStatus::Unmodified as _,
6417 }
6418}
6419
6420#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
6421pub struct ProjectEntryId(usize);
6422
6423impl ProjectEntryId {
6424 pub const MAX: Self = Self(usize::MAX);
6425 pub const MIN: Self = Self(usize::MIN);
6426
6427 pub fn new(counter: &AtomicUsize) -> Self {
6428 Self(counter.fetch_add(1, SeqCst))
6429 }
6430
6431 pub fn from_proto(id: u64) -> Self {
6432 Self(id as usize)
6433 }
6434
6435 pub fn to_proto(&self) -> u64 {
6436 self.0 as u64
6437 }
6438
6439 pub fn to_usize(&self) -> usize {
6440 self.0
6441 }
6442}
6443
6444#[cfg(any(test, feature = "test-support"))]
6445impl CreatedEntry {
6446 pub fn to_included(self) -> Option<Entry> {
6447 match self {
6448 CreatedEntry::Included(entry) => Some(entry),
6449 CreatedEntry::Excluded { .. } => None,
6450 }
6451 }
6452}