Skip to repository content7318 lines · 269.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:42:52.046Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
worktree.rs
1mod ignore;
2mod worktree_settings;
3
4use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
5use anyhow::{Context as _, Result, anyhow};
6use chardetng::EncodingDetector;
7use clock::ReplicaId;
8use collections::{BTreeMap, HashMap, HashSet, VecDeque};
9use encoding_rs::Encoding;
10use fs::{
11 Fs, MTime, PathEvent, PathEventKind, RemoveOptions, TrashId, Watcher, copy_recursive,
12 read_dir_items,
13};
14use futures::{
15 FutureExt as _, Stream, StreamExt,
16 channel::{
17 mpsc::{self, UnboundedSender},
18 oneshot,
19 },
20 select_biased, stream,
21 task::Poll,
22};
23use fuzzy::CharBag;
24use git::{
25 BISECT_LOG, COMMIT_MESSAGE, DOT_GIT, FETCH_HEAD, FSMONITOR_DAEMON, GC_PID, GITIGNORE,
26 HOOKS_DIR, INFO_DIR, LFS_DIR, LOGS_DIR, LOGS_REF_STASH, OBJECTS_DIR, ORIG_HEAD,
27 REBASE_APPLY_DIR, REBASE_MERGE_DIR, REFS_DIR, REFTABLE_DIR, REPO_EXCLUDE, SEQUENCER_DIR,
28 status::GitSummary,
29};
30use gpui::{
31 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority,
32 Task,
33};
34use ignore::IgnoreStack;
35use language::{ByteContent, DiskState, FILE_ANALYSIS_BYTES, analyze_byte_content};
36
37use async_channel::{self, Sender};
38use parking_lot::Mutex;
39use paths::{local_settings_folder_name, local_vscode_folder_name};
40use postage::{
41 barrier,
42 prelude::{Sink as _, Stream as _},
43 watch,
44};
45use rpc::{
46 AnyProtoClient,
47 proto::{self, split_worktree_update},
48};
49pub use settings::WorktreeId;
50use settings::{Settings, SettingsLocation, SettingsStore};
51use smallvec::{SmallVec, smallvec};
52use std::{
53 any::Any,
54 borrow::Borrow as _,
55 cmp::Ordering,
56 collections::hash_map,
57 convert::TryFrom,
58 ffi::OsStr,
59 fmt,
60 future::Future,
61 mem::{self},
62 ops::{Deref, DerefMut, Range},
63 path::{Path, PathBuf},
64 pin::Pin,
65 sync::{
66 Arc,
67 atomic::{AtomicUsize, Ordering::SeqCst},
68 },
69 time::{Duration, Instant},
70};
71use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};
72use text::{LineEnding, Rope};
73use util::{
74 ResultExt, maybe,
75 paths::{PathMatcher, PathStyle, SanitizedPath, home_dir},
76 rel_path::RelPath,
77};
78pub use worktree_settings::WorktreeSettings;
79
80use crate::ignore::IgnoreKind;
81
82pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
83
84/// A set of local or remote files that are being opened as part of a project.
85/// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates.
86/// Stores git repositories data and the diagnostics for the file(s).
87///
88/// Has an absolute path, and may be set to be visible in Zed UI or not.
89/// May correspond to a directory or a single file.
90/// Possible examples:
91/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree
92/// * a directory opened in Zed — may be added as a visible entry to the current worktree
93///
94/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.
95pub enum Worktree {
96 Local(LocalWorktree),
97 Remote(RemoteWorktree),
98}
99
100/// An entry, created in the worktree.
101#[derive(Debug)]
102pub enum CreatedEntry {
103 /// Got created and indexed by the worktree, receiving a corresponding entry.
104 Included(Entry),
105 /// Got created, but not indexed due to falling under exclusion filters.
106 Excluded { abs_path: PathBuf },
107}
108
109#[derive(Debug)]
110pub struct LoadedFile {
111 pub file: Arc<File>,
112 pub text: String,
113 pub encoding: &'static Encoding,
114 pub has_bom: bool,
115 pub is_writable: bool,
116}
117
118pub struct LoadedBinaryFile {
119 pub file: Arc<File>,
120 pub content: Vec<u8>,
121}
122
123impl fmt::Debug for LoadedBinaryFile {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("LoadedBinaryFile")
126 .field("file", &self.file)
127 .field("content_bytes", &self.content.len())
128 .finish()
129 }
130}
131
132pub struct LocalWorktree {
133 snapshot: LocalSnapshot,
134 scan_requests_tx: async_channel::Sender<ScanRequest>,
135 path_prefixes_to_scan_tx: async_channel::Sender<PathPrefixScanRequest>,
136 is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
137 snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
138 _background_scanner_tasks: Vec<Task<()>>,
139 update_observer: Option<UpdateObservationState>,
140 fs: Arc<dyn Fs>,
141 fs_case_sensitive: bool,
142 visible: bool,
143 next_entry_id: Arc<AtomicUsize>,
144 settings: WorktreeSettings,
145 share_private_files: bool,
146 scanning_enabled: bool,
147 force_defer_watch: bool,
148}
149
150pub struct PathPrefixScanRequest {
151 path: Arc<RelPath>,
152 done: SmallVec<[barrier::Sender; 1]>,
153}
154
155struct ScanRequest {
156 relative_paths: Vec<Arc<RelPath>>,
157 done: SmallVec<[barrier::Sender; 1]>,
158}
159
160pub struct RemoteWorktree {
161 snapshot: Snapshot,
162 background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,
163 project_id: u64,
164 client: AnyProtoClient,
165 file_scan_inclusions: PathMatcher,
166 updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
167 update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,
168 snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
169 replica_id: ReplicaId,
170 visible: bool,
171 disconnected: bool,
172 received_initial_update: bool,
173}
174
175#[derive(Clone)]
176pub struct Snapshot {
177 id: WorktreeId,
178 /// The absolute path of the worktree root.
179 abs_path: Arc<SanitizedPath>,
180 path_style: PathStyle,
181 root_name: Arc<RelPath>,
182 root_char_bag: CharBag,
183 entries_by_path: SumTree<Entry>,
184 entries_by_id: SumTree<PathEntry>,
185 root_repo_common_dir: Option<Arc<SanitizedPath>>,
186 root_repo_is_linked_worktree: bool,
187 always_included_entries: Vec<Arc<RelPath>>,
188
189 /// A number that increases every time the worktree begins scanning
190 /// a set of paths from the filesystem. This scanning could be caused
191 /// by some operation performed on the worktree, such as reading or
192 /// writing a file, or by an event reported by the filesystem.
193 scan_id: usize,
194
195 /// The latest scan id that has completed, and whose preceding scans
196 /// have all completed. The current `scan_id` could be more than one
197 /// greater than the `completed_scan_id` if operations are performed
198 /// on the worktree while it is processing a file-system event.
199 completed_scan_id: usize,
200}
201
202/// This path corresponds to the 'content path' of a repository in relation
203/// to Zed's project root.
204/// In the majority of the cases, this is the folder that contains the .git folder.
205/// But if a sub-folder of a git repository is opened, this corresponds to the
206/// project root and the .git folder is located in a parent directory.
207#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
208pub enum WorkDirectory {
209 InProject {
210 relative_path: Arc<RelPath>,
211 },
212 AboveProject {
213 absolute_path: Arc<Path>,
214 location_in_repo: Arc<Path>,
215 },
216}
217
218impl WorkDirectory {
219 fn path_key(&self) -> PathKey {
220 match self {
221 WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
222 WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty_arc()),
223 }
224 }
225
226 /// Returns true if the given path is a child of the work directory.
227 ///
228 /// Note that the path may not be a member of this repository, if there
229 /// is a repository in a directory between these two paths
230 /// external .git folder in a parent folder of the project root.
231 #[track_caller]
232 pub fn directory_contains(&self, path: &RelPath) -> bool {
233 match self {
234 WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
235 WorkDirectory::AboveProject { .. } => true,
236 }
237 }
238}
239
240impl Default for WorkDirectory {
241 fn default() -> Self {
242 Self::InProject {
243 relative_path: Arc::from(RelPath::empty()),
244 }
245 }
246}
247
248#[derive(Clone)]
249pub struct LocalSnapshot {
250 snapshot: Snapshot,
251 global_gitignore: Option<Arc<Gitignore>>,
252 /// Exclude files for all git repositories in the worktree, indexed by their absolute path.
253 /// The boolean indicates whether the gitignore needs to be updated.
254 repo_exclude_by_work_dir_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
255 /// All of the gitignore files in the worktree, indexed by their absolute path.
256 /// The boolean indicates whether the gitignore needs to be updated.
257 ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
258 /// All of the git repositories in the worktree, indexed by the project entry
259 /// id of their parent directory.
260 git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
261 /// The file handle of the worktree root
262 /// (so we can find it after it's been moved)
263 root_file_handle: Option<Arc<dyn fs::FileHandle>>,
264 /// Maps canonical absolute paths of externally watched symlinked directories
265 /// to their relative paths within the worktree, used to translate FSEvents
266 /// canonical-path events back to worktree-relative paths.
267 external_canonical_to_relative: BTreeMap<Arc<Path>, Arc<RelPath>>,
268}
269
270struct BackgroundScannerState {
271 snapshot: LocalSnapshot,
272 symlink_paths_by_target: HashMap<Arc<Path>, SmallVec<[Arc<RelPath>; 1]>>,
273 scanned_dirs: HashSet<ProjectEntryId>,
274 watched_dir_abs_paths_by_entry_id: HashMap<ProjectEntryId, Arc<Path>>,
275 path_prefixes_to_scan: HashSet<Arc<RelPath>>,
276 paths_to_scan: HashSet<Arc<RelPath>>,
277 removed_entries: RemovedEntries,
278 changed_paths: Vec<Arc<RelPath>>,
279 prev_snapshot: Snapshot,
280 scanning_enabled: bool,
281}
282
283/// The entries that were removed from the snapshot as part of the current
284/// update. Their entry ids may be re-used if the same inode is discovered
285/// at a new path, or if the given path is re-created after being deleted.
286///
287/// Symlink aliases inside the worktree share their inode (and usually mtime)
288/// with the symlink target, so an inode may correspond to several entries.
289/// The path index allows an exact match to take precedence over the
290/// inode-based rename heuristics in that case.
291#[derive(Default)]
292struct RemovedEntries {
293 by_inode: HashMap<u64, Entry>,
294 by_path: HashMap<Arc<RelPath>, Entry>,
295}
296
297impl RemovedEntries {
298 fn insert(&mut self, entry: &Entry) {
299 self.by_path.insert(entry.path.clone(), entry.clone());
300 match self.by_inode.entry(entry.inode) {
301 hash_map::Entry::Occupied(mut o) => {
302 if entry.id > o.get().id {
303 o.insert(entry.clone());
304 }
305 }
306 hash_map::Entry::Vacant(v) => {
307 v.insert(entry.clone());
308 }
309 }
310 }
311
312 fn take_by_path(&mut self, path: &RelPath, inode: u64) -> Option<Entry> {
313 if self.by_path.get(path)?.inode != inode {
314 return None;
315 }
316 let removed = self.by_path.remove(path)?;
317 if let hash_map::Entry::Occupied(o) = self.by_inode.entry(removed.inode)
318 && o.get().id == removed.id
319 {
320 o.remove();
321 }
322 Some(removed)
323 }
324
325 fn take_by_inode(&mut self, inode: u64) -> Option<Entry> {
326 let removed = self.by_inode.remove(&inode)?;
327 if let hash_map::Entry::Occupied(o) = self.by_path.entry(removed.path.clone())
328 && o.get().id == removed.id
329 {
330 o.remove();
331 }
332 Some(removed)
333 }
334}
335
336#[derive(Clone, Debug, Eq, PartialEq)]
337struct EventRoot {
338 path: Arc<RelPath>,
339 was_rescanned: bool,
340}
341
342#[derive(Debug, Clone)]
343struct LocalRepositoryEntry {
344 work_directory_id: ProjectEntryId,
345 work_directory: WorkDirectory,
346 work_directory_abs_path: Arc<Path>,
347 git_dir_scan_id: usize,
348 /// Absolute path to the original .git entry that caused us to create this repository.
349 ///
350 /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere
351 /// (whose path we then store in `repository_dir_abs_path`).
352 dot_git_abs_path: Arc<Path>,
353 /// Absolute path to the "commondir" for this repository.
354 ///
355 /// This is always a directory. For a normal repository, this is the same as
356 /// `dot_git_abs_path`. For a linked worktree, this is the main repo's `.git`
357 /// directory (resolved from the worktree's `commondir` file). For a submodule,
358 /// this equals `repository_dir_abs_path` (submodules don't have a `commondir`
359 /// file).
360 common_dir_abs_path: Arc<Path>,
361 /// Absolute path to the directory holding the repository's state.
362 ///
363 /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and
364 /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the
365 /// commondir like `/project/.git/modules/foo`.
366 repository_dir_abs_path: Arc<Path>,
367}
368
369impl sum_tree::Item for LocalRepositoryEntry {
370 type Summary = PathSummary<sum_tree::NoSummary>;
371
372 fn summary(&self, _: <Self::Summary as Summary>::Context<'_>) -> Self::Summary {
373 PathSummary {
374 max_path: self.work_directory.path_key().0,
375 item_summary: sum_tree::NoSummary,
376 }
377 }
378}
379
380impl KeyedItem for LocalRepositoryEntry {
381 type Key = PathKey;
382
383 fn key(&self) -> Self::Key {
384 self.work_directory.path_key()
385 }
386}
387
388impl Deref for LocalRepositoryEntry {
389 type Target = WorkDirectory;
390
391 fn deref(&self) -> &Self::Target {
392 &self.work_directory
393 }
394}
395
396impl Deref for LocalSnapshot {
397 type Target = Snapshot;
398
399 fn deref(&self) -> &Self::Target {
400 &self.snapshot
401 }
402}
403
404impl DerefMut for LocalSnapshot {
405 fn deref_mut(&mut self) -> &mut Self::Target {
406 &mut self.snapshot
407 }
408}
409
410enum ScanState {
411 Started,
412 Updated {
413 snapshot: LocalSnapshot,
414 changes: UpdatedEntriesSet,
415 barrier: SmallVec<[barrier::Sender; 1]>,
416 scanning: bool,
417 },
418 RootUpdated {
419 new_path: Arc<SanitizedPath>,
420 },
421 RootDeleted,
422}
423
424struct UpdateObservationState {
425 snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>,
426 resume_updates: watch::Sender<()>,
427 _maintain_remote_snapshot: Task<Option<()>>,
428}
429
430#[derive(Debug, Clone)]
431pub enum Event {
432 UpdatedEntries(UpdatedEntriesSet),
433 UpdatedGitRepositories(UpdatedGitRepositoriesSet),
434 UpdatedRootRepoCommonDir {
435 old: Option<Arc<SanitizedPath>>,
436 },
437 DeletedEntry(ProjectEntryId),
438 /// The worktree root itself has been deleted (for single-file worktrees)
439 Deleted,
440}
441
442impl EventEmitter<Event> for Worktree {}
443
444impl Worktree {
445 pub async fn local(
446 path: impl Into<Arc<Path>>,
447 visible: bool,
448 fs: Arc<dyn Fs>,
449 next_entry_id: Arc<AtomicUsize>,
450 scanning_enabled: bool,
451 worktree_id: WorktreeId,
452 cx: &mut AsyncApp,
453 ) -> Result<Entity<Self>> {
454 let abs_path = path.into();
455 let metadata = fs
456 .metadata(&abs_path)
457 .await
458 .context("failed to stat worktree path")?;
459
460 let fs_case_sensitive = fs.is_case_sensitive().await;
461
462 let root_file_handle = if metadata.as_ref().is_some() {
463 fs.open_handle(&abs_path)
464 .await
465 .with_context(|| {
466 format!(
467 "failed to open local worktree root at {}",
468 abs_path.display()
469 )
470 })
471 .log_err()
472 } else {
473 None
474 };
475
476 let (root_repo_common_dir, root_repo_is_linked_worktree) = if visible {
477 discover_root_repo_metadata(&abs_path, fs.as_ref())
478 .await
479 .map(|(common_dir, is_linked_worktree)| {
480 (
481 Some(SanitizedPath::from_arc(common_dir)),
482 is_linked_worktree,
483 )
484 })
485 .unwrap_or((None, false))
486 } else {
487 (None, false)
488 };
489 Ok(cx.new(move |cx: &mut Context<Worktree>| {
490 let mut snapshot = LocalSnapshot {
491 ignores_by_parent_abs_path: Default::default(),
492 global_gitignore: Default::default(),
493 repo_exclude_by_work_dir_abs_path: Default::default(),
494 git_repositories: Default::default(),
495 external_canonical_to_relative: Default::default(),
496 snapshot: Snapshot::new(
497 worktree_id,
498 abs_path
499 .file_name()
500 .and_then(|f| f.to_str())
501 .map_or(RelPath::empty_arc(), |f| {
502 RelPath::from_unix_str(f).unwrap().into()
503 }),
504 abs_path.clone(),
505 PathStyle::local(),
506 ),
507 root_file_handle,
508 };
509 snapshot.root_repo_common_dir = root_repo_common_dir;
510 snapshot.root_repo_is_linked_worktree = root_repo_is_linked_worktree;
511
512 let worktree_id = snapshot.id();
513 let settings_location = Some(SettingsLocation {
514 worktree_id,
515 path: RelPath::empty(),
516 });
517
518 let settings = WorktreeSettings::get(settings_location, cx).clone();
519 cx.observe_global::<SettingsStore>(move |this, cx| {
520 if let Self::Local(this) = this {
521 let settings = WorktreeSettings::get(settings_location, cx).clone();
522 if this.settings != settings {
523 this.settings = settings;
524 this.restart_background_scanners(cx);
525 }
526 }
527 })
528 .detach();
529
530 let share_private_files = false;
531 if let Some(metadata) = metadata {
532 let mut entry = Entry::new(
533 RelPath::empty_arc(),
534 &metadata,
535 ProjectEntryId::new(&next_entry_id),
536 snapshot.root_char_bag,
537 None,
538 );
539 if metadata.is_dir {
540 if !scanning_enabled {
541 entry.kind = EntryKind::UnloadedDir;
542 }
543 } else {
544 if let Some(file_name) = abs_path.file_name()
545 && let Some(file_name) = file_name.to_str()
546 && let Ok(path) = RelPath::from_unix_str(file_name)
547 {
548 entry.is_private = !share_private_files && settings.is_path_private(path);
549 entry.is_hidden = settings.is_path_hidden(path);
550 }
551 }
552 cx.foreground_executor()
553 .block_on(snapshot.insert_entry(entry, fs.as_ref()));
554 }
555
556 let (scan_requests_tx, scan_requests_rx) = async_channel::unbounded();
557 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = async_channel::unbounded();
558 let mut worktree = LocalWorktree {
559 share_private_files,
560 next_entry_id,
561 snapshot,
562 is_scanning: watch::channel_with(true),
563 snapshot_subscriptions: Default::default(),
564 update_observer: None,
565 scan_requests_tx,
566 path_prefixes_to_scan_tx,
567 _background_scanner_tasks: Vec::new(),
568 fs,
569 fs_case_sensitive,
570 visible,
571 settings,
572 scanning_enabled,
573 force_defer_watch: false,
574 };
575 worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
576 Worktree::Local(worktree)
577 }))
578 }
579
580 pub fn remote(
581 project_id: u64,
582 replica_id: ReplicaId,
583 worktree: proto::WorktreeMetadata,
584 client: AnyProtoClient,
585 path_style: PathStyle,
586 cx: &mut App,
587 ) -> Entity<Self> {
588 cx.new(|cx: &mut Context<Self>| {
589 let mut snapshot = Snapshot::new(
590 WorktreeId::from_proto(worktree.id),
591 RelPath::from_unix_str(&worktree.root_name)
592 .map_or_else(|_| RelPath::empty_arc(), Into::into),
593 Path::new(&worktree.abs_path).into(),
594 path_style,
595 );
596
597 snapshot.root_repo_common_dir = worktree
598 .root_repo_common_dir
599 .map(|p| SanitizedPath::new_arc(Path::new(&p)));
600 snapshot.root_repo_is_linked_worktree = worktree.root_repo_is_linked_worktree;
601
602 let background_snapshot = Arc::new(Mutex::new((
603 snapshot.clone(),
604 Vec::<proto::UpdateWorktree>::new(),
605 )));
606 let (background_updates_tx, mut background_updates_rx) =
607 mpsc::unbounded::<proto::UpdateWorktree>();
608 let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
609
610 let worktree_id = snapshot.id();
611 let settings_location = Some(SettingsLocation {
612 worktree_id,
613 path: RelPath::empty(),
614 });
615
616 let settings = WorktreeSettings::get(settings_location, cx).clone();
617 let worktree = RemoteWorktree {
618 client,
619 project_id,
620 replica_id,
621 snapshot,
622 file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(),
623 background_snapshot: background_snapshot.clone(),
624 updates_tx: Some(background_updates_tx),
625 update_observer: None,
626 snapshot_subscriptions: Default::default(),
627 visible: worktree.visible,
628 disconnected: false,
629 received_initial_update: false,
630 };
631
632 // Apply updates to a separate snapshot in a background task, then
633 // send them to a foreground task which updates the model.
634 cx.background_spawn(async move {
635 while let Some(update) = background_updates_rx.next().await {
636 {
637 let mut lock = background_snapshot.lock();
638 lock.0.apply_remote_update(
639 update.clone(),
640 &settings.parent_dir_scan_inclusions,
641 );
642 lock.1.push(update);
643 }
644 snapshot_updated_tx.send(()).await.ok();
645 }
646 })
647 .detach();
648
649 // On the foreground task, update to the latest snapshot and notify
650 // any update observer of all updates that led to that snapshot.
651 cx.spawn(async move |this, cx| {
652 while (snapshot_updated_rx.recv().await).is_some() {
653 this.update(cx, |this, cx| {
654 let this = this.as_remote_mut().unwrap();
655
656 // The watch channel delivers an initial signal before
657 // any real updates arrive. Skip these spurious wakeups.
658 if this.background_snapshot.lock().1.is_empty() {
659 return;
660 }
661
662 let old_root_repo_common_dir = this.snapshot.root_repo_common_dir.clone();
663 let old_root_repo_is_linked_worktree =
664 this.snapshot.root_repo_is_linked_worktree;
665 let mut changed_entries: Vec<(Arc<RelPath>, ProjectEntryId, PathChange)> =
666 Vec::new();
667 {
668 let mut lock = this.background_snapshot.lock();
669 // Replace the snapshot, keeping the previous one around so we can
670 // resolve the paths of removed entries (the new snapshot no longer
671 // contains them, and the wire format only carries their ids).
672 let old_snapshot = mem::replace(&mut this.snapshot, lock.0.clone());
673 for update in lock.1.drain(..) {
674 for entry_id in &update.removed_entries {
675 let entry_id = ProjectEntryId::from_proto(*entry_id);
676 if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
677 changed_entries.push((
678 entry.path.clone(),
679 entry_id,
680 PathChange::Removed,
681 ));
682 }
683 }
684 for entry in &update.updated_entries {
685 // Remote updates don't distinguish creation from
686 // modification, so report `AddedOrUpdated`.
687 if let Some(path) =
688 RelPath::from_unix_str(&entry.path).log_err()
689 {
690 changed_entries.push((
691 path.into(),
692 ProjectEntryId::from_proto(entry.id),
693 PathChange::AddedOrUpdated,
694 ));
695 }
696 }
697 if let Some(tx) = &this.update_observer {
698 tx.unbounded_send(update).ok();
699 }
700 }
701 };
702
703 if !changed_entries.is_empty() {
704 cx.emit(Event::UpdatedEntries(changed_entries.into()));
705 }
706 let is_first_update = !this.received_initial_update;
707 this.received_initial_update = true;
708 if this.snapshot.root_repo_common_dir != old_root_repo_common_dir
709 || this.snapshot.root_repo_is_linked_worktree
710 != old_root_repo_is_linked_worktree
711 || (is_first_update && this.snapshot.root_repo_common_dir.is_none())
712 {
713 cx.emit(Event::UpdatedRootRepoCommonDir {
714 old: old_root_repo_common_dir,
715 });
716 }
717 cx.notify();
718 while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
719 if this.observed_snapshot(*scan_id) {
720 let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
721 let _ = tx.send(());
722 } else {
723 break;
724 }
725 }
726 })?;
727 }
728 anyhow::Ok(())
729 })
730 .detach();
731
732 Worktree::Remote(worktree)
733 })
734 }
735
736 pub fn as_local(&self) -> Option<&LocalWorktree> {
737 if let Worktree::Local(worktree) = self {
738 Some(worktree)
739 } else {
740 None
741 }
742 }
743
744 pub fn as_remote(&self) -> Option<&RemoteWorktree> {
745 if let Worktree::Remote(worktree) = self {
746 Some(worktree)
747 } else {
748 None
749 }
750 }
751
752 pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> {
753 if let Worktree::Local(worktree) = self {
754 Some(worktree)
755 } else {
756 None
757 }
758 }
759
760 pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> {
761 if let Worktree::Remote(worktree) = self {
762 Some(worktree)
763 } else {
764 None
765 }
766 }
767
768 pub fn is_local(&self) -> bool {
769 matches!(self, Worktree::Local(_))
770 }
771
772 pub fn is_remote(&self) -> bool {
773 !self.is_local()
774 }
775
776 pub fn settings_location(&self, _: &Context<Self>) -> SettingsLocation<'static> {
777 SettingsLocation {
778 worktree_id: self.id(),
779 path: RelPath::empty(),
780 }
781 }
782
783 pub fn snapshot(&self) -> Snapshot {
784 match self {
785 Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(),
786 Worktree::Remote(worktree) => worktree.snapshot.clone(),
787 }
788 }
789
790 pub fn scan_id(&self) -> usize {
791 match self {
792 Worktree::Local(worktree) => worktree.snapshot.scan_id,
793 Worktree::Remote(worktree) => worktree.snapshot.scan_id,
794 }
795 }
796
797 pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
798 proto::WorktreeMetadata {
799 id: self.id().to_proto(),
800 root_name: self.root_name().as_unix_str().to_owned(),
801 visible: self.is_visible(),
802 abs_path: self.abs_path().to_string_lossy().into_owned(),
803 root_repo_common_dir: self
804 .root_repo_common_dir()
805 .map(|p| p.to_string_lossy().into_owned()),
806 root_repo_is_linked_worktree: self.root_repo_is_linked_worktree(),
807 }
808 }
809
810 pub fn completed_scan_id(&self) -> usize {
811 match self {
812 Worktree::Local(worktree) => worktree.snapshot.completed_scan_id,
813 Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id,
814 }
815 }
816
817 pub fn is_visible(&self) -> bool {
818 match self {
819 Worktree::Local(worktree) => worktree.visible,
820 Worktree::Remote(worktree) => worktree.visible,
821 }
822 }
823
824 pub fn replica_id(&self) -> ReplicaId {
825 match self {
826 Worktree::Local(_) => ReplicaId::LOCAL,
827 Worktree::Remote(worktree) => worktree.replica_id,
828 }
829 }
830
831 pub fn abs_path(&self) -> Arc<Path> {
832 match self {
833 Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
834 Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
835 }
836 }
837
838 pub fn root_file(&self, cx: &Context<Self>) -> Option<Arc<File>> {
839 let entry = self.root_entry()?;
840 Some(File::for_entry(entry.clone(), cx.entity()))
841 }
842
843 pub fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
844 where
845 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
846 Fut: 'static + Send + Future<Output = bool>,
847 {
848 match self {
849 Worktree::Local(this) => this.observe_updates(project_id, cx, callback),
850 Worktree::Remote(this) => this.observe_updates(project_id, cx, callback),
851 }
852 }
853
854 pub fn stop_observing_updates(&mut self) {
855 match self {
856 Worktree::Local(this) => {
857 this.update_observer.take();
858 }
859 Worktree::Remote(this) => {
860 this.update_observer.take();
861 }
862 }
863 }
864
865 pub fn wait_for_snapshot(
866 &mut self,
867 scan_id: usize,
868 ) -> impl Future<Output = Result<()>> + use<> {
869 match self {
870 Worktree::Local(this) => this.wait_for_snapshot(scan_id).boxed(),
871 Worktree::Remote(this) => this.wait_for_snapshot(scan_id).boxed(),
872 }
873 }
874
875 #[cfg(feature = "test-support")]
876 pub fn has_update_observer(&self) -> bool {
877 match self {
878 Worktree::Local(this) => this.update_observer.is_some(),
879 Worktree::Remote(this) => this.update_observer.is_some(),
880 }
881 }
882
883 pub fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
884 match self {
885 Worktree::Local(this) => this.load_file(path, cx),
886 Worktree::Remote(_) => {
887 Task::ready(Err(anyhow!("remote worktrees can't yet load files")))
888 }
889 }
890 }
891
892 pub fn load_binary_file(
893 &self,
894 path: &RelPath,
895 cx: &Context<Worktree>,
896 ) -> Task<Result<LoadedBinaryFile>> {
897 match self {
898 Worktree::Local(this) => this.load_binary_file(path, cx),
899 Worktree::Remote(_) => {
900 Task::ready(Err(anyhow!("remote worktrees can't yet load binary files")))
901 }
902 }
903 }
904
905 pub fn write_file(
906 &self,
907 path: Arc<RelPath>,
908 text: Rope,
909 line_ending: LineEnding,
910 encoding: &'static Encoding,
911 has_bom: bool,
912 cx: &Context<Worktree>,
913 ) -> Task<Result<Arc<File>>> {
914 match self {
915 Worktree::Local(this) => {
916 this.write_file(path, text, line_ending, encoding, has_bom, cx)
917 }
918 Worktree::Remote(_) => {
919 Task::ready(Err(anyhow!("remote worktree can't yet write files")))
920 }
921 }
922 }
923
924 pub fn create_entry(
925 &mut self,
926 path: Arc<RelPath>,
927 is_directory: bool,
928 content: Option<Vec<u8>>,
929 cx: &Context<Worktree>,
930 ) -> Task<Result<CreatedEntry>> {
931 let worktree_id = self.id();
932 match self {
933 Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
934 Worktree::Remote(this) => {
935 let project_id = this.project_id;
936 let request = this.client.request(proto::CreateProjectEntry {
937 worktree_id: worktree_id.to_proto(),
938 project_id,
939 path: path.as_ref().as_unix_str().to_owned(),
940 content,
941 is_directory,
942 });
943 cx.spawn(async move |this, cx| {
944 let response = request.await?;
945 match response.entry {
946 Some(entry) => this
947 .update(cx, |worktree, cx| {
948 worktree.as_remote_mut().unwrap().insert_entry(
949 entry,
950 response.worktree_scan_id as usize,
951 cx,
952 )
953 })?
954 .await
955 .map(CreatedEntry::Included),
956 None => {
957 let abs_path =
958 this.read_with(cx, |worktree, _| worktree.absolutize(&path))?;
959 Ok(CreatedEntry::Excluded { abs_path })
960 }
961 }
962 })
963 }
964 }
965 }
966
967 pub fn trash_entry(
968 &mut self,
969 entry_id: ProjectEntryId,
970 cx: &mut Context<Worktree>,
971 ) -> Option<Task<Result<TrashId>>> {
972 let entry = match self {
973 Worktree::Local(this) => this.entry_for_id(entry_id),
974 Worktree::Remote(this) => this.entry_for_id(entry_id),
975 }?
976 .clone();
977
978 let task = match self {
979 Worktree::Local(this) => this.trash_entry(entry.clone(), cx),
980 Worktree::Remote(this) => this.trash_entry(entry_id, cx),
981 };
982
983 let mut ids = vec![entry_id];
984 self.get_children_ids_recursive(&entry.path, &mut ids);
985
986 for id in ids {
987 cx.emit(Event::DeletedEntry(id));
988 }
989 Some(task)
990 }
991
992 pub fn delete_entry(
993 &mut self,
994 entry_id: ProjectEntryId,
995 cx: &mut Context<Worktree>,
996 ) -> Option<Task<Result<()>>> {
997 let entry = match self {
998 Worktree::Local(this) => this.entry_for_id(entry_id),
999 Worktree::Remote(this) => this.entry_for_id(entry_id),
1000 }?
1001 .clone();
1002
1003 let task = match self {
1004 Worktree::Local(this) => this.delete_entry(entry.clone(), cx),
1005 Worktree::Remote(this) => this.delete_entry(entry_id, cx),
1006 };
1007
1008 let mut ids = vec![entry_id];
1009 let path = entry.path;
1010
1011 self.get_children_ids_recursive(&path, &mut ids);
1012
1013 for id in ids {
1014 cx.emit(Event::DeletedEntry(id));
1015 }
1016 Some(task)
1017 }
1018
1019 pub fn restore_entry(
1020 &mut self,
1021 trash_id: TrashId,
1022 cx: &mut Context<'_, Worktree>,
1023 ) -> Task<Result<Entry>> {
1024 match self {
1025 Worktree::Local(this) => this.restore_entry(trash_id, cx),
1026 Worktree::Remote(this) => this.restore_entry(trash_id, cx),
1027 }
1028 }
1029
1030 fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec<ProjectEntryId>) {
1031 let children_iter = self.child_entries(path);
1032 for child in children_iter {
1033 ids.push(child.id);
1034 self.get_children_ids_recursive(&child.path, ids);
1035 }
1036 }
1037
1038 pub fn copy_external_entries(
1039 &mut self,
1040 target_directory: Arc<RelPath>,
1041 paths: Vec<Arc<Path>>,
1042 fs: Arc<dyn Fs>,
1043 cx: &Context<Worktree>,
1044 ) -> Task<Result<Vec<ProjectEntryId>>> {
1045 match self {
1046 Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx),
1047 Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx),
1048 }
1049 }
1050
1051 pub fn expand_entry(
1052 &mut self,
1053 entry_id: ProjectEntryId,
1054 cx: &Context<Worktree>,
1055 ) -> Option<Task<Result<()>>> {
1056 match self {
1057 Worktree::Local(this) => this.expand_entry(entry_id, cx),
1058 Worktree::Remote(this) => {
1059 let response = this.client.request(proto::ExpandProjectEntry {
1060 project_id: this.project_id,
1061 entry_id: entry_id.to_proto(),
1062 });
1063 Some(cx.spawn(async move |this, cx| {
1064 let response = response.await?;
1065 this.update(cx, |this, _| {
1066 this.as_remote_mut()
1067 .unwrap()
1068 .wait_for_snapshot(response.worktree_scan_id as usize)
1069 })?
1070 .await?;
1071 Ok(())
1072 }))
1073 }
1074 }
1075 }
1076
1077 pub fn expand_all_for_entry(
1078 &mut self,
1079 entry_id: ProjectEntryId,
1080 cx: &Context<Worktree>,
1081 ) -> Option<Task<Result<()>>> {
1082 match self {
1083 Worktree::Local(this) => this.expand_all_for_entry(entry_id, cx),
1084 Worktree::Remote(this) => {
1085 let response = this.client.request(proto::ExpandAllForProjectEntry {
1086 project_id: this.project_id,
1087 entry_id: entry_id.to_proto(),
1088 });
1089 Some(cx.spawn(async move |this, cx| {
1090 let response = response.await?;
1091 this.update(cx, |this, _| {
1092 this.as_remote_mut()
1093 .unwrap()
1094 .wait_for_snapshot(response.worktree_scan_id as usize)
1095 })?
1096 .await?;
1097 Ok(())
1098 }))
1099 }
1100 }
1101 }
1102
1103 pub async fn handle_create_entry(
1104 this: Entity<Self>,
1105 request: proto::CreateProjectEntry,
1106 mut cx: AsyncApp,
1107 ) -> Result<proto::ProjectEntryResponse> {
1108 let (scan_id, entry) = this.update(&mut cx, |this, cx| {
1109 anyhow::Ok((
1110 this.scan_id(),
1111 this.create_entry(
1112 RelPath::from_unix_str(&request.path)
1113 .with_context(|| {
1114 format!("received invalid relative path {:?}", request.path)
1115 })?
1116 .into(),
1117 request.is_directory,
1118 request.content,
1119 cx,
1120 ),
1121 ))
1122 })?;
1123 Ok(proto::ProjectEntryResponse {
1124 entry: match &entry.await? {
1125 CreatedEntry::Included(entry) => Some(entry.into()),
1126 CreatedEntry::Excluded { .. } => None,
1127 },
1128 worktree_scan_id: scan_id as u64,
1129 })
1130 }
1131
1132 pub async fn handle_trash_entry(
1133 this: Entity<Self>,
1134 request: proto::TrashProjectEntry,
1135 mut cx: AsyncApp,
1136 ) -> Result<proto::TrashProjectEntryResponse> {
1137 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1138 (
1139 this.scan_id(),
1140 this.trash_entry(ProjectEntryId::from_proto(request.entry_id), cx),
1141 )
1142 });
1143 let trash_id = task
1144 .ok_or_else(|| anyhow::anyhow!("invalid entry"))?
1145 .await?;
1146
1147 Ok(proto::TrashProjectEntryResponse {
1148 trash_id: trash_id.to_proto(),
1149 worktree_scan_id: scan_id as u64,
1150 })
1151 }
1152
1153 pub async fn handle_delete_entry(
1154 this: Entity<Self>,
1155 request: proto::DeleteProjectEntry,
1156 mut cx: AsyncApp,
1157 ) -> Result<proto::ProjectEntryResponse> {
1158 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1159 // While the `use_trash` field is deprecated but not removed, we
1160 // still need to support either trashing or deleting the file.
1161 // Otherwise, if an older client sends the `DeleteProjectEntry {
1162 // use_trash: true }` rather than the newer `TrashProjectEntry`, and
1163 // the flag was ignored, we'd permanently delete a file that was
1164 // actually meant to be trashed.
1165 #[allow(deprecated)]
1166 let task = if request.use_trash {
1167 this.trash_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1168 .map(|task| cx.background_spawn(async move { task.await.map(|_| ()) }))
1169 } else {
1170 this.delete_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1171 };
1172
1173 (this.scan_id(), task)
1174 });
1175 task.ok_or_else(|| anyhow::anyhow!("invalid entry"))?
1176 .await?;
1177 Ok(proto::ProjectEntryResponse {
1178 entry: None,
1179 worktree_scan_id: scan_id as u64,
1180 })
1181 }
1182
1183 pub async fn handle_restore_entry(
1184 this: Entity<Self>,
1185 request: proto::RestoreProjectEntry,
1186 mut cx: AsyncApp,
1187 ) -> Result<proto::RestoreProjectEntryResponse> {
1188 let (scan_id, task) = this.update(&mut cx, |this, cx| {
1189 (
1190 this.scan_id(),
1191 this.restore_entry(TrashId::from_proto(request.trash_id), cx),
1192 )
1193 });
1194
1195 let entry = task.await?;
1196
1197 Ok(proto::RestoreProjectEntryResponse {
1198 entry: Some(proto::Entry::from(&entry)),
1199 worktree_scan_id: scan_id as u64,
1200 })
1201 }
1202
1203 pub async fn handle_expand_entry(
1204 this: Entity<Self>,
1205 request: proto::ExpandProjectEntry,
1206 mut cx: AsyncApp,
1207 ) -> Result<proto::ExpandProjectEntryResponse> {
1208 let task = this.update(&mut cx, |this, cx| {
1209 this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1210 });
1211 task.ok_or_else(|| anyhow::anyhow!("no such entry"))?
1212 .await?;
1213 let scan_id = this.read_with(&cx, |this, _| this.scan_id());
1214 Ok(proto::ExpandProjectEntryResponse {
1215 worktree_scan_id: scan_id as u64,
1216 })
1217 }
1218
1219 pub async fn handle_expand_all_for_entry(
1220 this: Entity<Self>,
1221 request: proto::ExpandAllForProjectEntry,
1222 mut cx: AsyncApp,
1223 ) -> Result<proto::ExpandAllForProjectEntryResponse> {
1224 let task = this.update(&mut cx, |this, cx| {
1225 this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx)
1226 });
1227 task.ok_or_else(|| anyhow::anyhow!("no such entry"))?
1228 .await?;
1229 let scan_id = this.read_with(&cx, |this, _| this.scan_id());
1230 Ok(proto::ExpandAllForProjectEntryResponse {
1231 worktree_scan_id: scan_id as u64,
1232 })
1233 }
1234
1235 pub fn is_single_file(&self) -> bool {
1236 self.root_dir().is_none()
1237 }
1238
1239 /// For visible worktrees, returns the path with the worktree name as the first component.
1240 /// Otherwise, returns an absolute path.
1241 pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf {
1242 if self.is_visible() {
1243 self.root_name()
1244 .join(worktree_relative_path)
1245 .display(self.path_style)
1246 .to_string()
1247 .into()
1248 } else {
1249 let full_path = self.abs_path();
1250 let mut full_path_string = if self.is_local()
1251 && let Ok(stripped) = full_path.strip_prefix(home_dir())
1252 {
1253 self.path_style
1254 .join("~", &*stripped.to_string_lossy())
1255 .unwrap()
1256 } else {
1257 full_path.to_string_lossy().into_owned()
1258 };
1259
1260 if worktree_relative_path.components().next().is_some() {
1261 full_path_string.push_str(self.path_style.primary_separator());
1262 full_path_string.push_str(&worktree_relative_path.display(self.path_style));
1263 }
1264
1265 full_path_string.into()
1266 }
1267 }
1268}
1269
1270impl LocalWorktree {
1271 pub fn fs(&self) -> &Arc<dyn Fs> {
1272 &self.fs
1273 }
1274
1275 pub fn is_path_private(&self, path: &RelPath) -> bool {
1276 !self.share_private_files && self.settings.is_path_private(path)
1277 }
1278
1279 pub fn fs_is_case_sensitive(&self) -> bool {
1280 self.fs_case_sensitive
1281 }
1282
1283 fn restart_background_scanners(&mut self, cx: &Context<Worktree>) {
1284 let (scan_requests_tx, scan_requests_rx) = async_channel::unbounded();
1285 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = async_channel::unbounded();
1286 self.scan_requests_tx = scan_requests_tx;
1287 self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
1288
1289 self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
1290 let always_included_entries = mem::take(&mut self.snapshot.always_included_entries);
1291 log::debug!(
1292 "refreshing entries for the following always included paths: {:?}",
1293 always_included_entries
1294 );
1295
1296 // Cleans up old always included entries to ensure they get updated properly. Otherwise,
1297 // nested always included entries may not get updated and will result in out-of-date info.
1298 self.refresh_entries_for_paths(always_included_entries);
1299 }
1300
1301 fn start_background_scanner(
1302 &mut self,
1303 scan_requests_rx: async_channel::Receiver<ScanRequest>,
1304 path_prefixes_to_scan_rx: async_channel::Receiver<PathPrefixScanRequest>,
1305 cx: &Context<Worktree>,
1306 ) {
1307 let snapshot = self.snapshot();
1308 let share_private_files = self.share_private_files;
1309 let next_entry_id = self.next_entry_id.clone();
1310 let fs = self.fs.clone();
1311 let scanning_enabled = self.scanning_enabled;
1312 let force_defer_watch = self.force_defer_watch;
1313 let track_git_repositories = self.visible;
1314 let settings = self.settings.clone();
1315 let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
1316 let background_scanner = cx.background_spawn({
1317 let abs_path = snapshot.abs_path.as_path().to_path_buf();
1318 let background = cx.background_executor().clone();
1319 async move {
1320 let defer_watch =
1321 force_defer_watch || (scanning_enabled && fs::requires_poll_watcher(&abs_path));
1322
1323 let (events, watcher) = if scanning_enabled && !defer_watch {
1324 fs.watch(&abs_path, FS_WATCH_LATENCY).await
1325 } else {
1326 (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _)
1327 };
1328 let fs_case_sensitive = fs.is_case_sensitive().await;
1329
1330 let is_single_file = snapshot.snapshot.root_dir().is_none();
1331 let mut scanner = BackgroundScanner {
1332 fs,
1333 fs_case_sensitive,
1334 status_updates_tx: scan_states_tx,
1335 executor: background,
1336 scan_requests_rx,
1337 path_prefixes_to_scan_rx,
1338 next_entry_id,
1339 state: async_lock::Mutex::new(BackgroundScannerState {
1340 prev_snapshot: snapshot.snapshot.clone(),
1341 snapshot,
1342 symlink_paths_by_target: Default::default(),
1343 scanned_dirs: Default::default(),
1344 watched_dir_abs_paths_by_entry_id: Default::default(),
1345 scanning_enabled,
1346 path_prefixes_to_scan: Default::default(),
1347 paths_to_scan: Default::default(),
1348 removed_entries: RemovedEntries::default(),
1349 changed_paths: Default::default(),
1350 }),
1351 phase: BackgroundScannerPhase::InitialScan,
1352 share_private_files,
1353 settings,
1354 watcher,
1355 track_git_repositories,
1356 is_single_file,
1357 defer_watch,
1358 };
1359
1360 scanner.run(events).await;
1361 }
1362 });
1363 let scan_state_updater = cx.spawn(async move |this, cx| {
1364 while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
1365 this.update(cx, |this, cx| {
1366 let this = this.as_local_mut().unwrap();
1367 match state {
1368 ScanState::Started => {
1369 *this.is_scanning.0.borrow_mut() = true;
1370 }
1371 ScanState::Updated {
1372 snapshot,
1373 changes,
1374 barrier,
1375 scanning,
1376 } => {
1377 *this.is_scanning.0.borrow_mut() = scanning;
1378 this.set_snapshot(snapshot, changes, cx);
1379 drop(barrier);
1380 }
1381 ScanState::RootUpdated { new_path } => {
1382 this.update_abs_path_and_refresh(new_path, cx);
1383 }
1384 ScanState::RootDeleted => {
1385 log::info!(
1386 "worktree root {} no longer exists, closing worktree",
1387 this.abs_path().display()
1388 );
1389 cx.emit(Event::Deleted);
1390 }
1391 }
1392 });
1393 }
1394 });
1395 self._background_scanner_tasks = vec![background_scanner, scan_state_updater];
1396 *self.is_scanning.0.borrow_mut() = true;
1397 }
1398
1399 fn set_snapshot(
1400 &mut self,
1401 mut new_snapshot: LocalSnapshot,
1402 entry_changes: UpdatedEntriesSet,
1403 cx: &mut Context<Worktree>,
1404 ) {
1405 let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot);
1406
1407 if let Some((common_dir, is_linked_worktree)) = new_snapshot
1408 .local_repo_for_work_directory_path(RelPath::empty())
1409 .map(|repo| {
1410 (
1411 SanitizedPath::from_arc(repo.common_dir_abs_path.clone()),
1412 repo.repository_dir_abs_path != repo.common_dir_abs_path,
1413 )
1414 })
1415 {
1416 new_snapshot.root_repo_common_dir = Some(common_dir);
1417 new_snapshot.root_repo_is_linked_worktree = is_linked_worktree;
1418 } else {
1419 new_snapshot.root_repo_common_dir = None;
1420 new_snapshot.root_repo_is_linked_worktree = false;
1421 }
1422
1423 let root_repo_metadata_changed = self.snapshot.root_repo_common_dir
1424 != new_snapshot.root_repo_common_dir
1425 || self.snapshot.root_repo_is_linked_worktree
1426 != new_snapshot.root_repo_is_linked_worktree;
1427 let old_root_repo_common_dir =
1428 root_repo_metadata_changed.then(|| self.snapshot.root_repo_common_dir.clone());
1429 self.snapshot = new_snapshot;
1430
1431 if let Some(share) = self.update_observer.as_mut() {
1432 share
1433 .snapshots_tx
1434 .unbounded_send((self.snapshot.clone(), entry_changes.clone()))
1435 .ok();
1436 }
1437
1438 if !entry_changes.is_empty() {
1439 cx.emit(Event::UpdatedEntries(entry_changes));
1440 }
1441 if !repo_changes.is_empty() {
1442 cx.emit(Event::UpdatedGitRepositories(repo_changes));
1443 }
1444 if let Some(old) = old_root_repo_common_dir {
1445 cx.emit(Event::UpdatedRootRepoCommonDir { old });
1446 }
1447
1448 while let Some((scan_id, _)) = self.snapshot_subscriptions.front() {
1449 if self.snapshot.completed_scan_id >= *scan_id {
1450 let (_, tx) = self.snapshot_subscriptions.pop_front().unwrap();
1451 tx.send(()).ok();
1452 } else {
1453 break;
1454 }
1455 }
1456 }
1457
1458 fn changed_repos(
1459 &self,
1460 old_snapshot: &LocalSnapshot,
1461 new_snapshot: &mut LocalSnapshot,
1462 ) -> UpdatedGitRepositoriesSet {
1463 let mut changes = Vec::new();
1464 let mut old_repos = old_snapshot.git_repositories.iter().peekable();
1465 let new_repos = new_snapshot.git_repositories.clone();
1466 let mut new_repos = new_repos.iter().peekable();
1467
1468 loop {
1469 match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
1470 (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
1471 match Ord::cmp(&new_entry_id, &old_entry_id) {
1472 Ordering::Less => {
1473 changes.push(UpdatedGitRepository {
1474 work_directory_id: new_entry_id,
1475 old_work_directory_abs_path: None,
1476 new_work_directory_abs_path: Some(
1477 new_repo.work_directory_abs_path.clone(),
1478 ),
1479 dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
1480 repository_dir_abs_path: Some(
1481 new_repo.repository_dir_abs_path.clone(),
1482 ),
1483 common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
1484 });
1485 new_repos.next();
1486 }
1487 Ordering::Equal => {
1488 // A change to a repository's git state is signaled by bumping
1489 // `git_dir_scan_id`, and the diff below detects it via `!=`. If the
1490 // value ever regresses (e.g. a rescan re-inserting the repository
1491 // with a fresh scan id of 0), a bump from the same cycle is wiped
1492 // out and the corresponding git update is silently lost.
1493 debug_assert!(
1494 new_repo.git_dir_scan_id >= old_repo.git_dir_scan_id,
1495 "git_dir_scan_id for repository at {:?} regressed from {} to {}",
1496 new_repo.work_directory_abs_path,
1497 old_repo.git_dir_scan_id,
1498 new_repo.git_dir_scan_id,
1499 );
1500 if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id
1501 || new_repo.work_directory_abs_path
1502 != old_repo.work_directory_abs_path
1503 {
1504 changes.push(UpdatedGitRepository {
1505 work_directory_id: new_entry_id,
1506 old_work_directory_abs_path: Some(
1507 old_repo.work_directory_abs_path.clone(),
1508 ),
1509 new_work_directory_abs_path: Some(
1510 new_repo.work_directory_abs_path.clone(),
1511 ),
1512 dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
1513 repository_dir_abs_path: Some(
1514 new_repo.repository_dir_abs_path.clone(),
1515 ),
1516 common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
1517 });
1518 }
1519 new_repos.next();
1520 old_repos.next();
1521 }
1522 Ordering::Greater => {
1523 changes.push(UpdatedGitRepository {
1524 work_directory_id: old_entry_id,
1525 old_work_directory_abs_path: Some(
1526 old_repo.work_directory_abs_path.clone(),
1527 ),
1528 new_work_directory_abs_path: None,
1529 dot_git_abs_path: None,
1530 repository_dir_abs_path: None,
1531 common_dir_abs_path: None,
1532 });
1533 old_repos.next();
1534 }
1535 }
1536 }
1537 (Some((entry_id, repo)), None) => {
1538 changes.push(UpdatedGitRepository {
1539 work_directory_id: entry_id,
1540 old_work_directory_abs_path: None,
1541 new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
1542 dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
1543 repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
1544 common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
1545 });
1546 new_repos.next();
1547 }
1548 (None, Some((entry_id, repo))) => {
1549 changes.push(UpdatedGitRepository {
1550 work_directory_id: entry_id,
1551 old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
1552 new_work_directory_abs_path: None,
1553 dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
1554 repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
1555 common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
1556 });
1557 old_repos.next();
1558 }
1559 (None, None) => break,
1560 }
1561 }
1562
1563 fn clone<T: Clone, U: Clone>(value: &(&T, &U)) -> (T, U) {
1564 (value.0.clone(), value.1.clone())
1565 }
1566
1567 changes.into()
1568 }
1569
1570 pub fn scan_complete(&self) -> impl Future<Output = ()> + use<> {
1571 let mut is_scanning_rx = self.is_scanning.1.clone();
1572 async move {
1573 let mut is_scanning = *is_scanning_rx.borrow();
1574 while is_scanning {
1575 if let Some(value) = is_scanning_rx.recv().await {
1576 is_scanning = value;
1577 } else {
1578 break;
1579 }
1580 }
1581 }
1582 }
1583
1584 pub fn wait_for_snapshot(
1585 &mut self,
1586 scan_id: usize,
1587 ) -> impl Future<Output = Result<()>> + use<> {
1588 let (tx, rx) = oneshot::channel();
1589 if self.snapshot.completed_scan_id >= scan_id {
1590 tx.send(()).ok();
1591 } else {
1592 match self
1593 .snapshot_subscriptions
1594 .binary_search_by_key(&scan_id, |probe| probe.0)
1595 {
1596 Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
1597 }
1598 }
1599
1600 async move {
1601 rx.await?;
1602 Ok(())
1603 }
1604 }
1605
1606 pub fn snapshot(&self) -> LocalSnapshot {
1607 self.snapshot.clone()
1608 }
1609
1610 pub fn settings(&self) -> WorktreeSettings {
1611 self.settings.clone()
1612 }
1613
1614 fn load_binary_file(
1615 &self,
1616 path: &RelPath,
1617 cx: &Context<Worktree>,
1618 ) -> Task<Result<LoadedBinaryFile>> {
1619 let path = Arc::from(path);
1620 let abs_path = self.absolutize(&path);
1621 let fs = self.fs.clone();
1622 let entry = self.refresh_entry(path.clone(), None, cx);
1623 let is_private = self.is_path_private(&path);
1624
1625 let worktree = cx.weak_entity();
1626 cx.background_spawn(async move {
1627 let content = fs.load_bytes(&abs_path).await?;
1628
1629 let worktree = worktree.upgrade().context("worktree was dropped")?;
1630 let file = match entry.await? {
1631 Some(entry) => File::for_entry(entry, worktree),
1632 None => {
1633 let metadata = fs
1634 .metadata(&abs_path)
1635 .await
1636 .with_context(|| {
1637 format!("Loading metadata for excluded file {abs_path:?}")
1638 })?
1639 .with_context(|| {
1640 format!("Excluded file {abs_path:?} got removed during loading")
1641 })?;
1642 Arc::new(File {
1643 entry_id: None,
1644 worktree,
1645 path,
1646 disk_state: DiskState::Present {
1647 mtime: metadata.mtime,
1648 size: metadata.len,
1649 },
1650 is_local: true,
1651 is_private,
1652 })
1653 }
1654 };
1655
1656 Ok(LoadedBinaryFile { file, content })
1657 })
1658 }
1659
1660 #[ztracing::instrument(skip_all)]
1661 fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
1662 let path = Arc::from(path);
1663 let abs_path = self.absolutize(&path);
1664 let fs = self.fs.clone();
1665 let entry = self.refresh_entry(path.clone(), None, cx);
1666 let is_private = self.is_path_private(path.as_ref());
1667
1668 let this = cx.weak_entity();
1669 cx.background_spawn(async move {
1670 // WARN: Temporary workaround for #27283.
1671 // We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file
1672 // Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file
1673 // if it is too large
1674 // 5GB seems to be more reasonable, peaking at ~16GB, while 6GB jumps up to >24GB which seems like a
1675 // reasonable limit
1676 const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB
1677 let metadata = fs.metadata(&abs_path).await?;
1678 if let Some(metadata) = metadata.as_ref()
1679 && metadata.len >= FILE_SIZE_MAX
1680 {
1681 anyhow::bail!("File is too large to load");
1682 }
1683 let (text, encoding, has_bom) = decode_file_text(fs.as_ref(), &abs_path).await?;
1684 let is_writable = metadata.is_some_and(|metadata| metadata.is_writable);
1685
1686 let worktree = this.upgrade().context("worktree was dropped")?;
1687 let file = match entry.await? {
1688 Some(entry) => File::for_entry(entry, worktree),
1689 None => {
1690 let metadata = fs
1691 .metadata(&abs_path)
1692 .await
1693 .with_context(|| {
1694 format!("Loading metadata for excluded file {abs_path:?}")
1695 })?
1696 .with_context(|| {
1697 format!("Excluded file {abs_path:?} got removed during loading")
1698 })?;
1699 Arc::new(File {
1700 entry_id: None,
1701 worktree,
1702 path,
1703 disk_state: DiskState::Present {
1704 mtime: metadata.mtime,
1705 size: metadata.len,
1706 },
1707 is_local: true,
1708 is_private,
1709 })
1710 }
1711 };
1712
1713 Ok(LoadedFile {
1714 file,
1715 text,
1716 encoding,
1717 has_bom,
1718 is_writable,
1719 })
1720 })
1721 }
1722
1723 /// Find the lowest path in the worktree's datastructures that is an ancestor
1724 fn lowest_ancestor(&self, path: &RelPath) -> Arc<RelPath> {
1725 let mut lowest_ancestor = None;
1726 for path in path.ancestors() {
1727 if self.entry_for_path(path).is_some() {
1728 lowest_ancestor = Some(path.into());
1729 break;
1730 }
1731 }
1732
1733 lowest_ancestor.unwrap_or_else(|| RelPath::empty_arc())
1734 }
1735
1736 pub fn create_entry(
1737 &self,
1738 path: Arc<RelPath>,
1739 is_dir: bool,
1740 content: Option<Vec<u8>>,
1741 cx: &Context<Worktree>,
1742 ) -> Task<Result<CreatedEntry>> {
1743 let abs_path = self.absolutize(&path);
1744 let path_excluded = self.settings.is_path_excluded(&path);
1745 let fs = self.fs.clone();
1746 let task_abs_path = abs_path.clone();
1747 let write = cx.background_spawn(async move {
1748 if is_dir {
1749 fs.create_dir(&task_abs_path)
1750 .await
1751 .with_context(|| format!("creating directory {task_abs_path:?}"))
1752 } else {
1753 fs.write(&task_abs_path, content.as_deref().unwrap_or(&[]))
1754 .await
1755 .with_context(|| format!("creating file {task_abs_path:?}"))
1756 }
1757 });
1758
1759 let lowest_ancestor = self.lowest_ancestor(&path);
1760 cx.spawn(async move |this, cx| {
1761 write.await?;
1762 if path_excluded {
1763 return Ok(CreatedEntry::Excluded { abs_path });
1764 }
1765
1766 let (result, refreshes) = this.update(cx, |this, cx| {
1767 let mut refreshes = Vec::new();
1768 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
1769 for refresh_path in refresh_paths.ancestors() {
1770 if refresh_path == RelPath::empty() {
1771 continue;
1772 }
1773 let refresh_full_path = lowest_ancestor.join(refresh_path);
1774
1775 refreshes.push(this.as_local_mut().unwrap().refresh_entry(
1776 refresh_full_path.into(),
1777 None,
1778 cx,
1779 ));
1780 }
1781 (
1782 this.as_local_mut().unwrap().refresh_entry(path, None, cx),
1783 refreshes,
1784 )
1785 })?;
1786 for refresh in refreshes {
1787 refresh.await.log_err();
1788 }
1789
1790 Ok(result
1791 .await?
1792 .map(CreatedEntry::Included)
1793 .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
1794 })
1795 }
1796
1797 pub fn write_file(
1798 &self,
1799 path: Arc<RelPath>,
1800 text: Rope,
1801 line_ending: LineEnding,
1802 encoding: &'static Encoding,
1803 has_bom: bool,
1804 cx: &Context<Worktree>,
1805 ) -> Task<Result<Arc<File>>> {
1806 let fs = self.fs.clone();
1807 let is_private = self.is_path_private(&path);
1808 let abs_path = self.absolutize(&path);
1809
1810 let write = cx.background_spawn({
1811 let fs = fs.clone();
1812 let abs_path = abs_path.clone();
1813 async move {
1814 // For UTF-8, use the optimized `fs.save` which writes Rope chunks directly to disk
1815 // without allocating a contiguous string.
1816 if encoding == encoding_rs::UTF_8 && !has_bom {
1817 return fs.save(&abs_path, &text, line_ending).await;
1818 }
1819
1820 // For legacy encodings (e.g. Shift-JIS), we fall back to converting the entire Rope
1821 // to a String/Bytes in memory before writing.
1822 //
1823 // Note: This is inefficient for very large files compared to the streaming approach above,
1824 // but supporting streaming writes for arbitrary encodings would require a significant
1825 // refactor of the `fs` crate to expose a Writer interface.
1826 let text_string = text.to_string();
1827 let normalized_text = match line_ending {
1828 LineEnding::Unix => text_string,
1829 LineEnding::Windows => text_string.replace('\n', "\r\n"),
1830 };
1831
1832 // Create the byte vector manually for UTF-16 encodings because encoding_rs encodes to UTF-8 by default (per WHATWG standards),
1833 // which is not what we want for saving files.
1834 let bytes = if encoding == encoding_rs::UTF_16BE {
1835 let mut data = Vec::with_capacity(normalized_text.len() * 2 + 2);
1836 if has_bom {
1837 data.extend_from_slice(&[0xFE, 0xFF]); // BOM
1838 }
1839 let utf16be_bytes =
1840 normalized_text.encode_utf16().flat_map(|u| u.to_be_bytes());
1841 data.extend(utf16be_bytes);
1842 data.into()
1843 } else if encoding == encoding_rs::UTF_16LE {
1844 let mut data = Vec::with_capacity(normalized_text.len() * 2 + 2);
1845 if has_bom {
1846 data.extend_from_slice(&[0xFF, 0xFE]); // BOM
1847 }
1848 let utf16le_bytes =
1849 normalized_text.encode_utf16().flat_map(|u| u.to_le_bytes());
1850 data.extend(utf16le_bytes);
1851 data.into()
1852 } else {
1853 // For other encodings (Shift-JIS, UTF-8 with BOM, etc.), delegate to encoding_rs.
1854 let bom_bytes = if has_bom {
1855 if encoding == encoding_rs::UTF_8 {
1856 vec![0xEF, 0xBB, 0xBF]
1857 } else {
1858 vec![]
1859 }
1860 } else {
1861 vec![]
1862 };
1863 let (cow, _, _) = encoding.encode(&normalized_text);
1864 if !bom_bytes.is_empty() {
1865 let mut bytes = bom_bytes;
1866 bytes.extend_from_slice(&cow);
1867 bytes.into()
1868 } else {
1869 cow
1870 }
1871 };
1872
1873 fs.write(&abs_path, &bytes).await
1874 }
1875 });
1876
1877 cx.spawn(async move |this, cx| {
1878 write.await?;
1879 let entry = this
1880 .update(cx, |this, cx| {
1881 this.as_local_mut()
1882 .unwrap()
1883 .refresh_entry(path.clone(), None, cx)
1884 })?
1885 .await?;
1886 let worktree = this.upgrade().context("worktree dropped")?;
1887 if let Some(entry) = entry {
1888 Ok(File::for_entry(entry, worktree))
1889 } else {
1890 let metadata = fs
1891 .metadata(&abs_path)
1892 .await
1893 .with_context(|| {
1894 format!("Fetching metadata after saving the excluded buffer {abs_path:?}")
1895 })?
1896 .with_context(|| {
1897 format!("Excluded buffer {path:?} got removed during saving")
1898 })?;
1899 Ok(Arc::new(File {
1900 worktree,
1901 path,
1902 disk_state: DiskState::Present {
1903 mtime: metadata.mtime,
1904 size: metadata.len,
1905 },
1906 entry_id: None,
1907 is_local: true,
1908 is_private,
1909 }))
1910 }
1911 })
1912 }
1913
1914 pub fn trash_entry(&self, entry: Entry, cx: &Context<Worktree>) -> Task<Result<TrashId>> {
1915 let abs_path = self.absolutize(&entry.path);
1916 let fs = self.fs.clone();
1917
1918 cx.spawn(async move |this, cx| {
1919 let trash_id = if entry.is_file() {
1920 fs.trash(&abs_path, Default::default()).await?
1921 } else {
1922 fs.trash(
1923 &abs_path,
1924 RemoveOptions {
1925 recursive: true,
1926 ignore_if_not_exists: false,
1927 },
1928 )
1929 .await?
1930 };
1931
1932 this.update(cx, |this, _| {
1933 this.as_local_mut()
1934 .unwrap()
1935 .refresh_entries_for_paths(vec![entry.path])
1936 })?
1937 .recv()
1938 .await;
1939
1940 Ok(trash_id)
1941 })
1942 }
1943
1944 pub fn delete_entry(&self, entry: Entry, cx: &Context<Worktree>) -> Task<Result<()>> {
1945 let abs_path = self.absolutize(&entry.path);
1946 let fs = self.fs.clone();
1947
1948 cx.spawn(async move |this, cx| {
1949 if entry.is_file() {
1950 fs.remove_file(&abs_path, Default::default()).await?
1951 } else {
1952 fs.remove_dir(
1953 &abs_path,
1954 RemoveOptions {
1955 recursive: true,
1956 ignore_if_not_exists: false,
1957 },
1958 )
1959 .await?
1960 };
1961
1962 this.update(cx, |this, _| {
1963 this.as_local_mut()
1964 .unwrap()
1965 .refresh_entries_for_paths(vec![entry.path])
1966 })?
1967 .recv()
1968 .await;
1969
1970 Ok(())
1971 })
1972 }
1973
1974 pub fn restore_entry(
1975 &mut self,
1976 trash_id: TrashId,
1977 cx: &mut Context<'_, Worktree>,
1978 ) -> Task<Result<Entry>> {
1979 let fs = self.fs.clone();
1980 let worktree_abs_path = self.abs_path().clone();
1981 let path_style = self.path_style();
1982
1983 cx.spawn(async move |this, cx| {
1984 let path_buf = fs.restore(trash_id).await?;
1985 let path = path_buf
1986 .strip_prefix(worktree_abs_path)
1987 .context("Could not strip prefix")?;
1988 let path = Arc::from(RelPath::new(&path, path_style)?.as_ref());
1989
1990 let entry = this
1991 .update(cx, |this, cx| {
1992 this.as_local_mut().unwrap().refresh_entry(path, None, cx)
1993 })?
1994 .await?
1995 .context("Entry not found after restore")?;
1996
1997 Ok(entry)
1998 })
1999 }
2000
2001 pub fn copy_external_entries(
2002 &self,
2003 target_directory: Arc<RelPath>,
2004 paths: Vec<Arc<Path>>,
2005 cx: &Context<Worktree>,
2006 ) -> Task<Result<Vec<ProjectEntryId>>> {
2007 let target_directory = self.absolutize(&target_directory);
2008 let worktree_path = self.abs_path().clone();
2009 let fs = self.fs.clone();
2010 let paths = paths
2011 .into_iter()
2012 .filter_map(|source| {
2013 let file_name = source.file_name()?;
2014 let mut target = target_directory.clone();
2015 target.push(file_name);
2016
2017 // Do not allow copying the same file to itself.
2018 if source.as_ref() != target.as_path() {
2019 Some((source, target))
2020 } else {
2021 None
2022 }
2023 })
2024 .collect::<Vec<_>>();
2025
2026 let paths_to_refresh = paths
2027 .iter()
2028 .filter_map(|(_, target)| {
2029 RelPath::new(
2030 target.strip_prefix(&worktree_path).ok()?,
2031 PathStyle::local(),
2032 )
2033 .ok()
2034 .map(|path| path.into_arc())
2035 })
2036 .collect::<Vec<_>>();
2037
2038 cx.spawn(async move |this, cx| {
2039 cx.background_spawn(async move {
2040 for (source, target) in paths {
2041 copy_recursive(
2042 fs.as_ref(),
2043 &source,
2044 &target,
2045 fs::CopyOptions {
2046 overwrite: true,
2047 ..Default::default()
2048 },
2049 )
2050 .await
2051 .with_context(|| {
2052 format!("Failed to copy file from {source:?} to {target:?}")
2053 })?;
2054 }
2055 anyhow::Ok(())
2056 })
2057 .await
2058 .log_err();
2059 let mut refresh = cx.read_entity(
2060 &this.upgrade().with_context(|| "Dropped worktree")?,
2061 |this, _| {
2062 anyhow::Ok::<postage::barrier::Receiver>(
2063 this.as_local()
2064 .with_context(|| "Worktree is not local")?
2065 .refresh_entries_for_paths(paths_to_refresh.clone()),
2066 )
2067 },
2068 )?;
2069
2070 cx.background_spawn(async move {
2071 refresh.next().await;
2072 anyhow::Ok(())
2073 })
2074 .await
2075 .log_err();
2076
2077 let this = this.upgrade().with_context(|| "Dropped worktree")?;
2078 Ok(cx.read_entity(&this, |this, _| {
2079 paths_to_refresh
2080 .iter()
2081 .filter_map(|path| Some(this.entry_for_path(path)?.id))
2082 .collect()
2083 }))
2084 })
2085 }
2086
2087 fn expand_entry(
2088 &self,
2089 entry_id: ProjectEntryId,
2090 cx: &Context<Worktree>,
2091 ) -> Option<Task<Result<()>>> {
2092 let path = self.entry_for_id(entry_id)?.path.clone();
2093 let mut refresh = self.refresh_entries_for_paths(vec![path]);
2094 Some(cx.background_spawn(async move {
2095 refresh.next().await;
2096 Ok(())
2097 }))
2098 }
2099
2100 fn expand_all_for_entry(
2101 &self,
2102 entry_id: ProjectEntryId,
2103 cx: &Context<Worktree>,
2104 ) -> Option<Task<Result<()>>> {
2105 let path = self.entry_for_id(entry_id).unwrap().path.clone();
2106 let mut rx = self.add_path_prefix_to_scan(path);
2107 Some(cx.background_spawn(async move {
2108 rx.next().await;
2109 Ok(())
2110 }))
2111 }
2112
2113 pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<RelPath>>) -> barrier::Receiver {
2114 let (tx, rx) = barrier::channel();
2115 self.scan_requests_tx
2116 .try_send(ScanRequest {
2117 relative_paths: paths,
2118 done: smallvec![tx],
2119 })
2120 .ok();
2121 rx
2122 }
2123
2124 #[cfg(feature = "test-support")]
2125 pub fn manually_refresh_entries_for_paths(
2126 &self,
2127 paths: Vec<Arc<RelPath>>,
2128 ) -> barrier::Receiver {
2129 self.refresh_entries_for_paths(paths)
2130 }
2131
2132 pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<RelPath>) -> barrier::Receiver {
2133 let (tx, rx) = barrier::channel();
2134 self.path_prefixes_to_scan_tx
2135 .try_send(PathPrefixScanRequest {
2136 path: path_prefix,
2137 done: smallvec![tx],
2138 })
2139 .ok();
2140 rx
2141 }
2142
2143 pub fn refresh_entry(
2144 &self,
2145 path: Arc<RelPath>,
2146 old_path: Option<Arc<RelPath>>,
2147 cx: &Context<Worktree>,
2148 ) -> Task<Result<Option<Entry>>> {
2149 if self.settings.is_path_excluded(&path) {
2150 return Task::ready(Ok(None));
2151 }
2152 let paths = if let Some(old_path) = old_path.as_ref() {
2153 vec![old_path.clone(), path.clone()]
2154 } else {
2155 vec![path.clone()]
2156 };
2157 let t0 = Instant::now();
2158 let mut refresh = self.refresh_entries_for_paths(paths);
2159 // todo(lw): Hot foreground spawn
2160 cx.spawn(async move |this, cx| {
2161 refresh.recv().await;
2162 log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed());
2163 let new_entry = this.read_with(cx, |this, _| {
2164 this.entry_for_path(&path).cloned().with_context(|| {
2165 format!("Could not find entry in worktree for {path:?} after refresh")
2166 })
2167 })??;
2168 Ok(Some(new_entry))
2169 })
2170 }
2171
2172 pub fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
2173 where
2174 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
2175 Fut: 'static + Send + Future<Output = bool>,
2176 {
2177 if let Some(observer) = self.update_observer.as_mut() {
2178 *observer.resume_updates.borrow_mut() = ();
2179 return;
2180 }
2181
2182 let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>();
2183 let (snapshots_tx, mut snapshots_rx) =
2184 mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet)>();
2185 snapshots_tx
2186 .unbounded_send((self.snapshot(), Arc::default()))
2187 .ok();
2188
2189 let worktree_id = self.id.to_proto();
2190 let _maintain_remote_snapshot = cx.background_spawn(async move {
2191 let mut is_first = true;
2192 while let Some((snapshot, entry_changes)) = snapshots_rx.next().await {
2193 let update = if is_first {
2194 is_first = false;
2195 snapshot.build_initial_update(project_id, worktree_id)
2196 } else {
2197 snapshot.build_update(project_id, worktree_id, entry_changes)
2198 };
2199
2200 for update in proto::split_worktree_update(update) {
2201 let _ = resume_updates_rx.try_recv();
2202 loop {
2203 let result = callback(update.clone());
2204 if result.await {
2205 break;
2206 } else {
2207 log::info!("waiting to resume updates");
2208 if resume_updates_rx.next().await.is_none() {
2209 return Some(());
2210 }
2211 }
2212 }
2213 }
2214 }
2215 Some(())
2216 });
2217
2218 self.update_observer = Some(UpdateObservationState {
2219 snapshots_tx,
2220 resume_updates: resume_updates_tx,
2221 _maintain_remote_snapshot,
2222 });
2223 }
2224
2225 pub fn share_private_files(&mut self, cx: &Context<Worktree>) {
2226 self.share_private_files = true;
2227 self.restart_background_scanners(cx);
2228 }
2229
2230 pub fn update_abs_path_and_refresh(
2231 &mut self,
2232 new_path: Arc<SanitizedPath>,
2233 cx: &Context<Worktree>,
2234 ) {
2235 self.snapshot.git_repositories = Default::default();
2236 self.snapshot.ignores_by_parent_abs_path = Default::default();
2237 let root_name = new_path
2238 .as_path()
2239 .file_name()
2240 .and_then(|f| f.to_str())
2241 .map_or(RelPath::empty_arc(), |f| {
2242 RelPath::from_unix_str(f).unwrap().into()
2243 });
2244 self.snapshot.update_abs_path(new_path, root_name);
2245 self.restart_background_scanners(cx);
2246 }
2247 #[cfg(feature = "test-support")]
2248 pub fn set_defer_watch(&mut self, defer: bool, cx: &mut Context<Worktree>) {
2249 self.force_defer_watch = defer;
2250 self.restart_background_scanners(cx);
2251 }
2252
2253 #[cfg(feature = "test-support")]
2254 pub fn repositories(&self) -> Vec<Arc<Path>> {
2255 self.git_repositories
2256 .values()
2257 .map(|entry| entry.work_directory_abs_path.clone())
2258 .collect::<Vec<_>>()
2259 }
2260}
2261
2262impl RemoteWorktree {
2263 pub fn project_id(&self) -> u64 {
2264 self.project_id
2265 }
2266
2267 pub fn client(&self) -> AnyProtoClient {
2268 self.client.clone()
2269 }
2270
2271 pub fn disconnected_from_host(&mut self) {
2272 self.updates_tx.take();
2273 self.snapshot_subscriptions.clear();
2274 self.disconnected = true;
2275 }
2276
2277 pub fn update_from_remote(&self, update: proto::UpdateWorktree) {
2278 if let Some(updates_tx) = &self.updates_tx {
2279 updates_tx
2280 .unbounded_send(update)
2281 .expect("consumer runs to completion");
2282 }
2283 }
2284
2285 fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
2286 where
2287 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
2288 Fut: 'static + Send + Future<Output = bool>,
2289 {
2290 let (tx, mut rx) = mpsc::unbounded();
2291 let initial_update = self
2292 .snapshot
2293 .build_initial_update(project_id, self.id().to_proto());
2294 self.update_observer = Some(tx);
2295 cx.spawn(async move |this, cx| {
2296 let mut update = initial_update;
2297 'outer: loop {
2298 // SSH projects use a special project ID of 0, and we need to
2299 // remap it to the correct one here.
2300 update.project_id = project_id;
2301
2302 for chunk in split_worktree_update(update) {
2303 if !callback(chunk).await {
2304 break 'outer;
2305 }
2306 }
2307
2308 if let Some(next_update) = rx.next().await {
2309 update = next_update;
2310 } else {
2311 break;
2312 }
2313 }
2314 this.update(cx, |this, _| {
2315 let this = this.as_remote_mut().unwrap();
2316 this.update_observer.take();
2317 })
2318 })
2319 .detach();
2320 }
2321
2322 fn observed_snapshot(&self, scan_id: usize) -> bool {
2323 self.completed_scan_id >= scan_id
2324 }
2325
2326 pub fn wait_for_snapshot(
2327 &mut self,
2328 scan_id: usize,
2329 ) -> impl Future<Output = Result<()>> + use<> {
2330 let (tx, rx) = oneshot::channel();
2331 if self.observed_snapshot(scan_id) {
2332 let _ = tx.send(());
2333 } else if self.disconnected {
2334 drop(tx);
2335 } else {
2336 match self
2337 .snapshot_subscriptions
2338 .binary_search_by_key(&scan_id, |probe| probe.0)
2339 {
2340 Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
2341 }
2342 }
2343
2344 async move {
2345 rx.await?;
2346 Ok(())
2347 }
2348 }
2349
2350 pub fn insert_entry(
2351 &mut self,
2352 entry: proto::Entry,
2353 scan_id: usize,
2354 cx: &Context<Worktree>,
2355 ) -> Task<Result<Entry>> {
2356 let wait_for_snapshot = self.wait_for_snapshot(scan_id);
2357 cx.spawn(async move |this, cx| {
2358 wait_for_snapshot.await?;
2359 this.update(cx, |worktree, _| {
2360 let worktree = worktree.as_remote_mut().unwrap();
2361 let snapshot = &mut worktree.background_snapshot.lock().0;
2362 let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions);
2363 worktree.snapshot = snapshot.clone();
2364 entry
2365 })?
2366 })
2367 }
2368
2369 fn trash_entry(
2370 &self,
2371 entry_id: ProjectEntryId,
2372 cx: &Context<Worktree>,
2373 ) -> Task<Result<TrashId>> {
2374 let response = self.client.request(proto::TrashProjectEntry {
2375 project_id: self.project_id,
2376 entry_id: entry_id.to_proto(),
2377 });
2378
2379 cx.spawn(async move |this, cx| {
2380 let response = response.await?;
2381 let scan_id = response.worktree_scan_id as usize;
2382 let trash_id = response.trash_id;
2383
2384 this.update(cx, move |this, _| {
2385 this.as_remote_mut().unwrap().wait_for_snapshot(scan_id)
2386 })?
2387 .await?;
2388
2389 this.update(cx, |this, _| {
2390 let this = this.as_remote_mut().unwrap();
2391 let snapshot = &mut this.background_snapshot.lock().0;
2392 snapshot.delete_entry(entry_id);
2393 this.snapshot = snapshot.clone();
2394 })?;
2395
2396 Ok(TrashId::from_proto(trash_id))
2397 })
2398 }
2399
2400 fn delete_entry(&self, entry_id: ProjectEntryId, cx: &Context<Worktree>) -> Task<Result<()>> {
2401 let response = self.client.request(proto::DeleteProjectEntry {
2402 project_id: self.project_id,
2403 entry_id: entry_id.to_proto(),
2404 // The `use_trash` field is being deprecated but it's still required
2405 // in the message, hence the `#[allow(deprecated)]` attribute.
2406 #[allow(deprecated)]
2407 use_trash: false,
2408 });
2409
2410 cx.spawn(async move |this, cx| {
2411 let response = response.await?;
2412 let scan_id = response.worktree_scan_id as usize;
2413
2414 this.update(cx, move |this, _| {
2415 this.as_remote_mut().unwrap().wait_for_snapshot(scan_id)
2416 })?
2417 .await?;
2418
2419 this.update(cx, |this, _| {
2420 let this = this.as_remote_mut().unwrap();
2421 let snapshot = &mut this.background_snapshot.lock().0;
2422 snapshot.delete_entry(entry_id);
2423 this.snapshot = snapshot.clone();
2424 })
2425 })
2426 }
2427
2428 fn restore_entry(&mut self, trash_id: TrashId, cx: &Context<Worktree>) -> Task<Result<Entry>> {
2429 let project_id = self.project_id();
2430 let worktree_id = self.id().to_proto();
2431 let trash_id = trash_id.to_proto();
2432
2433 let request = self.client.request(proto::RestoreProjectEntry {
2434 project_id,
2435 worktree_id,
2436 trash_id,
2437 });
2438
2439 cx.spawn(async move |this, cx| {
2440 let response = request.await?;
2441 let scan_id = response.worktree_scan_id as usize;
2442 let proto_entry = response.entry.context("Missing entry in in response")?;
2443
2444 this.update(cx, move |worktree, cx| {
2445 worktree
2446 .as_remote_mut()
2447 .unwrap()
2448 .insert_entry(proto_entry, scan_id, cx)
2449 })?
2450 .await
2451 })
2452 }
2453
2454 fn copy_external_entries(
2455 &self,
2456 target_directory: Arc<RelPath>,
2457 paths_to_copy: Vec<Arc<Path>>,
2458 local_fs: Arc<dyn Fs>,
2459 cx: &Context<Worktree>,
2460 ) -> Task<anyhow::Result<Vec<ProjectEntryId>>> {
2461 let client = self.client.clone();
2462 let worktree_id = self.id().to_proto();
2463 let project_id = self.project_id;
2464
2465 cx.background_spawn(async move {
2466 let mut requests = Vec::new();
2467 for root_path_to_copy in paths_to_copy {
2468 let Some(filename) = root_path_to_copy
2469 .file_name()
2470 .and_then(|name| name.to_str())
2471 .and_then(|filename| RelPath::from_unix_str(filename).ok())
2472 else {
2473 continue;
2474 };
2475 for (abs_path, is_directory) in
2476 read_dir_items(local_fs.as_ref(), &root_path_to_copy).await?
2477 {
2478 let Some(relative_path) = abs_path
2479 .strip_prefix(&root_path_to_copy)
2480 .map_err(|e| anyhow::Error::from(e))
2481 .and_then(|relative_path| RelPath::new(relative_path, PathStyle::local()))
2482 .log_err()
2483 else {
2484 continue;
2485 };
2486 let content = if is_directory {
2487 None
2488 } else {
2489 Some(local_fs.load_bytes(&abs_path).await?)
2490 };
2491
2492 let mut target_path = target_directory.join(filename);
2493 if relative_path.file_name().is_some() {
2494 target_path = target_path.join(&relative_path);
2495 }
2496
2497 requests.push(proto::CreateProjectEntry {
2498 project_id,
2499 worktree_id,
2500 path: target_path.as_unix_str().to_owned(),
2501 is_directory,
2502 content,
2503 });
2504 }
2505 }
2506 requests.sort_unstable_by(|a, b| a.path.cmp(&b.path));
2507 requests.dedup();
2508
2509 let mut copied_entry_ids = Vec::new();
2510 for request in requests {
2511 let response = client.request(request).await?;
2512 copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id)));
2513 }
2514
2515 Ok(copied_entry_ids)
2516 })
2517 }
2518}
2519
2520impl Snapshot {
2521 pub fn new(
2522 id: WorktreeId,
2523 root_name: Arc<RelPath>,
2524 abs_path: Arc<Path>,
2525 path_style: PathStyle,
2526 ) -> Self {
2527 Snapshot {
2528 id,
2529 abs_path: SanitizedPath::from_arc(abs_path),
2530 path_style,
2531 root_char_bag: root_name
2532 .as_unix_str()
2533 .chars()
2534 .map(|c| c.to_ascii_lowercase())
2535 .collect(),
2536 root_name,
2537 always_included_entries: Default::default(),
2538 entries_by_path: Default::default(),
2539 entries_by_id: Default::default(),
2540 root_repo_common_dir: None,
2541 root_repo_is_linked_worktree: false,
2542 scan_id: 1,
2543 completed_scan_id: 0,
2544 }
2545 }
2546
2547 pub fn id(&self) -> WorktreeId {
2548 self.id
2549 }
2550
2551 // TODO:
2552 // Consider the following:
2553 //
2554 // ```rust
2555 // let abs_path: Arc<Path> = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project"
2556 // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs");
2557 // // The caller perform some actions here:
2558 // some_non_trimmed_path.strip_prefix(abs_path); // This fails
2559 // some_non_trimmed_path.starts_with(abs_path); // This fails too
2560 // ```
2561 //
2562 // This is definitely a bug, but it's not clear if we should handle it here or not.
2563 pub fn abs_path(&self) -> &Arc<Path> {
2564 SanitizedPath::cast_arc_ref(&self.abs_path)
2565 }
2566
2567 pub fn root_repo_common_dir(&self) -> Option<&Arc<Path>> {
2568 self.root_repo_common_dir
2569 .as_ref()
2570 .map(SanitizedPath::cast_arc_ref)
2571 }
2572
2573 pub fn root_repo_is_linked_worktree(&self) -> bool {
2574 self.root_repo_is_linked_worktree
2575 }
2576
2577 fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
2578 let mut updated_entries = self
2579 .entries_by_path
2580 .iter()
2581 .map(proto::Entry::from)
2582 .collect::<Vec<_>>();
2583 updated_entries.sort_unstable_by_key(|e| e.id);
2584
2585 proto::UpdateWorktree {
2586 project_id,
2587 worktree_id,
2588 abs_path: self.abs_path().to_string_lossy().into_owned(),
2589 root_name: self.root_name().as_unix_str().to_owned(),
2590 root_repo_common_dir: self
2591 .root_repo_common_dir()
2592 .map(|p| p.to_string_lossy().into_owned()),
2593 root_repo_is_linked_worktree: self.root_repo_is_linked_worktree,
2594 updated_entries,
2595 removed_entries: Vec::new(),
2596 scan_id: self.scan_id as u64,
2597 is_last_update: self.completed_scan_id == self.scan_id,
2598 // Sent in separate messages.
2599 updated_repositories: Vec::new(),
2600 removed_repositories: Vec::new(),
2601 }
2602 }
2603
2604 pub fn work_directory_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
2605 match work_directory {
2606 WorkDirectory::InProject { relative_path } => self.absolutize(relative_path),
2607 WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
2608 }
2609 }
2610
2611 pub fn absolutize(&self, path: &RelPath) -> PathBuf {
2612 if path.file_name().is_some() {
2613 let mut abs_path = self.abs_path.to_string();
2614 for component in path.components() {
2615 if !abs_path.ends_with(self.path_style.primary_separator()) {
2616 abs_path.push_str(self.path_style.primary_separator());
2617 }
2618 abs_path.push_str(component);
2619 }
2620 PathBuf::from(abs_path)
2621 } else {
2622 self.abs_path.as_path().to_path_buf()
2623 }
2624 }
2625
2626 pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
2627 self.entries_by_id.get(&entry_id, ()).is_some()
2628 }
2629
2630 fn insert_entry(
2631 &mut self,
2632 entry: proto::Entry,
2633 always_included_paths: &PathMatcher,
2634 ) -> Result<Entry> {
2635 let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
2636 let old_entry = self.entries_by_id.insert_or_replace(
2637 PathEntry {
2638 id: entry.id,
2639 path: entry.path.clone(),
2640 is_ignored: entry.is_ignored,
2641 scan_id: 0,
2642 },
2643 (),
2644 );
2645 if let Some(old_entry) = old_entry {
2646 self.entries_by_path.remove(&PathKey(old_entry.path), ());
2647 }
2648 self.entries_by_path.insert_or_replace(entry.clone(), ());
2649 Ok(entry)
2650 }
2651
2652 fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<RelPath>> {
2653 let removed_entry = self.entries_by_id.remove(&entry_id, ())?;
2654 self.entries_by_path = {
2655 let mut cursor = self.entries_by_path.cursor::<TraversalProgress>(());
2656 let mut new_entries_by_path =
2657 cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left);
2658 while let Some(entry) = cursor.item() {
2659 if entry.path.starts_with(&removed_entry.path) {
2660 self.entries_by_id.remove(&entry.id, ());
2661 cursor.next();
2662 } else {
2663 break;
2664 }
2665 }
2666 new_entries_by_path.append(cursor.suffix(), ());
2667 new_entries_by_path
2668 };
2669
2670 Some(removed_entry.path)
2671 }
2672
2673 fn update_abs_path(&mut self, abs_path: Arc<SanitizedPath>, root_name: Arc<RelPath>) {
2674 self.abs_path = abs_path;
2675 if root_name != self.root_name {
2676 self.root_char_bag = root_name
2677 .as_unix_str()
2678 .chars()
2679 .map(|c| c.to_ascii_lowercase())
2680 .collect();
2681 self.root_name = root_name;
2682 }
2683 }
2684
2685 pub fn apply_remote_update(
2686 &mut self,
2687 update: proto::UpdateWorktree,
2688 always_included_paths: &PathMatcher,
2689 ) {
2690 log::debug!(
2691 "applying remote worktree update. {} entries updated, {} removed",
2692 update.updated_entries.len(),
2693 update.removed_entries.len()
2694 );
2695 if let Some(root_name) = RelPath::from_unix_str(&update.root_name).log_err() {
2696 self.update_abs_path(
2697 SanitizedPath::new_arc(&Path::new(&update.abs_path)),
2698 root_name.into(),
2699 );
2700 }
2701
2702 let mut entries_by_path_edits = Vec::new();
2703 let mut entries_by_id_edits = Vec::new();
2704
2705 for entry_id in update.removed_entries {
2706 let entry_id = ProjectEntryId::from_proto(entry_id);
2707 entries_by_id_edits.push(Edit::Remove(entry_id));
2708 if let Some(entry) = self.entry_for_id(entry_id) {
2709 entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
2710 }
2711 }
2712
2713 for entry in update.updated_entries {
2714 let Some(entry) =
2715 Entry::try_from((&self.root_char_bag, always_included_paths, entry)).log_err()
2716 else {
2717 continue;
2718 };
2719 if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, ()) {
2720 entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
2721 }
2722 if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ())
2723 && old_entry.id != entry.id
2724 {
2725 entries_by_id_edits.push(Edit::Remove(old_entry.id));
2726 }
2727 entries_by_id_edits.push(Edit::Insert(PathEntry {
2728 id: entry.id,
2729 path: entry.path.clone(),
2730 is_ignored: entry.is_ignored,
2731 scan_id: 0,
2732 }));
2733 entries_by_path_edits.push(Edit::Insert(entry));
2734 }
2735
2736 self.entries_by_path.edit(entries_by_path_edits, ());
2737 self.entries_by_id.edit(entries_by_id_edits, ());
2738
2739 // A `None` from a completed scan is a real repo removal, whereas a `None`
2740 // mid-scan may just mean the sender hasn't registered the root repo yet.
2741 match update
2742 .root_repo_common_dir
2743 .map(|p| SanitizedPath::new_arc(Path::new(&p)))
2744 {
2745 Some(dir) => {
2746 self.root_repo_common_dir = Some(dir);
2747 self.root_repo_is_linked_worktree = update.root_repo_is_linked_worktree;
2748 }
2749 None if update.is_last_update => {
2750 self.root_repo_common_dir = None;
2751 self.root_repo_is_linked_worktree = false;
2752 }
2753 None => {}
2754 }
2755
2756 self.scan_id = update.scan_id as usize;
2757 if update.is_last_update {
2758 self.completed_scan_id = update.scan_id as usize;
2759 }
2760 }
2761
2762 pub fn entry_count(&self) -> usize {
2763 self.entries_by_path.summary().count
2764 }
2765
2766 pub fn visible_entry_count(&self) -> usize {
2767 self.entries_by_path.summary().non_ignored_count
2768 }
2769
2770 pub fn dir_count(&self) -> usize {
2771 let summary = self.entries_by_path.summary();
2772 summary.count - summary.file_count
2773 }
2774
2775 pub fn visible_dir_count(&self) -> usize {
2776 let summary = self.entries_by_path.summary();
2777 summary.non_ignored_count - summary.non_ignored_file_count
2778 }
2779
2780 pub fn file_count(&self) -> usize {
2781 self.entries_by_path.summary().file_count
2782 }
2783
2784 pub fn visible_file_count(&self) -> usize {
2785 self.entries_by_path.summary().non_ignored_file_count
2786 }
2787
2788 fn traverse_from_offset(
2789 &self,
2790 include_files: bool,
2791 include_dirs: bool,
2792 include_ignored: bool,
2793 start_offset: usize,
2794 ) -> Traversal<'_> {
2795 let mut cursor = self.entries_by_path.cursor(());
2796 cursor.seek(
2797 &TraversalTarget::Count {
2798 count: start_offset,
2799 include_files,
2800 include_dirs,
2801 include_ignored,
2802 },
2803 Bias::Right,
2804 );
2805 Traversal {
2806 snapshot: self,
2807 cursor,
2808 include_files,
2809 include_dirs,
2810 include_ignored,
2811 }
2812 }
2813
2814 pub fn traverse_from_path(
2815 &self,
2816 include_files: bool,
2817 include_dirs: bool,
2818 include_ignored: bool,
2819 path: &RelPath,
2820 ) -> Traversal<'_> {
2821 Traversal::new(self, include_files, include_dirs, include_ignored, path)
2822 }
2823
2824 pub fn files(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2825 self.traverse_from_offset(true, false, include_ignored, start)
2826 }
2827
2828 pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2829 self.traverse_from_offset(false, true, include_ignored, start)
2830 }
2831
2832 pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2833 self.traverse_from_offset(true, true, include_ignored, start)
2834 }
2835
2836 pub fn paths(&self) -> impl Iterator<Item = &RelPath> {
2837 self.entries_by_path
2838 .cursor::<()>(())
2839 .filter(move |entry| !entry.path.is_empty())
2840 .map(|entry| entry.path.as_ref())
2841 }
2842
2843 pub fn child_entries<'a>(&'a self, parent_path: &'a RelPath) -> ChildEntriesIter<'a> {
2844 let options = ChildEntriesOptions {
2845 include_files: true,
2846 include_dirs: true,
2847 include_ignored: true,
2848 };
2849 self.child_entries_with_options(parent_path, options)
2850 }
2851
2852 pub fn child_entries_with_options<'a>(
2853 &'a self,
2854 parent_path: &'a RelPath,
2855 options: ChildEntriesOptions,
2856 ) -> ChildEntriesIter<'a> {
2857 let mut cursor = self.entries_by_path.cursor(());
2858 cursor.seek(&TraversalTarget::path(parent_path), Bias::Right);
2859 let traversal = Traversal {
2860 snapshot: self,
2861 cursor,
2862 include_files: options.include_files,
2863 include_dirs: options.include_dirs,
2864 include_ignored: options.include_ignored,
2865 };
2866 ChildEntriesIter {
2867 traversal,
2868 parent_path,
2869 }
2870 }
2871
2872 pub fn root_entry(&self) -> Option<&Entry> {
2873 self.entries_by_path.first()
2874 }
2875
2876 /// Returns `None` for a single file worktree, or `Some(self.abs_path())` if
2877 /// it is a directory.
2878 pub fn root_dir(&self) -> Option<Arc<Path>> {
2879 self.root_entry()
2880 .filter(|entry| entry.is_dir())
2881 .map(|_| self.abs_path().clone())
2882 }
2883
2884 pub fn root_name(&self) -> &RelPath {
2885 &self.root_name
2886 }
2887
2888 pub fn root_name_str(&self) -> &str {
2889 self.root_name.as_unix_str()
2890 }
2891
2892 pub fn scan_id(&self) -> usize {
2893 self.scan_id
2894 }
2895
2896 pub fn entry_for_path(&self, path: &RelPath) -> Option<&Entry> {
2897 let entry = self.traverse_from_path(true, true, true, path).entry();
2898 entry.and_then(|entry| {
2899 if entry.path.as_ref() == path {
2900 Some(entry)
2901 } else {
2902 None
2903 }
2904 })
2905 }
2906
2907 /// Whether `path` is gitignored, or lies inside a gitignored directory.
2908 ///
2909 /// The contents of ignored directories aren't scanned until explicitly
2910 /// expanded, so when `path` has no entry this falls back to the ignore
2911 /// status of its nearest scanned ancestor.
2912 pub fn is_path_ignored(&self, path: &RelPath) -> bool {
2913 path.ancestors()
2914 .find_map(|ancestor| self.entry_for_path(ancestor))
2915 .is_some_and(|entry| entry.is_ignored)
2916 }
2917
2918 /// Resolves a path to an executable using the following heuristics:
2919 ///
2920 /// 1. If the path starts with `~`, it is expanded to the user's home directory.
2921 /// 2. If the path is relative and contains more than one component,
2922 /// it is joined to the worktree root path.
2923 /// 3. If the path is relative and exists in the worktree
2924 /// (even if falls under an exclusion filter),
2925 /// it is joined to the worktree root path.
2926 /// 4. Otherwise the path is returned unmodified.
2927 ///
2928 /// Relative paths that do not exist in the worktree may
2929 /// still be found using the `PATH` environment variable.
2930 pub fn resolve_relative_path(&self, path: PathBuf) -> PathBuf {
2931 if let Some(path_str) = path.to_str() {
2932 if let Some(remaining_path) = path_str.strip_prefix("~/") {
2933 return home_dir().join(remaining_path);
2934 } else if path_str == "~" {
2935 return home_dir().to_path_buf();
2936 }
2937 }
2938
2939 if let Ok(rel_path) = RelPath::new(&path, self.path_style)
2940 && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some())
2941 {
2942 self.abs_path().join(path)
2943 } else {
2944 path
2945 }
2946 }
2947
2948 pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
2949 let entry = self.entries_by_id.get(&id, ())?;
2950 self.entry_for_path(&entry.path)
2951 }
2952
2953 pub fn path_style(&self) -> PathStyle {
2954 self.path_style
2955 }
2956}
2957
2958impl LocalSnapshot {
2959 fn local_repo_for_work_directory_path(&self, path: &RelPath) -> Option<&LocalRepositoryEntry> {
2960 self.git_repositories
2961 .iter()
2962 .map(|(_, entry)| entry)
2963 .find(|entry| entry.work_directory.path_key() == PathKey(path.into()))
2964 }
2965
2966 fn build_update(
2967 &self,
2968 project_id: u64,
2969 worktree_id: u64,
2970 entry_changes: UpdatedEntriesSet,
2971 ) -> proto::UpdateWorktree {
2972 let mut updated_entries = Vec::new();
2973 let mut removed_entries = Vec::new();
2974
2975 for (_, entry_id, path_change) in entry_changes.iter() {
2976 if let PathChange::Removed = path_change {
2977 removed_entries.push(entry_id.0 as u64);
2978 } else if let Some(entry) = self.entry_for_id(*entry_id) {
2979 updated_entries.push(proto::Entry::from(entry));
2980 }
2981 }
2982
2983 removed_entries.sort_unstable();
2984 updated_entries.sort_unstable_by_key(|e| e.id);
2985
2986 // TODO - optimize, knowing that removed_entries are sorted.
2987 removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err());
2988
2989 proto::UpdateWorktree {
2990 project_id,
2991 worktree_id,
2992 abs_path: self.abs_path().to_string_lossy().into_owned(),
2993 root_name: self.root_name().as_unix_str().to_owned(),
2994 root_repo_common_dir: self
2995 .root_repo_common_dir()
2996 .map(|p| p.to_string_lossy().into_owned()),
2997 root_repo_is_linked_worktree: self.root_repo_is_linked_worktree,
2998 updated_entries,
2999 removed_entries,
3000 scan_id: self.scan_id as u64,
3001 is_last_update: self.completed_scan_id == self.scan_id,
3002 // Sent in separate messages.
3003 updated_repositories: Vec::new(),
3004 removed_repositories: Vec::new(),
3005 }
3006 }
3007
3008 async fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
3009 log::trace!("insert entry {:?}", entry.path);
3010 if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
3011 let abs_path = self.absolutize(&entry.path);
3012 match build_gitignore(&abs_path, fs).await {
3013 Ok(ignore) => {
3014 self.ignores_by_parent_abs_path
3015 .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
3016 }
3017 Err(error) => {
3018 log::error!(
3019 "error loading .gitignore file {:?} - {:?}",
3020 &entry.path,
3021 error
3022 );
3023 }
3024 }
3025 }
3026
3027 if entry.kind == EntryKind::PendingDir
3028 && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ())
3029 {
3030 entry.kind = existing_entry.kind;
3031 }
3032
3033 let scan_id = self.scan_id;
3034 let removed = self.entries_by_path.insert_or_replace(entry.clone(), ());
3035 if let Some(removed) = removed
3036 && removed.id != entry.id
3037 {
3038 self.entries_by_id.remove(&removed.id, ());
3039 }
3040 self.entries_by_id.insert_or_replace(
3041 PathEntry {
3042 id: entry.id,
3043 path: entry.path.clone(),
3044 is_ignored: entry.is_ignored,
3045 scan_id,
3046 },
3047 (),
3048 );
3049
3050 entry
3051 }
3052
3053 fn ancestor_inodes_for_path(&self, path: &RelPath) -> TreeSet<u64> {
3054 let mut inodes = TreeSet::default();
3055 for ancestor in path.ancestors().skip(1) {
3056 if let Some(entry) = self.entry_for_path(ancestor) {
3057 inodes.insert(entry.inode);
3058 }
3059 }
3060 inodes
3061 }
3062
3063 async fn ignore_stack_for_abs_path(
3064 &self,
3065 abs_path: &Path,
3066 is_dir: bool,
3067 fs: &dyn Fs,
3068 ) -> IgnoreStack {
3069 let mut new_ignores = Vec::new();
3070 let mut repo_excludes = Vec::new();
3071 let mut repo_root = None;
3072 for (index, ancestor) in abs_path.ancestors().enumerate() {
3073 if index > 0 {
3074 if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
3075 new_ignores.push((ancestor, Some(ignore.clone())));
3076 } else {
3077 new_ignores.push((ancestor, None));
3078 }
3079 }
3080
3081 // Collect the `info/exclude` rules of every containing repository, not just
3082 // the innermost one: a nested repository's files are still governed by the
3083 // exclude rules of the outer repository that contains it.
3084 if let Some((repo_exclude, _)) = self.repo_exclude_by_work_dir_abs_path.get(ancestor) {
3085 repo_excludes.push(repo_exclude.clone());
3086 }
3087
3088 if repo_root.is_none() {
3089 let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten();
3090 if metadata.is_some() {
3091 repo_root = Some(Arc::from(ancestor));
3092 }
3093 }
3094 }
3095
3096 let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() {
3097 IgnoreStack::global(global_gitignore)
3098 } else {
3099 IgnoreStack::none()
3100 };
3101
3102 for repo_exclude in repo_excludes.into_iter().rev() {
3103 ignore_stack = ignore_stack.append(IgnoreKind::RepoExclude, repo_exclude);
3104 }
3105 ignore_stack.repo_root = repo_root;
3106 let mut ancestor_ignore_stack = ignore_stack.clone();
3107 for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
3108 if ancestor_ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
3109 ignore_stack = IgnoreStack::all();
3110 break;
3111 } else if let Some(ignore) = ignore {
3112 let kind = IgnoreKind::Gitignore(parent_abs_path.into());
3113 ancestor_ignore_stack = ancestor_ignore_stack.append(kind, ignore.clone());
3114 ignore_stack =
3115 ignore_stack.append(IgnoreKind::Gitignore(parent_abs_path.into()), ignore);
3116 }
3117 }
3118
3119 if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
3120 ignore_stack = IgnoreStack::all();
3121 }
3122
3123 ignore_stack
3124 }
3125
3126 #[cfg(feature = "test-support")]
3127 pub fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
3128 self.entries_by_path
3129 .cursor::<()>(())
3130 .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
3131 }
3132
3133 #[cfg(feature = "test-support")]
3134 pub fn check_invariants(&self, git_state: bool) {
3135 use pretty_assertions::assert_eq;
3136
3137 assert_eq!(
3138 self.entries_by_path
3139 .cursor::<()>(())
3140 .map(|e| (&e.path, e.id))
3141 .collect::<Vec<_>>(),
3142 self.entries_by_id
3143 .cursor::<()>(())
3144 .map(|e| (&e.path, e.id))
3145 .collect::<collections::BTreeSet<_>>()
3146 .into_iter()
3147 .collect::<Vec<_>>(),
3148 "entries_by_path and entries_by_id are inconsistent"
3149 );
3150
3151 let mut files = self.files(true, 0);
3152 let mut visible_files = self.files(false, 0);
3153 for entry in self.entries_by_path.cursor::<()>(()) {
3154 if entry.is_file() {
3155 assert_eq!(files.next().unwrap().inode, entry.inode);
3156 if !entry.is_ignored || entry.is_always_included {
3157 assert_eq!(visible_files.next().unwrap().inode, entry.inode);
3158 }
3159 }
3160 }
3161
3162 assert!(files.next().is_none());
3163 assert!(visible_files.next().is_none());
3164
3165 let mut bfs_paths = Vec::new();
3166 let mut stack = self
3167 .root_entry()
3168 .map(|e| e.path.as_ref())
3169 .into_iter()
3170 .collect::<Vec<_>>();
3171 while let Some(path) = stack.pop() {
3172 bfs_paths.push(path);
3173 let ix = stack.len();
3174 for child_entry in self.child_entries(path) {
3175 stack.insert(ix, &child_entry.path);
3176 }
3177 }
3178
3179 let dfs_paths_via_iter = self
3180 .entries_by_path
3181 .cursor::<()>(())
3182 .map(|e| e.path.as_ref())
3183 .collect::<Vec<_>>();
3184 assert_eq!(bfs_paths, dfs_paths_via_iter);
3185
3186 let dfs_paths_via_traversal = self
3187 .entries(true, 0)
3188 .map(|e| e.path.as_ref())
3189 .collect::<Vec<_>>();
3190
3191 assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
3192
3193 if git_state {
3194 for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
3195 let ignore_parent_path = &RelPath::new(
3196 ignore_parent_abs_path
3197 .strip_prefix(self.abs_path.as_path())
3198 .unwrap(),
3199 PathStyle::local(),
3200 )
3201 .unwrap();
3202 assert!(self.entry_for_path(ignore_parent_path).is_some());
3203 assert!(
3204 self.entry_for_path(
3205 &ignore_parent_path.join(RelPath::from_unix_str(GITIGNORE).unwrap())
3206 )
3207 .is_some()
3208 );
3209 }
3210 }
3211 }
3212
3213 #[cfg(feature = "test-support")]
3214 pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&RelPath, u64, bool)> {
3215 let mut paths = Vec::new();
3216 for entry in self.entries_by_path.cursor::<()>(()) {
3217 if include_ignored || !entry.is_ignored {
3218 paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored));
3219 }
3220 }
3221 paths.sort_by(|a, b| a.0.cmp(b.0));
3222 paths
3223 }
3224}
3225
3226impl BackgroundScannerState {
3227 async fn enqueue_scan_dir(
3228 &self,
3229 abs_path: Arc<Path>,
3230 entry: &Entry,
3231 scan_job_tx: &Sender<ScanJob>,
3232 fs: &dyn Fs,
3233 ) {
3234 let path = entry.path.clone();
3235 let ignore_stack = self
3236 .snapshot
3237 .ignore_stack_for_abs_path(&abs_path, true, fs)
3238 .await;
3239 let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
3240
3241 if !ancestor_inodes.contains(&entry.inode) {
3242 ancestor_inodes.insert(entry.inode);
3243 scan_job_tx
3244 .try_send(ScanJob {
3245 abs_path,
3246 path,
3247 ignore_stack,
3248 scan_queue: scan_job_tx.clone(),
3249 ancestor_inodes,
3250 is_external: entry.is_external,
3251 })
3252 .unwrap();
3253 }
3254 }
3255
3256 fn reuse_entry_id(&mut self, entry: &mut Entry) {
3257 let Some(mtime) = entry.mtime else {
3258 return;
3259 };
3260 if let Some(entry_id) = self.reused_entry_id(&entry.path, entry.inode, mtime) {
3261 entry.id = entry_id;
3262 }
3263 }
3264
3265 fn entry_id_for(
3266 &mut self,
3267 next_entry_id: &AtomicUsize,
3268 path: &RelPath,
3269 metadata: &fs::Metadata,
3270 ) -> ProjectEntryId {
3271 self.reused_entry_id(path, metadata.inode, metadata.mtime)
3272 .unwrap_or_else(|| ProjectEntryId::new(next_entry_id))
3273 }
3274
3275 fn reused_entry_id(
3276 &mut self,
3277 path: &RelPath,
3278 inode: u64,
3279 mtime: MTime,
3280 ) -> Option<ProjectEntryId> {
3281 if let Some(removed_entry) = self.removed_entries.take_by_path(path, inode) {
3282 return Some(removed_entry.id);
3283 }
3284
3285 // If an entry with the same inode was removed from the worktree during this scan,
3286 // then it *might* represent the same file or directory. But the OS might also have
3287 // re-used the inode for a completely different file or directory.
3288 //
3289 // Conditionally reuse the old entry's id:
3290 // * if the mtime is the same, the file was probably been renamed.
3291 // * if the path is the same, the file may just have been updated
3292 if let Some(removed_entry) = self.removed_entries.take_by_inode(inode) {
3293 (removed_entry.mtime == Some(mtime) || *removed_entry.path == *path)
3294 .then_some(removed_entry.id)
3295 } else {
3296 Some(self.snapshot.entry_for_path(path)?.id)
3297 }
3298 }
3299
3300 async fn insert_entry(&mut self, entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry {
3301 let entry = self.snapshot.insert_entry(entry, fs).await;
3302 if entry.path.file_name() == Some(&DOT_GIT) {
3303 self.insert_git_repository(entry.path.clone(), fs, watcher)
3304 .await;
3305 }
3306
3307 #[cfg(feature = "test-support")]
3308 self.snapshot.check_invariants(false);
3309
3310 entry
3311 }
3312
3313 fn populate_dir(
3314 &mut self,
3315 parent_path: Arc<RelPath>,
3316 entries: impl IntoIterator<Item = Entry>,
3317 ignore: Option<Arc<Gitignore>>,
3318 ) {
3319 let mut parent_entry = if let Some(parent_entry) = self
3320 .snapshot
3321 .entries_by_path
3322 .get(&PathKey(parent_path.clone()), ())
3323 {
3324 parent_entry.clone()
3325 } else {
3326 log::warn!(
3327 "populating a directory {:?} that has been removed",
3328 parent_path
3329 );
3330 return;
3331 };
3332
3333 match parent_entry.kind {
3334 EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir,
3335 EntryKind::Dir => {}
3336 _ => return,
3337 }
3338
3339 if let Some(ignore) = ignore {
3340 let abs_parent_path = self
3341 .snapshot
3342 .abs_path
3343 .as_path()
3344 .join(parent_path.as_std_path())
3345 .into();
3346 self.snapshot
3347 .ignores_by_parent_abs_path
3348 .insert(abs_parent_path, (ignore, false));
3349 }
3350
3351 let parent_entry_id = parent_entry.id;
3352 self.scanned_dirs.insert(parent_entry_id);
3353 let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
3354 let mut entries_by_id_edits = Vec::new();
3355
3356 for entry in entries {
3357 entries_by_id_edits.push(Edit::Insert(PathEntry {
3358 id: entry.id,
3359 path: entry.path.clone(),
3360 is_ignored: entry.is_ignored,
3361 scan_id: self.snapshot.scan_id,
3362 }));
3363 entries_by_path_edits.push(Edit::Insert(entry));
3364 }
3365
3366 self.snapshot
3367 .entries_by_path
3368 .edit(entries_by_path_edits, ());
3369 self.snapshot.entries_by_id.edit(entries_by_id_edits, ());
3370
3371 if let Err(ix) = self.changed_paths.binary_search(&parent_path) {
3372 self.changed_paths.insert(ix, parent_path.clone());
3373 }
3374
3375 #[cfg(feature = "test-support")]
3376 self.snapshot.check_invariants(false);
3377 }
3378
3379 fn remove_path_from_snapshot_and_unwatch(
3380 &mut self,
3381 path: &RelPath,
3382 watcher: &dyn Watcher,
3383 preserve_repository_watches: bool,
3384 ) {
3385 // When the caller preserves repository watches, it intends to re-scan
3386 // this subtree and keep its git repositories; pruning them here would
3387 // transiently drop and then re-create them with fresh `RepositoryId`s.
3388 let prune_repositories = !preserve_repository_watches;
3389 let removed_descendant_abs_paths = self.remove_path_from_snapshot(path, prune_repositories);
3390 self.unwatch_path(
3391 watcher,
3392 path,
3393 removed_descendant_abs_paths,
3394 preserve_repository_watches,
3395 );
3396 }
3397
3398 fn unwatch_path(
3399 &mut self,
3400 watcher: &dyn Watcher,
3401 path: &RelPath,
3402 removed_descendant_abs_paths: Vec<PathBuf>,
3403 preserve_repository_watches: bool,
3404 ) {
3405 let mut repository_watches_to_preserve = HashSet::<Arc<Path>>::default();
3406 if preserve_repository_watches {
3407 for repository in self.snapshot.git_repositories.values() {
3408 repository_watches_to_preserve.insert(repository.common_dir_abs_path.clone());
3409 repository_watches_to_preserve.insert(repository.repository_dir_abs_path.clone());
3410 }
3411 }
3412
3413 for removed_dir_abs_path in removed_descendant_abs_paths {
3414 if repository_watches_to_preserve.contains(removed_dir_abs_path.as_path()) {
3415 continue;
3416 }
3417 watcher.remove(&removed_dir_abs_path).log_err();
3418 }
3419
3420 self.snapshot
3421 .external_canonical_to_relative
3422 .retain(|canonical, relative| {
3423 if relative.starts_with(path) {
3424 if !repository_watches_to_preserve.contains(canonical.as_ref()) {
3425 watcher.remove(canonical.as_ref()).log_err();
3426 }
3427 false
3428 } else {
3429 true
3430 }
3431 });
3432 }
3433
3434 fn remove_path_from_snapshot(
3435 &mut self,
3436 path: &RelPath,
3437 prune_repositories: bool,
3438 ) -> Vec<PathBuf> {
3439 log::trace!("background scanner removing path {path:?}");
3440 let mut new_entries;
3441 let removed_entries;
3442 {
3443 let mut cursor = self
3444 .snapshot
3445 .entries_by_path
3446 .cursor::<TraversalProgress>(());
3447 new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left);
3448 removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left);
3449 new_entries.append(cursor.suffix(), ());
3450 }
3451 self.snapshot.entries_by_path = new_entries;
3452
3453 let mut removed_ids = Vec::with_capacity(removed_entries.summary().count);
3454 let mut removed_dir_abs_paths = Vec::new();
3455 for entry in removed_entries.cursor::<()>(()) {
3456 if entry.is_dir() {
3457 let watch_path = self
3458 .watched_dir_abs_paths_by_entry_id
3459 .remove(&entry.id)
3460 .map(|path| path.as_ref().to_path_buf())
3461 .unwrap_or_else(|| self.snapshot.absolutize(&entry.path));
3462 removed_dir_abs_paths.push(watch_path);
3463 }
3464
3465 self.removed_entries.insert(entry);
3466
3467 if entry.path.file_name() == Some(GITIGNORE) {
3468 let abs_parent_path = self.snapshot.absolutize(&entry.path.parent().unwrap());
3469 if let Some((_, needs_update)) = self
3470 .snapshot
3471 .ignores_by_parent_abs_path
3472 .get_mut(abs_parent_path.as_path())
3473 {
3474 *needs_update = true;
3475 }
3476 }
3477
3478 if let Err(ix) = removed_ids.binary_search(&entry.id) {
3479 removed_ids.insert(ix, entry.id);
3480 }
3481 }
3482
3483 self.snapshot
3484 .entries_by_id
3485 .edit(removed_ids.iter().map(|&id| Edit::Remove(id)).collect(), ());
3486
3487 // Only prune git repositories when the entries are being genuinely
3488 // removed. During a recursive refresh (e.g. a watcher-forced rescan),
3489 // the subtree is removed and immediately re-scanned; dropping the
3490 // repositories here would make them flap, causing the GitStore to
3491 // tear them down and re-create them with fresh `RepositoryId`s. Stale
3492 // repositories are instead reaped authoritatively (against the actual
3493 // filesystem) in `update_git_repositories`.
3494 if prune_repositories {
3495 self.snapshot
3496 .git_repositories
3497 .retain(|id, _| removed_ids.binary_search(id).is_err());
3498 }
3499
3500 #[cfg(feature = "test-support")]
3501 self.snapshot.check_invariants(false);
3502
3503 removed_dir_abs_paths
3504 }
3505
3506 async fn insert_git_repository(
3507 &mut self,
3508 dot_git_path: Arc<RelPath>,
3509 fs: &dyn Fs,
3510 watcher: &dyn Watcher,
3511 ) {
3512 let work_dir_path: Arc<RelPath> = match dot_git_path.parent() {
3513 Some(parent_dir) => {
3514 // Guard against repositories inside the repository metadata
3515 if parent_dir
3516 .components()
3517 .any(|component| component == DOT_GIT)
3518 {
3519 log::debug!(
3520 "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
3521 );
3522 return;
3523 };
3524
3525 parent_dir.into()
3526 }
3527 None => {
3528 // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself,
3529 // no files inside that directory are tracked by git, so no need to build the repo around it
3530 log::debug!(
3531 "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
3532 );
3533 return;
3534 }
3535 };
3536
3537 let dot_git_abs_path = Arc::from(self.snapshot.absolutize(&dot_git_path).as_ref());
3538
3539 self.insert_git_repository_for_path(
3540 WorkDirectory::InProject {
3541 relative_path: work_dir_path,
3542 },
3543 dot_git_abs_path,
3544 fs,
3545 watcher,
3546 )
3547 .await
3548 .log_err();
3549 }
3550
3551 async fn insert_git_repository_for_path(
3552 &mut self,
3553 work_directory: WorkDirectory,
3554 dot_git_abs_path: Arc<Path>,
3555 fs: &dyn Fs,
3556 watcher: &dyn Watcher,
3557 ) -> Result<LocalRepositoryEntry> {
3558 let work_dir_entry = self
3559 .snapshot
3560 .entry_for_path(&work_directory.path_key().0)
3561 .with_context(|| {
3562 format!(
3563 "working directory `{}` not indexed",
3564 work_directory
3565 .path_key()
3566 .0
3567 .display(self.snapshot.path_style)
3568 )
3569 })?;
3570 let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory);
3571
3572 let (repository_dir_abs_path, common_dir_abs_path) =
3573 discover_git_paths(&dot_git_abs_path, fs).await;
3574 watcher
3575 .add(&common_dir_abs_path)
3576 .context("failed to add common directory to watcher")
3577 .log_err();
3578 watcher
3579 .add(&repository_dir_abs_path)
3580 .context("failed to add repository directory to watcher")
3581 .log_err();
3582
3583 watch_git_dir_subdirectories(&common_dir_abs_path, fs, watcher).await;
3584 if repository_dir_abs_path != common_dir_abs_path {
3585 watch_git_dir_subdirectories(&repository_dir_abs_path, fs, watcher).await;
3586 }
3587
3588 let work_directory_id = work_dir_entry.id;
3589
3590 // A repository can be re-inserted when its `.git` entry is re-discovered, e.g.
3591 // during a watcher-forced rescan. Carry the existing `git_dir_scan_id` forward
3592 // in that case: the snapshot diff detects git changes by comparing scan ids, so
3593 // resetting the id would wipe out a bump made earlier in the same scan cycle,
3594 // silently dropping the corresponding git update. Deliberately don't bump the
3595 // id here either: re-insertion is snapshot bookkeeping, not evidence that git
3596 // state changed. It also happens on non-lossy paths (explicit refreshes,
3597 // path-prefix scans) where we know nothing in `.git` changed, and a bump would
3598 // trigger a spurious full reload of the repository's git state. Only
3599 // `update_git_repositories` claims that git state changed, by stamping the
3600 // current scan id.
3601 let git_dir_scan_id = self
3602 .snapshot
3603 .git_repositories
3604 .get(&work_directory_id)
3605 .map_or(0, |existing_repository| existing_repository.git_dir_scan_id);
3606
3607 let local_repository = LocalRepositoryEntry {
3608 work_directory_id,
3609 work_directory,
3610 work_directory_abs_path: work_directory_abs_path.as_path().into(),
3611 git_dir_scan_id,
3612 dot_git_abs_path,
3613 common_dir_abs_path,
3614 repository_dir_abs_path,
3615 };
3616
3617 self.snapshot
3618 .git_repositories
3619 .insert(work_directory_id, local_repository.clone());
3620
3621 log::trace!("inserting new local git repository");
3622 Ok(local_repository)
3623 }
3624}
3625
3626/// Watches the directories inside a git directory that git writes ref updates to.
3627///
3628/// On Linux and FreeBSD the native file watcher is non-recursive, so a watch on the git
3629/// directory itself does not report changes to files nested below it, such as the loose
3630/// refs that git updates on commit, fetch, and branch operations. Watch the `refs` tree
3631/// (its directories are watched individually because branch names may contain slashes)
3632/// and, for repositories using the reftable backend, the `reftable` directory. On
3633/// platforms with recursive watchers these calls are deduplicated against the existing
3634/// recursive registration, making them effectively free.
3635async fn watch_git_dir_subdirectories(git_dir_abs_path: &Path, fs: &dyn Fs, watcher: &dyn Watcher) {
3636 let reftable_dir_abs_path = git_dir_abs_path.join(REFTABLE_DIR);
3637 if fs.is_dir(&reftable_dir_abs_path).await {
3638 watcher
3639 .add(&reftable_dir_abs_path)
3640 .context("failed to add reftable directory to watcher")
3641 .log_err();
3642 }
3643
3644 watch_dir_tree(git_dir_abs_path.join(REFS_DIR), fs, watcher).await;
3645}
3646
3647/// Watches a directory and all of its descendant directories.
3648///
3649/// Each directory is watched before its children are enumerated, so that a child
3650/// created concurrently is either seen by the enumeration or reported by the watch.
3651async fn watch_dir_tree(root_abs_path: PathBuf, fs: &dyn Fs, watcher: &dyn Watcher) {
3652 let mut dirs_to_watch = vec![root_abs_path];
3653 while let Some(dir_abs_path) = dirs_to_watch.pop() {
3654 if !fs.is_dir(&dir_abs_path).await {
3655 continue;
3656 }
3657 watcher
3658 .add(&dir_abs_path)
3659 .with_context(|| format!("failed to watch directory {dir_abs_path:?}"))
3660 .log_err();
3661 let Some(mut children) = fs.read_dir(&dir_abs_path).await.log_err() else {
3662 continue;
3663 };
3664 while let Some(child_abs_path) = children.next().await {
3665 let Some(child_abs_path) = child_abs_path.log_err() else {
3666 continue;
3667 };
3668 if fs.is_dir(&child_abs_path).await {
3669 dirs_to_watch.push(child_abs_path);
3670 }
3671 }
3672 }
3673}
3674
3675async fn is_dot_git(path: &Path, fs: &dyn Fs) -> bool {
3676 if let Some(file_name) = path.file_name()
3677 && file_name == DOT_GIT
3678 {
3679 return true;
3680 }
3681
3682 // If we're in a bare repository, we are not inside a `.git` folder. In a
3683 // bare repository, the root folder contains what would normally be in the
3684 // `.git` folder.
3685 let head_metadata = fs.metadata(&path.join("HEAD")).await;
3686 if !matches!(head_metadata, Ok(Some(_))) {
3687 return false;
3688 }
3689 let config_metadata = fs.metadata(&path.join("config")).await;
3690 matches!(config_metadata, Ok(Some(_)))
3691}
3692
3693async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
3694 let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
3695 build_gitignore_with_root(abs_path, parent, fs).await
3696}
3697
3698async fn build_gitignore_with_root(abs_path: &Path, root: &Path, fs: &dyn Fs) -> Result<Gitignore> {
3699 let contents = fs
3700 .load(abs_path)
3701 .await
3702 .with_context(|| format!("failed to load gitignore file at {}", abs_path.display()))?;
3703 let mut builder = GitignoreBuilder::new(root);
3704 for line in contents.lines() {
3705 builder.add_line(Some(abs_path.into()), line)?;
3706 }
3707 Ok(builder.build()?)
3708}
3709
3710impl Deref for Worktree {
3711 type Target = Snapshot;
3712
3713 fn deref(&self) -> &Self::Target {
3714 match self {
3715 Worktree::Local(worktree) => &worktree.snapshot,
3716 Worktree::Remote(worktree) => &worktree.snapshot,
3717 }
3718 }
3719}
3720
3721impl Deref for LocalWorktree {
3722 type Target = LocalSnapshot;
3723
3724 fn deref(&self) -> &Self::Target {
3725 &self.snapshot
3726 }
3727}
3728
3729impl Deref for RemoteWorktree {
3730 type Target = Snapshot;
3731
3732 fn deref(&self) -> &Self::Target {
3733 &self.snapshot
3734 }
3735}
3736
3737impl fmt::Debug for LocalWorktree {
3738 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3739 self.snapshot.fmt(f)
3740 }
3741}
3742
3743impl fmt::Debug for Snapshot {
3744 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3745 struct EntriesById<'a>(&'a SumTree<PathEntry>);
3746 struct EntriesByPath<'a>(&'a SumTree<Entry>);
3747
3748 impl fmt::Debug for EntriesByPath<'_> {
3749 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3750 f.debug_map()
3751 .entries(self.0.iter().map(|entry| (&entry.path, entry.id)))
3752 .finish()
3753 }
3754 }
3755
3756 impl fmt::Debug for EntriesById<'_> {
3757 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3758 f.debug_list().entries(self.0.iter()).finish()
3759 }
3760 }
3761
3762 f.debug_struct("Snapshot")
3763 .field("id", &self.id)
3764 .field("root_name", &self.root_name)
3765 .field("entries_by_path", &EntriesByPath(&self.entries_by_path))
3766 .field("entries_by_id", &EntriesById(&self.entries_by_id))
3767 .finish()
3768 }
3769}
3770
3771#[derive(Debug, Clone, PartialEq)]
3772pub struct File {
3773 pub worktree: Entity<Worktree>,
3774 pub path: Arc<RelPath>,
3775 pub disk_state: DiskState,
3776 pub entry_id: Option<ProjectEntryId>,
3777 pub is_local: bool,
3778 pub is_private: bool,
3779}
3780
3781impl language::File for File {
3782 fn as_local(&self) -> Option<&dyn language::LocalFile> {
3783 if self.is_local { Some(self) } else { None }
3784 }
3785
3786 fn disk_state(&self) -> DiskState {
3787 self.disk_state
3788 }
3789
3790 fn path(&self) -> &Arc<RelPath> {
3791 &self.path
3792 }
3793
3794 fn full_path(&self, cx: &App) -> PathBuf {
3795 self.worktree.read(cx).full_path(&self.path)
3796 }
3797
3798 /// Returns the last component of this handle's absolute path. If this handle refers to the root
3799 /// of its worktree, then this method will return the name of the worktree itself.
3800 fn file_name<'a>(&'a self, cx: &'a App) -> &'a str {
3801 self.path
3802 .file_name()
3803 .unwrap_or_else(|| self.worktree.read(cx).root_name_str())
3804 }
3805
3806 fn worktree_id(&self, cx: &App) -> WorktreeId {
3807 self.worktree.read(cx).id()
3808 }
3809
3810 fn to_proto(&self, cx: &App) -> rpc::proto::File {
3811 rpc::proto::File {
3812 worktree_id: self.worktree.read(cx).id().to_proto(),
3813 entry_id: self.entry_id.map(|id| id.to_proto()),
3814 path: self.path.as_ref().as_unix_str().to_owned(),
3815 mtime: self.disk_state.mtime().map(|time| time.into()),
3816 is_deleted: self.disk_state.is_deleted(),
3817 is_historic: matches!(self.disk_state, DiskState::Historic { .. }),
3818 }
3819 }
3820
3821 fn is_private(&self) -> bool {
3822 self.is_private
3823 }
3824
3825 fn path_style(&self, cx: &App) -> PathStyle {
3826 self.worktree.read(cx).path_style()
3827 }
3828
3829 fn can_open(&self) -> bool {
3830 true
3831 }
3832}
3833
3834impl language::LocalFile for File {
3835 fn abs_path(&self, cx: &App) -> PathBuf {
3836 self.worktree.read(cx).absolutize(&self.path)
3837 }
3838
3839 fn load(&self, cx: &App) -> Task<Result<String>> {
3840 let worktree = self.worktree.read(cx).as_local().unwrap();
3841 let abs_path = worktree.absolutize(&self.path);
3842 let fs = worktree.fs.clone();
3843 cx.background_spawn(async move { fs.load(&abs_path).await })
3844 }
3845
3846 fn load_bytes(&self, cx: &App) -> Task<Result<Vec<u8>>> {
3847 let worktree = self.worktree.read(cx).as_local().unwrap();
3848 let abs_path = worktree.absolutize(&self.path);
3849 let fs = worktree.fs.clone();
3850 cx.background_spawn(async move { fs.load_bytes(&abs_path).await })
3851 }
3852}
3853
3854impl File {
3855 pub fn for_entry(entry: Entry, worktree: Entity<Worktree>) -> Arc<Self> {
3856 Arc::new(Self {
3857 worktree,
3858 path: entry.path.clone(),
3859 disk_state: if let Some(mtime) = entry.mtime {
3860 DiskState::Present {
3861 mtime,
3862 size: entry.size,
3863 }
3864 } else {
3865 DiskState::New
3866 },
3867 entry_id: Some(entry.id),
3868 is_local: true,
3869 is_private: entry.is_private,
3870 })
3871 }
3872
3873 pub fn from_proto(
3874 proto: rpc::proto::File,
3875 worktree: Entity<Worktree>,
3876 cx: &App,
3877 ) -> Result<Self> {
3878 let worktree_id = worktree.read(cx).as_remote().context("not remote")?.id();
3879
3880 anyhow::ensure!(
3881 worktree_id.to_proto() == proto.worktree_id,
3882 "worktree id does not match file"
3883 );
3884
3885 let disk_state = if proto.is_historic {
3886 DiskState::Historic {
3887 was_deleted: proto.is_deleted,
3888 }
3889 } else if proto.is_deleted {
3890 DiskState::Deleted
3891 } else if let Some(mtime) = proto.mtime.map(&Into::into) {
3892 DiskState::Present { mtime, size: 0 }
3893 } else {
3894 DiskState::New
3895 };
3896
3897 Ok(Self {
3898 worktree,
3899 path: RelPath::from_unix_str(&proto.path)
3900 .context("invalid path in file protobuf")?
3901 .into(),
3902 disk_state,
3903 entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
3904 is_local: false,
3905 is_private: false,
3906 })
3907 }
3908
3909 pub fn from_dyn(file: Option<&Arc<dyn language::File>>) -> Option<&Self> {
3910 file.and_then(|f| {
3911 let f: &dyn language::File = f.borrow();
3912 let f: &dyn Any = f;
3913 f.downcast_ref()
3914 })
3915 }
3916
3917 pub fn worktree_id(&self, cx: &App) -> WorktreeId {
3918 self.worktree.read(cx).id()
3919 }
3920
3921 pub fn project_entry_id(&self) -> Option<ProjectEntryId> {
3922 match self.disk_state {
3923 DiskState::Deleted => None,
3924 _ => self.entry_id,
3925 }
3926 }
3927}
3928
3929#[derive(Clone, Debug, PartialEq, Eq)]
3930pub struct Entry {
3931 pub id: ProjectEntryId,
3932 pub kind: EntryKind,
3933 pub path: Arc<RelPath>,
3934 pub inode: u64,
3935 pub mtime: Option<MTime>,
3936
3937 pub canonical_path: Option<Arc<Path>>,
3938 /// Whether this entry is ignored by Git.
3939 ///
3940 /// We only scan ignored entries once the directory is expanded and
3941 /// exclude them from searches.
3942 pub is_ignored: bool,
3943
3944 /// Whether this entry is hidden or inside hidden directory.
3945 ///
3946 /// We only scan hidden entries once the directory is expanded.
3947 pub is_hidden: bool,
3948
3949 /// Whether this entry is always included in searches.
3950 ///
3951 /// This is used for entries that are always included in searches, even
3952 /// if they are ignored by git. Overridden by file_scan_exclusions.
3953 pub is_always_included: bool,
3954
3955 /// Whether this entry's canonical path is outside of the worktree.
3956 /// This means the entry is only accessible from the worktree root via a
3957 /// symlink.
3958 ///
3959 /// We only scan entries outside of the worktree once the symlinked
3960 /// directory is expanded.
3961 pub is_external: bool,
3962
3963 /// Whether this entry is considered to be a `.env` file.
3964 pub is_private: bool,
3965 /// The entry's size on disk, in bytes.
3966 pub size: u64,
3967 pub char_bag: CharBag,
3968 pub is_fifo: bool,
3969}
3970
3971#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3972pub enum EntryKind {
3973 UnloadedDir,
3974 PendingDir,
3975 Dir,
3976 File,
3977}
3978
3979#[derive(Clone, Copy, Debug, PartialEq)]
3980pub enum PathChange {
3981 /// A filesystem entry was was created.
3982 Added,
3983 /// A filesystem entry was removed.
3984 Removed,
3985 /// A filesystem entry was updated.
3986 Updated,
3987 /// A filesystem entry was either updated or added. We don't know
3988 /// whether or not it already existed, because the path had not
3989 /// been loaded before the event.
3990 AddedOrUpdated,
3991 /// A filesystem entry was found during the initial scan of the worktree.
3992 Loaded,
3993}
3994
3995#[derive(Clone, Debug, PartialEq, Eq)]
3996pub struct UpdatedGitRepository {
3997 /// ID of the repository's working directory.
3998 ///
3999 /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique.
4000 /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed.
4001 pub work_directory_id: ProjectEntryId,
4002 pub old_work_directory_abs_path: Option<Arc<Path>>,
4003 pub new_work_directory_abs_path: Option<Arc<Path>>,
4004 /// For a normal git repository checkout, the absolute path to the .git directory.
4005 /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
4006 pub dot_git_abs_path: Option<Arc<Path>>,
4007 pub repository_dir_abs_path: Option<Arc<Path>>,
4008 pub common_dir_abs_path: Option<Arc<Path>>,
4009}
4010
4011pub type UpdatedEntriesSet = Arc<[(Arc<RelPath>, ProjectEntryId, PathChange)]>;
4012pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>;
4013
4014#[derive(Clone, Debug)]
4015pub struct PathProgress<'a> {
4016 pub max_path: &'a RelPath,
4017}
4018
4019#[derive(Clone, Debug)]
4020pub struct PathSummary<S> {
4021 pub max_path: Arc<RelPath>,
4022 pub item_summary: S,
4023}
4024
4025impl<S: Summary> Summary for PathSummary<S> {
4026 type Context<'a> = S::Context<'a>;
4027
4028 fn zero(cx: Self::Context<'_>) -> Self {
4029 Self {
4030 max_path: RelPath::empty_arc(),
4031 item_summary: S::zero(cx),
4032 }
4033 }
4034
4035 fn add_summary(&mut self, rhs: &Self, cx: Self::Context<'_>) {
4036 self.max_path = rhs.max_path.clone();
4037 self.item_summary.add_summary(&rhs.item_summary, cx);
4038 }
4039}
4040
4041impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathProgress<'a> {
4042 fn zero(_: <PathSummary<S> as Summary>::Context<'_>) -> Self {
4043 Self {
4044 max_path: RelPath::empty(),
4045 }
4046 }
4047
4048 fn add_summary(
4049 &mut self,
4050 summary: &'a PathSummary<S>,
4051 _: <PathSummary<S> as Summary>::Context<'_>,
4052 ) {
4053 self.max_path = summary.max_path.as_ref()
4054 }
4055}
4056
4057impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
4058 fn zero(_cx: ()) -> Self {
4059 Default::default()
4060 }
4061
4062 fn add_summary(&mut self, summary: &'a PathSummary<GitSummary>, _: ()) {
4063 *self += summary.item_summary
4064 }
4065}
4066
4067impl<'a>
4068 sum_tree::SeekTarget<'a, PathSummary<GitSummary>, Dimensions<TraversalProgress<'a>, GitSummary>>
4069 for PathTarget<'_>
4070{
4071 fn cmp(
4072 &self,
4073 cursor_location: &Dimensions<TraversalProgress<'a>, GitSummary>,
4074 _: (),
4075 ) -> Ordering {
4076 self.cmp_path(cursor_location.0.max_path)
4077 }
4078}
4079
4080impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathKey {
4081 fn zero(_: S::Context<'_>) -> Self {
4082 Default::default()
4083 }
4084
4085 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: S::Context<'_>) {
4086 self.0 = summary.max_path.clone();
4087 }
4088}
4089
4090impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for TraversalProgress<'a> {
4091 fn zero(_cx: S::Context<'_>) -> Self {
4092 Default::default()
4093 }
4094
4095 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: S::Context<'_>) {
4096 self.max_path = summary.max_path.as_ref();
4097 }
4098}
4099
4100impl Entry {
4101 fn new(
4102 path: Arc<RelPath>,
4103 metadata: &fs::Metadata,
4104 id: ProjectEntryId,
4105 root_char_bag: CharBag,
4106 canonical_path: Option<Arc<Path>>,
4107 ) -> Self {
4108 let char_bag = char_bag_for_path(root_char_bag, &path);
4109 Self {
4110 id,
4111 kind: if metadata.is_dir {
4112 EntryKind::PendingDir
4113 } else {
4114 EntryKind::File
4115 },
4116 path,
4117 inode: metadata.inode,
4118 mtime: Some(metadata.mtime),
4119 size: metadata.len,
4120 canonical_path,
4121 is_ignored: false,
4122 is_hidden: false,
4123 is_always_included: false,
4124 is_external: false,
4125 is_private: false,
4126 char_bag,
4127 is_fifo: metadata.is_fifo,
4128 }
4129 }
4130
4131 pub fn is_created(&self) -> bool {
4132 self.mtime.is_some()
4133 }
4134
4135 pub fn is_dir(&self) -> bool {
4136 self.kind.is_dir()
4137 }
4138
4139 pub fn is_file(&self) -> bool {
4140 self.kind.is_file()
4141 }
4142}
4143
4144impl EntryKind {
4145 pub fn is_dir(&self) -> bool {
4146 matches!(
4147 self,
4148 EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir
4149 )
4150 }
4151
4152 pub fn is_unloaded(&self) -> bool {
4153 matches!(self, EntryKind::UnloadedDir)
4154 }
4155
4156 pub fn is_file(&self) -> bool {
4157 matches!(self, EntryKind::File)
4158 }
4159}
4160
4161impl sum_tree::Item for Entry {
4162 type Summary = EntrySummary;
4163
4164 fn summary(&self, _cx: ()) -> Self::Summary {
4165 let non_ignored_count = if self.is_ignored && !self.is_always_included {
4166 0
4167 } else {
4168 1
4169 };
4170 let file_count;
4171 let non_ignored_file_count;
4172 if self.is_file() {
4173 file_count = 1;
4174 non_ignored_file_count = non_ignored_count;
4175 } else {
4176 file_count = 0;
4177 non_ignored_file_count = 0;
4178 }
4179
4180 EntrySummary {
4181 max_path: self.path.clone(),
4182 count: 1,
4183 non_ignored_count,
4184 file_count,
4185 non_ignored_file_count,
4186 }
4187 }
4188}
4189
4190impl sum_tree::KeyedItem for Entry {
4191 type Key = PathKey;
4192
4193 fn key(&self) -> Self::Key {
4194 PathKey(self.path.clone())
4195 }
4196}
4197
4198#[derive(Clone, Debug)]
4199pub struct EntrySummary {
4200 max_path: Arc<RelPath>,
4201 count: usize,
4202 non_ignored_count: usize,
4203 file_count: usize,
4204 non_ignored_file_count: usize,
4205}
4206
4207impl Default for EntrySummary {
4208 fn default() -> Self {
4209 Self {
4210 max_path: Arc::from(RelPath::empty()),
4211 count: 0,
4212 non_ignored_count: 0,
4213 file_count: 0,
4214 non_ignored_file_count: 0,
4215 }
4216 }
4217}
4218
4219impl sum_tree::ContextLessSummary for EntrySummary {
4220 fn zero() -> Self {
4221 Default::default()
4222 }
4223
4224 fn add_summary(&mut self, rhs: &Self) {
4225 self.max_path = rhs.max_path.clone();
4226 self.count += rhs.count;
4227 self.non_ignored_count += rhs.non_ignored_count;
4228 self.file_count += rhs.file_count;
4229 self.non_ignored_file_count += rhs.non_ignored_file_count;
4230 }
4231}
4232
4233#[derive(Clone, Debug)]
4234struct PathEntry {
4235 id: ProjectEntryId,
4236 path: Arc<RelPath>,
4237 is_ignored: bool,
4238 scan_id: usize,
4239}
4240
4241impl sum_tree::Item for PathEntry {
4242 type Summary = PathEntrySummary;
4243
4244 fn summary(&self, _cx: ()) -> Self::Summary {
4245 PathEntrySummary { max_id: self.id }
4246 }
4247}
4248
4249impl sum_tree::KeyedItem for PathEntry {
4250 type Key = ProjectEntryId;
4251
4252 fn key(&self) -> Self::Key {
4253 self.id
4254 }
4255}
4256
4257#[derive(Clone, Debug, Default)]
4258struct PathEntrySummary {
4259 max_id: ProjectEntryId,
4260}
4261
4262impl sum_tree::ContextLessSummary for PathEntrySummary {
4263 fn zero() -> Self {
4264 Default::default()
4265 }
4266
4267 fn add_summary(&mut self, summary: &Self) {
4268 self.max_id = summary.max_id;
4269 }
4270}
4271
4272impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
4273 fn zero(_cx: ()) -> Self {
4274 Default::default()
4275 }
4276
4277 fn add_summary(&mut self, summary: &'a PathEntrySummary, _: ()) {
4278 *self = summary.max_id;
4279 }
4280}
4281
4282#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
4283pub struct PathKey(pub Arc<RelPath>);
4284
4285impl Default for PathKey {
4286 fn default() -> Self {
4287 Self(RelPath::empty_arc())
4288 }
4289}
4290
4291impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
4292 fn zero(_cx: ()) -> Self {
4293 Default::default()
4294 }
4295
4296 fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) {
4297 self.0 = summary.max_path.clone();
4298 }
4299}
4300
4301struct BackgroundScanner {
4302 state: async_lock::Mutex<BackgroundScannerState>,
4303 fs: Arc<dyn Fs>,
4304 fs_case_sensitive: bool,
4305 status_updates_tx: UnboundedSender<ScanState>,
4306 executor: BackgroundExecutor,
4307 scan_requests_rx: async_channel::Receiver<ScanRequest>,
4308 path_prefixes_to_scan_rx: async_channel::Receiver<PathPrefixScanRequest>,
4309 next_entry_id: Arc<AtomicUsize>,
4310 phase: BackgroundScannerPhase,
4311 watcher: Arc<dyn Watcher>,
4312 settings: WorktreeSettings,
4313 share_private_files: bool,
4314 track_git_repositories: bool,
4315 /// Whether this is a single-file worktree (root is a file, not a directory).
4316 /// Used to determine if we should give up after repeated canonicalization failures.
4317 is_single_file: bool,
4318 defer_watch: bool,
4319}
4320
4321#[derive(Copy, Clone, PartialEq)]
4322enum BackgroundScannerPhase {
4323 InitialScan,
4324 EventsReceivedDuringInitialScan,
4325 Events,
4326}
4327
4328impl BackgroundScanner {
4329 async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>) {
4330 let root_abs_path;
4331 let scanning_enabled;
4332 {
4333 let state = self.state.lock().await;
4334 root_abs_path = state.snapshot.abs_path.clone();
4335 scanning_enabled = state.scanning_enabled;
4336 }
4337
4338 // If the worktree root does not contain a git repository, then find
4339 // the git repository in an ancestor directory. Find any gitignore files
4340 // in ancestor directories.
4341 let repo = if scanning_enabled && self.track_git_repositories {
4342 let (ignores, exclude, repo) =
4343 discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
4344 let mut state = self.state.lock().await;
4345 state.snapshot.ignores_by_parent_abs_path.extend(ignores);
4346 if let Some(exclude) = exclude {
4347 let work_directory_abs_path: Arc<Path> = repo
4348 .as_ref()
4349 .map(|(_, work_directory)| {
4350 state
4351 .snapshot
4352 .work_directory_abs_path(work_directory)
4353 .into()
4354 })
4355 .unwrap_or_else(|| root_abs_path.as_path().into());
4356 state
4357 .snapshot
4358 .repo_exclude_by_work_dir_abs_path
4359 .insert(work_directory_abs_path, (exclude, false));
4360 }
4361
4362 repo
4363 } else {
4364 None
4365 };
4366
4367 let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo
4368 && scanning_enabled
4369 && self.track_git_repositories
4370 {
4371 maybe!(async {
4372 self.state
4373 .lock()
4374 .await
4375 .insert_git_repository_for_path(
4376 work_directory,
4377 ancestor_dot_git.clone().into(),
4378 self.fs.as_ref(),
4379 self.watcher.as_ref(),
4380 )
4381 .await
4382 .log_err()?;
4383 Some(ancestor_dot_git)
4384 })
4385 .await
4386 } else {
4387 None
4388 };
4389
4390 log::trace!("containing git repository: {containing_git_repository:?}");
4391
4392 let global_gitignore_file = paths::global_gitignore_path();
4393 let mut global_gitignore_events = if let Some(global_gitignore_path) =
4394 &global_gitignore_file
4395 && scanning_enabled
4396 && self.track_git_repositories
4397 {
4398 let is_file = self.fs.is_file(&global_gitignore_path).await;
4399 self.state.lock().await.snapshot.global_gitignore = if is_file {
4400 build_gitignore(global_gitignore_path, self.fs.as_ref())
4401 .await
4402 .ok()
4403 .map(Arc::new)
4404 } else {
4405 None
4406 };
4407 if is_file {
4408 self.fs
4409 .watch(global_gitignore_path, FS_WATCH_LATENCY)
4410 .await
4411 .0
4412 } else {
4413 Box::pin(futures::stream::pending())
4414 }
4415 } else {
4416 self.state.lock().await.snapshot.global_gitignore = None;
4417 Box::pin(futures::stream::pending())
4418 };
4419
4420 let (scan_job_tx, scan_job_rx) = async_channel::unbounded();
4421 {
4422 let mut state = self.state.lock().await;
4423 state.snapshot.scan_id += 1;
4424 if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
4425 let ignore_stack = state
4426 .snapshot
4427 .ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref())
4428 .await;
4429 if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
4430 root_entry.is_ignored = true;
4431 let mut root_entry = root_entry.clone();
4432 state.reuse_entry_id(&mut root_entry);
4433 state
4434 .insert_entry(root_entry, self.fs.as_ref(), self.watcher.as_ref())
4435 .await;
4436 }
4437 if root_entry.is_dir() && state.scanning_enabled {
4438 state
4439 .enqueue_scan_dir(
4440 root_abs_path.as_path().into(),
4441 &root_entry,
4442 &scan_job_tx,
4443 self.fs.as_ref(),
4444 )
4445 .await;
4446 }
4447 }
4448 };
4449
4450 // Perform an initial scan of the directory.
4451 drop(scan_job_tx);
4452 self.scan_dirs(true, scan_job_rx).await;
4453 {
4454 let mut state = self.state.lock().await;
4455 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4456 }
4457
4458 self.send_status_update(false, SmallVec::new(), &[]).await;
4459
4460 if self.defer_watch {
4461 let (events, watcher) = self
4462 .fs
4463 .watch(root_abs_path.as_path(), FS_WATCH_LATENCY)
4464 .await;
4465 self.watcher = watcher;
4466 fs_events_rx = Box::pin(events.map(|events| events.into_iter().collect()));
4467
4468 let state = self.state.lock().await;
4469 for target in state.symlink_paths_by_target.keys() {
4470 if !target.starts_with(root_abs_path.as_path()) {
4471 self.watcher.add(target).log_err();
4472 }
4473 }
4474 for repo in state.snapshot.git_repositories.values() {
4475 if !repo
4476 .common_dir_abs_path
4477 .starts_with(root_abs_path.as_path())
4478 {
4479 self.watcher.add(&repo.common_dir_abs_path).log_err();
4480 }
4481 if !repo
4482 .repository_dir_abs_path
4483 .starts_with(root_abs_path.as_path())
4484 {
4485 self.watcher.add(&repo.repository_dir_abs_path).log_err();
4486 }
4487 }
4488 drop(state);
4489 }
4490
4491 // Process any any FS events that occurred while performing the initial scan.
4492 // For these events, update events cannot be as precise, because we didn't
4493 // have the previous state loaded yet.
4494 self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
4495 if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) {
4496 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
4497 paths.extend(more_paths);
4498 }
4499 self.process_events(
4500 paths
4501 .into_iter()
4502 .filter(|event| event.kind.is_some())
4503 .collect(),
4504 )
4505 .await;
4506 }
4507 if let Some(abs_path) = containing_git_repository {
4508 self.process_events(vec![PathEvent {
4509 path: abs_path,
4510 kind: Some(fs::PathEventKind::Changed),
4511 }])
4512 .await;
4513 }
4514
4515 // Continue processing events until the worktree is dropped.
4516 self.phase = BackgroundScannerPhase::Events;
4517
4518 loop {
4519 select_biased! {
4520 // Process any path refresh requests from the worktree. Prioritize
4521 // these before handling changes reported by the filesystem.
4522 request = self.next_scan_request().fuse() => {
4523 let Ok(request) = request else { break };
4524 if !self.process_scan_request(request, false).await {
4525 return;
4526 }
4527 }
4528
4529 path_prefix_request = self.path_prefixes_to_scan_rx.recv().fuse() => {
4530 let Ok(request) = path_prefix_request else { break };
4531
4532 if self.state.lock().await.path_prefixes_to_scan.contains(&request.path) {
4533 self.send_status_update(false, request.done, &[]).await;
4534 continue;
4535 }
4536
4537 log::trace!("adding path prefix {:?}", request.path);
4538
4539 let did_scan = self.forcibly_load_paths(std::slice::from_ref(&request.path)).await;
4540 if did_scan {
4541 let abs_path =
4542 {
4543 let mut state = self.state.lock().await;
4544 state.path_prefixes_to_scan.insert(request.path.clone());
4545 state.snapshot.absolutize(&request.path)
4546 };
4547
4548 if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
4549 self.process_events(vec![PathEvent {
4550 path: abs_path,
4551 kind: Some(fs::PathEventKind::Changed),
4552 }])
4553 .await;
4554 }
4555 }
4556 self.send_status_update(false, request.done, &[]).await;
4557 }
4558
4559 paths = fs_events_rx.next().fuse() => {
4560 let Some(mut paths) = paths else { break };
4561 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
4562 paths.extend(more_paths);
4563 }
4564 self.process_events(paths.into_iter().filter(|event| event.kind.is_some()).collect()).await;
4565 }
4566
4567 _ = global_gitignore_events.next().fuse() => {
4568 if let Some(path) = &global_gitignore_file {
4569 self.update_global_gitignore(&path).await;
4570 }
4571 }
4572 }
4573 }
4574 }
4575
4576 async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
4577 log::debug!("rescanning paths {:?}", request.relative_paths);
4578
4579 request.relative_paths.sort_unstable();
4580 self.forcibly_load_paths(&request.relative_paths).await;
4581
4582 let root_path = self.state.lock().await.snapshot.abs_path.clone();
4583 let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
4584 let root_canonical_path = match &root_canonical_path {
4585 Ok(path) => SanitizedPath::new(path),
4586 Err(err) => {
4587 log::error!("failed to canonicalize root path {root_path:?}: {err:#}");
4588 return true;
4589 }
4590 };
4591 let abs_paths = request
4592 .relative_paths
4593 .iter()
4594 .map(|path| {
4595 if path.file_name().is_some() {
4596 root_canonical_path.as_path().join(path.as_std_path())
4597 } else {
4598 root_canonical_path.as_path().to_path_buf()
4599 }
4600 })
4601 .collect::<Vec<_>>();
4602
4603 {
4604 let mut state = self.state.lock().await;
4605 let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id;
4606 state.snapshot.scan_id += 1;
4607 if is_idle {
4608 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4609 }
4610 }
4611
4612 self.reload_entries_for_paths(
4613 &root_path,
4614 &root_canonical_path,
4615 &request.relative_paths,
4616 abs_paths,
4617 None,
4618 )
4619 .await;
4620
4621 self.send_status_update(scanning, request.done, &[]).await
4622 }
4623
4624 fn normalized_events_for_worktree(
4625 state: &BackgroundScannerState,
4626 root_canonical_path: &SanitizedPath,
4627 mut events: Vec<PathEvent>,
4628 ) -> Vec<PathEvent> {
4629 if state.symlink_paths_by_target.is_empty() {
4630 return events;
4631 }
4632 let mut mapped_events = Vec::new();
4633
4634 events.retain(|event| {
4635 let abs_path = SanitizedPath::new(&event.path);
4636
4637 let mut best_match: Option<(&Arc<Path>, &SmallVec<[Arc<RelPath>; 1]>)> = None;
4638 let mut best_depth = 0;
4639 for (target_root, symlink_paths) in &state.symlink_paths_by_target {
4640 if abs_path.as_path().starts_with(target_root.as_ref()) {
4641 let depth = target_root.as_ref().components().count();
4642 if depth > best_depth {
4643 best_depth = depth;
4644 best_match = Some((target_root, symlink_paths));
4645 }
4646 }
4647 }
4648
4649 let Some((target_root, symlink_paths)) = best_match else {
4650 return true;
4651 };
4652
4653 let Ok(suffix) = abs_path.as_path().strip_prefix(target_root.as_ref()) else {
4654 return true;
4655 };
4656
4657 // If the symlink's real target is outside this worktree, the original path
4658 // isn't visible to the worktree. Keep only the remapped symlink events.
4659 let keep_original = target_root.starts_with(root_canonical_path.as_path());
4660
4661 for symlink_path in symlink_paths {
4662 let mapped_path = if suffix.as_os_str().is_empty() {
4663 root_canonical_path
4664 .as_path()
4665 .join(symlink_path.as_std_path())
4666 } else {
4667 root_canonical_path
4668 .as_path()
4669 .join(symlink_path.as_std_path())
4670 .join(suffix)
4671 };
4672 if mapped_path != event.path {
4673 mapped_events.push(PathEvent {
4674 path: mapped_path,
4675 kind: event.kind,
4676 });
4677 }
4678 }
4679 keep_original
4680 });
4681 events.extend(mapped_events);
4682 events
4683 }
4684
4685 async fn process_events(&self, mut events: Vec<PathEvent>) {
4686 let root_path = self.state.lock().await.snapshot.abs_path.clone();
4687 let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
4688 let root_canonical_path = match &root_canonical_path {
4689 Ok(path) => SanitizedPath::new(path),
4690 Err(err) => {
4691 let new_path = self
4692 .state
4693 .lock()
4694 .await
4695 .snapshot
4696 .root_file_handle
4697 .clone()
4698 .and_then(|handle| match handle.current_path(&self.fs) {
4699 Ok(new_path) => Some(new_path),
4700 Err(e) => {
4701 log::error!("Failed to refresh worktree root path: {e:#}");
4702 None
4703 }
4704 })
4705 .map(|path| SanitizedPath::new_arc(&path))
4706 .filter(|new_path| *new_path != root_path);
4707
4708 if let Some(new_path) = new_path {
4709 log::info!(
4710 "root renamed from {:?} to {:?}",
4711 root_path.as_path(),
4712 new_path.as_path(),
4713 );
4714 self.status_updates_tx
4715 .unbounded_send(ScanState::RootUpdated { new_path })
4716 .ok();
4717 } else {
4718 log::error!("root path could not be canonicalized: {err:#}");
4719
4720 // For single-file worktrees, if we can't canonicalize and the file handle
4721 // fallback also failed, the file is gone - close the worktree
4722 if self.is_single_file {
4723 log::info!(
4724 "single-file worktree root {:?} no longer exists, marking as deleted",
4725 root_path.as_path()
4726 );
4727 self.status_updates_tx
4728 .unbounded_send(ScanState::RootDeleted)
4729 .ok();
4730 }
4731 }
4732 return;
4733 }
4734 };
4735
4736 {
4737 let state = self.state.lock().await;
4738 events = Self::normalized_events_for_worktree(&state, &root_canonical_path, events);
4739 }
4740
4741 log::debug!("raw events for process_events: {events:?}");
4742
4743 fn skip_ix(ranges: &mut SmallVec<[Range<usize>; 4]>, ix: usize) {
4744 if let Some(last_range) = ranges.last_mut()
4745 && last_range.end == ix
4746 {
4747 last_range.end += 1;
4748 } else {
4749 ranges.push(ix..ix + 1);
4750 }
4751 }
4752
4753 // Check for events inside .git directories, so that we know which repositories need their git state reloaded.
4754 //
4755 // Certain directories may have FS changes, but do not lead to git data changes that Zed cares about.
4756 // Ignore these, to avoid Zed unnecessarily rescanning git metadata.
4757 let skipped_file_names_in_dot_git =
4758 [COMMIT_MESSAGE, FETCH_HEAD, ORIG_HEAD, BISECT_LOG, GC_PID];
4759 let skipped_dirs_in_dot_git = [
4760 FSMONITOR_DAEMON,
4761 LFS_DIR,
4762 OBJECTS_DIR,
4763 HOOKS_DIR,
4764 REBASE_MERGE_DIR,
4765 REBASE_APPLY_DIR,
4766 SEQUENCER_DIR,
4767 ];
4768
4769 let mut dot_git_abs_paths = Vec::new();
4770 let mut work_dirs_needing_exclude_update = Vec::new();
4771
4772 {
4773 let snapshot = &self.state.lock().await.snapshot;
4774
4775 let mut ranges_to_drop = SmallVec::<[Range<usize>; 4]>::new();
4776
4777 for (ix, event) in events.iter().enumerate() {
4778 let abs_path = SanitizedPath::new(&event.path);
4779
4780 let mut dot_git_paths = None;
4781
4782 if self.track_git_repositories {
4783 for ancestor in abs_path.as_path().ancestors() {
4784 if is_dot_git(ancestor, self.fs.as_ref()).await {
4785 let path_in_git_dir = abs_path
4786 .as_path()
4787 .strip_prefix(ancestor)
4788 .expect("stripping off the ancestor");
4789 dot_git_paths = Some((ancestor.to_owned(), path_in_git_dir.to_owned()));
4790 break;
4791 }
4792 }
4793 }
4794
4795 if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths {
4796 let is_ignored = skipped_file_names_in_dot_git.iter().any(|skipped| {
4797 path_in_git_dir
4798 .file_name()
4799 .is_some_and(|file_name| file_name == OsStr::new(skipped))
4800 }) || (path_in_git_dir.starts_with(LOGS_DIR)
4801 && path_in_git_dir != Path::new(LOGS_REF_STASH))
4802 || (path_in_git_dir.starts_with(INFO_DIR)
4803 && path_in_git_dir != Path::new(REPO_EXCLUDE))
4804 || skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| {
4805 path_in_git_dir.starts_with(skipped_git_subdir)
4806 })
4807 || path_in_git_dir.extension().is_some_and(|ext| ext == "lock")
4808 || (path_in_git_dir.components().count() == 1
4809 && path_in_git_dir
4810 .extension()
4811 .is_some_and(|ext| ext == "new" || ext == "tmp"));
4812 let is_dot_git = path_in_git_dir == Path::new("")
4813 && matches!(event.kind, Some(PathEventKind::Changed))
4814 && self.fs.is_dir(&dot_git_abs_path).await;
4815 if is_ignored {
4816 log::debug!(
4817 "ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories"
4818 );
4819 skip_ix(&mut ranges_to_drop, ix);
4820 continue;
4821 }
4822
4823 if !dot_git_abs_paths.contains(&dot_git_abs_path) {
4824 log::debug!(
4825 "detected update within git repo at {dot_git_abs_path:?}: {abs_path:?}"
4826 );
4827 dot_git_abs_paths.push(dot_git_abs_path);
4828 }
4829
4830 if is_dot_git {
4831 log::debug!(
4832 "ignoring event {abs_path:?} for .git directory itself (kind: {:?})",
4833 event.kind
4834 );
4835 skip_ix(&mut ranges_to_drop, ix);
4836 continue;
4837 }
4838
4839 // New directories can appear under the `refs` tree at any time, e.g. when a
4840 // remote is added or a branch name contains slashes. On platforms where the
4841 // native watcher is non-recursive they need their own watches, or subsequent
4842 // ref updates inside them would go unnoticed. The subtree is walked because
4843 // nested directories may have been created before this watch took effect.
4844 if matches!(event.kind, Some(PathEventKind::Created))
4845 && path_in_git_dir
4846 .components()
4847 .any(|component| component.as_os_str() == OsStr::new(REFS_DIR))
4848 {
4849 watch_dir_tree(
4850 abs_path.as_path().to_path_buf(),
4851 self.fs.as_ref(),
4852 self.watcher.as_ref(),
4853 )
4854 .await;
4855 }
4856 }
4857
4858 // A rescan event means the watcher lost sync and events under the
4859 // rescanned path were dropped, possibly including events inside `.git`
4860 // directories. Reload the git state of every repository with a git
4861 // directory under the rescanned path, since changes there may have
4862 // gone unseen.
4863 if self.track_git_repositories && matches!(event.kind, Some(PathEventKind::Rescan))
4864 {
4865 for repository in snapshot.git_repositories.values() {
4866 let affected_by_rescan = [
4867 &repository.dot_git_abs_path,
4868 &repository.common_dir_abs_path,
4869 &repository.repository_dir_abs_path,
4870 ]
4871 .iter()
4872 .any(|git_dir_abs_path| git_dir_abs_path.starts_with(abs_path.as_path()));
4873 let dot_git_abs_path = repository.dot_git_abs_path.to_path_buf();
4874 if affected_by_rescan && !dot_git_abs_paths.contains(&dot_git_abs_path) {
4875 log::debug!(
4876 "reloading git repo at {dot_git_abs_path:?} due to rescan of {abs_path:?}"
4877 );
4878 dot_git_abs_paths.push(dot_git_abs_path);
4879 }
4880 }
4881 }
4882
4883 if self.track_git_repositories
4884 && abs_path
4885 .as_path()
4886 .ends_with(Path::new(DOT_GIT).join(REPO_EXCLUDE))
4887 {
4888 if let Some(repository) = snapshot.git_repositories.values().find(|repo| {
4889 repo.common_dir_abs_path.join(REPO_EXCLUDE) == abs_path.as_path()
4890 }) {
4891 work_dirs_needing_exclude_update
4892 .push(repository.work_directory_abs_path.clone());
4893 }
4894 }
4895 }
4896
4897 for range_to_drop in ranges_to_drop.into_iter().rev() {
4898 events.drain(range_to_drop);
4899 }
4900 }
4901
4902 events.sort_unstable_by(|left, right| left.path.cmp(&right.path));
4903 events.dedup_by(|left, right| {
4904 if left.path == right.path {
4905 if matches!(left.kind, Some(fs::PathEventKind::Rescan)) {
4906 right.kind = left.kind;
4907 }
4908 true
4909 } else if left.path.starts_with(&right.path) {
4910 if matches!(left.kind, Some(fs::PathEventKind::Rescan)) {
4911 right.kind = left.kind;
4912 }
4913 true
4914 } else {
4915 false
4916 }
4917 });
4918
4919 let mut relative_paths = Vec::with_capacity(events.len());
4920
4921 {
4922 let snapshot = &self.state.lock().await.snapshot;
4923
4924 let mut ranges_to_drop = SmallVec::<[Range<usize>; 4]>::new();
4925
4926 for (ix, event) in events.iter().enumerate() {
4927 let abs_path = SanitizedPath::new(&event.path);
4928 // TODO: this strips the root case-sensitively, so on a case-insensitive
4929 // volume an event whose casing differs from the canonical root is
4930 // dropped. Once `fs` exposes per-volume case-sensitivity (e.g. on the
4931 // `Fs` trait, with a per-volume cache + `FakeFs` support), fold this
4932 // comparison on case-insensitive volumes.
4933 let relative_path = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path)
4934 && let Ok(path) = RelPath::new(path, PathStyle::local())
4935 {
4936 path
4937 } else if let Ok(path) = abs_path.strip_prefix(&root_path)
4938 && let Ok(path) = RelPath::new(path, PathStyle::local())
4939 {
4940 path
4941 } else if let Some(path) = snapshot.external_canonical_to_relative.iter().find_map(
4942 |(canonical, relative)| {
4943 abs_path
4944 .as_path()
4945 .strip_prefix(canonical.as_ref())
4946 .ok()
4947 .and_then(|suffix| {
4948 RelPath::new(suffix, PathStyle::local())
4949 .ok()
4950 .map(|suffix_rel| {
4951 std::borrow::Cow::Owned(
4952 relative.join(&suffix_rel).to_rel_path_buf(),
4953 )
4954 })
4955 })
4956 },
4957 ) {
4958 path
4959 } else {
4960 skip_ix(&mut ranges_to_drop, ix);
4961 continue;
4962 };
4963
4964 if self.track_git_repositories
4965 && abs_path.file_name() == Some(OsStr::new(GITIGNORE))
4966 {
4967 for (_, repo) in snapshot
4968 .git_repositories
4969 .iter()
4970 .filter(|(_, repo)| repo.directory_contains(&relative_path))
4971 {
4972 if !dot_git_abs_paths.iter().any(|dot_git_abs_path| {
4973 dot_git_abs_path == repo.common_dir_abs_path.as_ref()
4974 }) {
4975 dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf());
4976 }
4977 }
4978 }
4979
4980 let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| {
4981 snapshot
4982 .entry_for_path(parent)
4983 .is_some_and(|entry| entry.kind == EntryKind::Dir)
4984 });
4985 if !parent_dir_is_loaded {
4986 log::debug!("filtering event {relative_path:?} within unloaded directory");
4987 skip_ix(&mut ranges_to_drop, ix);
4988 continue;
4989 }
4990
4991 if self.settings.is_path_excluded(&relative_path) {
4992 skip_ix(&mut ranges_to_drop, ix);
4993 continue;
4994 }
4995
4996 relative_paths.push(EventRoot {
4997 path: relative_path.into_arc(),
4998 was_rescanned: matches!(event.kind, Some(fs::PathEventKind::Rescan)),
4999 });
5000 }
5001
5002 for range_to_drop in ranges_to_drop.into_iter().rev() {
5003 events.drain(range_to_drop);
5004 }
5005 }
5006
5007 if relative_paths.is_empty() && dot_git_abs_paths.is_empty() {
5008 return;
5009 }
5010
5011 if !work_dirs_needing_exclude_update.is_empty() {
5012 let mut state = self.state.lock().await;
5013 for work_dir_abs_path in work_dirs_needing_exclude_update {
5014 if let Some((_, needs_update)) = state
5015 .snapshot
5016 .repo_exclude_by_work_dir_abs_path
5017 .get_mut(&work_dir_abs_path)
5018 {
5019 *needs_update = true;
5020 }
5021 }
5022 }
5023
5024 self.state.lock().await.snapshot.scan_id += 1;
5025
5026 let (scan_job_tx, scan_job_rx) = async_channel::unbounded();
5027 if !relative_paths.is_empty() {
5028 log::debug!(
5029 "will update project paths {:?}",
5030 relative_paths
5031 .iter()
5032 .map(|event_root| &event_root.path)
5033 .collect::<Vec<_>>()
5034 );
5035 }
5036 self.reload_entries_for_paths(
5037 &root_path,
5038 &root_canonical_path,
5039 &relative_paths
5040 .iter()
5041 .map(|event_root| event_root.path.clone())
5042 .collect::<Vec<_>>(),
5043 events
5044 .into_iter()
5045 .map(|event| event.path)
5046 .collect::<Vec<_>>(),
5047 Some(scan_job_tx.clone()),
5048 )
5049 .await;
5050
5051 let affected_repo_roots = if !dot_git_abs_paths.is_empty() {
5052 self.update_git_repositories(dot_git_abs_paths).await
5053 } else {
5054 Vec::new()
5055 };
5056
5057 {
5058 let mut ignores_to_update = self.ignores_needing_update().await;
5059 ignores_to_update.extend(affected_repo_roots);
5060 let ignores_to_update = self.order_ignores(ignores_to_update).await;
5061 let snapshot = self.state.lock().await.snapshot.clone();
5062 self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update)
5063 .await;
5064 self.scan_dirs(false, scan_job_rx).await;
5065 }
5066
5067 {
5068 let mut state = self.state.lock().await;
5069 state.snapshot.completed_scan_id = state.snapshot.scan_id;
5070 let RemovedEntries { by_inode, by_path } = mem::take(&mut state.removed_entries);
5071 for entry in by_inode.into_values().chain(by_path.into_values()) {
5072 state.scanned_dirs.remove(&entry.id);
5073 }
5074 }
5075 self.send_status_update(false, SmallVec::new(), &relative_paths)
5076 .await;
5077 }
5078
5079 async fn update_global_gitignore(&self, abs_path: &Path) {
5080 let ignore = build_gitignore(abs_path, self.fs.as_ref())
5081 .await
5082 .log_err()
5083 .map(Arc::new);
5084 let (prev_snapshot, ignore_stack, abs_path) = {
5085 let mut state = self.state.lock().await;
5086 state.snapshot.global_gitignore = ignore;
5087 let abs_path = state.snapshot.abs_path().clone();
5088 let ignore_stack = state
5089 .snapshot
5090 .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref())
5091 .await;
5092 (state.snapshot.clone(), ignore_stack, abs_path)
5093 };
5094 let (scan_job_tx, scan_job_rx) = async_channel::unbounded();
5095 self.update_ignore_statuses_for_paths(
5096 scan_job_tx,
5097 prev_snapshot,
5098 vec![(abs_path, ignore_stack)],
5099 )
5100 .await;
5101 self.scan_dirs(false, scan_job_rx).await;
5102 self.send_status_update(false, SmallVec::new(), &[]).await;
5103 }
5104
5105 async fn forcibly_load_paths(&self, paths: &[Arc<RelPath>]) -> bool {
5106 let (scan_job_tx, scan_job_rx) = async_channel::unbounded();
5107 {
5108 let mut state = self.state.lock().await;
5109 let root_path = state.snapshot.abs_path.clone();
5110 for path in paths {
5111 for ancestor in path.ancestors() {
5112 if let Some(entry) = state.snapshot.entry_for_path(ancestor)
5113 && entry.kind == EntryKind::UnloadedDir
5114 {
5115 let abs_path = if entry.is_external {
5116 entry
5117 .canonical_path
5118 .as_ref()
5119 .map(|path| path.as_ref().to_path_buf())
5120 .unwrap_or_else(|| root_path.join(ancestor.as_std_path()))
5121 } else {
5122 root_path.join(ancestor.as_std_path())
5123 };
5124 state
5125 .enqueue_scan_dir(
5126 abs_path.into(),
5127 entry,
5128 &scan_job_tx,
5129 self.fs.as_ref(),
5130 )
5131 .await;
5132 state.paths_to_scan.insert(path.clone());
5133 break;
5134 }
5135 }
5136 }
5137 drop(scan_job_tx);
5138 }
5139 while let Ok(job) = scan_job_rx.recv().await {
5140 self.scan_dir(&job).await.log_err();
5141 }
5142
5143 !mem::take(&mut self.state.lock().await.paths_to_scan).is_empty()
5144 }
5145
5146 async fn scan_dirs(
5147 &self,
5148 enable_progress_updates: bool,
5149 scan_jobs_rx: async_channel::Receiver<ScanJob>,
5150 ) {
5151 if self
5152 .status_updates_tx
5153 .unbounded_send(ScanState::Started)
5154 .is_err()
5155 {
5156 return;
5157 }
5158
5159 let progress_update_count = AtomicUsize::new(0);
5160 self.executor
5161 .scoped_priority(Priority::Low, |scope| {
5162 for _ in 0..self.executor.num_cpus() {
5163 scope.spawn(async {
5164 let mut last_progress_update_count = 0;
5165 let progress_update_timer = self.progress_timer(enable_progress_updates).fuse();
5166 futures::pin_mut!(progress_update_timer);
5167
5168 loop {
5169 select_biased! {
5170 // Process any path refresh requests before moving on to process
5171 // the scan queue, so that user operations are prioritized.
5172 request = self.next_scan_request().fuse() => {
5173 let Ok(request) = request else { break };
5174 if !self.process_scan_request(request, true).await {
5175 return;
5176 }
5177 }
5178
5179 // Send periodic progress updates to the worktree. Use an atomic counter
5180 // to ensure that only one of the workers sends a progress update after
5181 // the update interval elapses.
5182 _ = progress_update_timer => {
5183 match progress_update_count.compare_exchange(
5184 last_progress_update_count,
5185 last_progress_update_count + 1,
5186 SeqCst,
5187 SeqCst
5188 ) {
5189 Ok(_) => {
5190 last_progress_update_count += 1;
5191 self.send_status_update(true, SmallVec::new(), &[])
5192 .await;
5193 }
5194 Err(count) => {
5195 last_progress_update_count = count;
5196 }
5197 }
5198 progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse());
5199 }
5200
5201 // Recursively load directories from the file system.
5202 job = scan_jobs_rx.recv().fuse() => {
5203 let Ok(job) = job else { break };
5204 if let Err(err) = self.scan_dir(&job).await
5205 && job.path.is_empty() {
5206 log::error!("error scanning directory {:?}: {}", job.abs_path, err);
5207 }
5208 }
5209 }
5210 }
5211 });
5212 }
5213 })
5214 .await;
5215 }
5216
5217 async fn send_status_update(
5218 &self,
5219 scanning: bool,
5220 barrier: SmallVec<[barrier::Sender; 1]>,
5221 event_roots: &[EventRoot],
5222 ) -> bool {
5223 let mut state = self.state.lock().await;
5224 if state.changed_paths.is_empty() && event_roots.is_empty() && scanning {
5225 return true;
5226 }
5227
5228 let merged_event_roots = merge_event_roots(&state.changed_paths, event_roots);
5229
5230 let new_snapshot = state.snapshot.clone();
5231 let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
5232 let changes = build_diff(
5233 self.phase,
5234 &old_snapshot,
5235 &new_snapshot,
5236 &merged_event_roots,
5237 );
5238 state.changed_paths.clear();
5239
5240 self.status_updates_tx
5241 .unbounded_send(ScanState::Updated {
5242 snapshot: new_snapshot,
5243 changes,
5244 scanning,
5245 barrier,
5246 })
5247 .is_ok()
5248 }
5249
5250 async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
5251 let root_abs_path;
5252 let root_char_bag;
5253 {
5254 let snapshot = &self.state.lock().await.snapshot;
5255 if self.settings.is_path_excluded(&job.path) {
5256 log::error!("skipping excluded directory {:?}", job.path);
5257 return Ok(());
5258 }
5259 log::trace!("scanning directory {:?}", job.path);
5260 root_abs_path = snapshot.abs_path().clone();
5261 root_char_bag = snapshot.root_char_bag;
5262 }
5263
5264 let next_entry_id = self.next_entry_id.clone();
5265 let mut ignore_stack = job.ignore_stack.clone();
5266 let mut new_ignore = None;
5267 let mut root_canonical_path = None;
5268 let mut new_entries: Vec<Entry> = Vec::new();
5269 let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
5270 let mut child_paths = self
5271 .fs
5272 .read_dir(&job.abs_path)
5273 .await?
5274 .filter_map(|entry| async {
5275 match entry {
5276 Ok(entry) => Some(entry),
5277 Err(error) => {
5278 log::error!("error processing entry {:?}", error);
5279 None
5280 }
5281 }
5282 })
5283 .collect::<Vec<_>>()
5284 .await;
5285
5286 // Ensure that .git and .gitignore are processed first.
5287 swap_to_front(&mut child_paths, GITIGNORE);
5288 swap_to_front(&mut child_paths, DOT_GIT);
5289
5290 if let Some(path) = child_paths.first()
5291 && path.ends_with(DOT_GIT)
5292 {
5293 ignore_stack.repo_root = Some(job.abs_path.clone());
5294 }
5295
5296 for child_abs_path in child_paths {
5297 let child_abs_path: Arc<Path> = child_abs_path.into();
5298 let child_name = child_abs_path.file_name().unwrap();
5299 let Some(child_path) = child_name
5300 .to_str()
5301 .and_then(|name| Some(job.path.join(RelPath::from_unix_str(name).ok()?)))
5302 else {
5303 continue;
5304 };
5305 let child_path: Arc<RelPath> = child_path.into();
5306
5307 if self.track_git_repositories {
5308 if child_name == DOT_GIT {
5309 let mut state = self.state.lock().await;
5310 state
5311 .insert_git_repository(
5312 child_path.clone(),
5313 self.fs.as_ref(),
5314 self.watcher.as_ref(),
5315 )
5316 .await;
5317 } else if child_name == GITIGNORE {
5318 match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
5319 Ok(ignore) => {
5320 let ignore = Arc::new(ignore);
5321 ignore_stack = ignore_stack.append(
5322 IgnoreKind::Gitignore(job.abs_path.clone()),
5323 ignore.clone(),
5324 );
5325 new_ignore = Some(ignore);
5326 }
5327 Err(error) => {
5328 log::error!(
5329 "error loading .gitignore file {:?} - {:?}",
5330 child_name,
5331 error
5332 );
5333 }
5334 }
5335 }
5336 }
5337
5338 if self.settings.is_path_excluded(&child_path) {
5339 log::debug!("skipping excluded child entry {child_path:?}");
5340
5341 self.state
5342 .lock()
5343 .await
5344 .remove_path_from_snapshot_and_unwatch(
5345 &child_path,
5346 self.watcher.as_ref(),
5347 true,
5348 );
5349 continue;
5350 }
5351
5352 let child_metadata = match self.fs.metadata(&child_abs_path).await {
5353 Ok(Some(metadata)) => metadata,
5354 Ok(None) => continue,
5355 Err(err) => {
5356 log::error!("error processing {:?}: {err:#}", child_abs_path.display());
5357 continue;
5358 }
5359 };
5360
5361 let mut child_entry = Entry::new(
5362 child_path.clone(),
5363 &child_metadata,
5364 ProjectEntryId::new(&next_entry_id),
5365 root_char_bag,
5366 None,
5367 );
5368
5369 if job.is_external {
5370 child_entry.is_external = true;
5371 } else if child_metadata.is_symlink {
5372 let canonical_path = match self.fs.canonicalize(&child_abs_path).await {
5373 Ok(path) => path,
5374 Err(err) => {
5375 log::error!("error reading target of symlink {child_abs_path:?}: {err:#}",);
5376 continue;
5377 }
5378 };
5379
5380 // lazily canonicalize the root path in order to determine if
5381 // symlinks point outside of the worktree.
5382 let root_canonical_path = match &root_canonical_path {
5383 Some(path) => path,
5384 None => match self.fs.canonicalize(&root_abs_path).await {
5385 Ok(path) => root_canonical_path.insert(path),
5386 Err(err) => {
5387 log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err);
5388 continue;
5389 }
5390 },
5391 };
5392
5393 if !canonical_path.starts_with(root_canonical_path) {
5394 child_entry.is_external = true;
5395 }
5396
5397 if child_metadata.is_dir {
5398 let mut state = self.state.lock().await;
5399 let paths = state
5400 .symlink_paths_by_target
5401 .entry(Arc::from(canonical_path.clone()))
5402 .or_default();
5403 if !paths.iter().any(|path| path == &child_path) {
5404 paths.push(child_path.clone());
5405 }
5406 }
5407
5408 child_entry.canonical_path = Some(canonical_path.into());
5409 }
5410
5411 if child_entry.is_dir() {
5412 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
5413 child_entry.is_always_included =
5414 self.settings.is_path_always_included(&child_path, true);
5415
5416 // Avoid recursing until crash in the case of a recursive symlink
5417 if job.ancestor_inodes.contains(&child_entry.inode) {
5418 new_jobs.push(None);
5419 } else {
5420 let mut ancestor_inodes = job.ancestor_inodes.clone();
5421 ancestor_inodes.insert(child_entry.inode);
5422
5423 new_jobs.push(Some(ScanJob {
5424 abs_path: child_abs_path.clone(),
5425 path: child_path,
5426 is_external: child_entry.is_external,
5427 ignore_stack: if child_entry.is_ignored {
5428 IgnoreStack::all()
5429 } else {
5430 ignore_stack.clone()
5431 },
5432 ancestor_inodes,
5433 scan_queue: job.scan_queue.clone(),
5434 }));
5435 }
5436 } else {
5437 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
5438 child_entry.is_always_included =
5439 self.settings.is_path_always_included(&child_path, false);
5440 }
5441
5442 {
5443 let relative_path = job
5444 .path
5445 .join(RelPath::from_unix_str(child_name.to_str().unwrap()).unwrap());
5446 if self.is_path_private(&relative_path) {
5447 log::debug!("detected private file: {relative_path:?}");
5448 child_entry.is_private = true;
5449 }
5450 if self.settings.is_path_hidden(&relative_path) {
5451 log::debug!("detected hidden file: {relative_path:?}");
5452 child_entry.is_hidden = true;
5453 }
5454 }
5455
5456 new_entries.push(child_entry);
5457 }
5458
5459 let mut state = self.state.lock().await;
5460 // Identify any subdirectories that should not be scanned.
5461 let mut job_ix = 0;
5462 for entry in &mut new_entries {
5463 state.reuse_entry_id(entry);
5464 if entry.is_dir() {
5465 if !self.should_scan_directory(&state, entry) {
5466 log::debug!("defer scanning directory {:?}", entry.path);
5467 entry.kind = EntryKind::UnloadedDir;
5468 new_jobs[job_ix] = None;
5469 }
5470 job_ix += 1;
5471 }
5472 if entry.is_always_included {
5473 state
5474 .snapshot
5475 .always_included_entries
5476 .push(entry.path.clone());
5477 }
5478 }
5479
5480 state.populate_dir(job.path.clone(), new_entries, new_ignore);
5481 // For external entries, watch the canonical (resolved) path so OS-level
5482 // FS events on the real filesystem location are observed. The same
5483 // canonical path is stored in both `external_canonical_to_relative`
5484 // (for translating canonical-path FS events back to worktree-relative
5485 // paths) and `watched_dir_abs_paths_by_entry_id` (used by `remove_path`
5486 // to know which abs path to unwatch), so both cleanup paths agree on
5487 // the path the watcher was actually registered on.
5488 //
5489 // `canonicalize` is an async filesystem operation that may suspend, so
5490 // the lock must not be held across the await point below.
5491 drop(state);
5492 let watched_abs_path: Option<Arc<Path>> = if job.is_external {
5493 self.fs
5494 .canonicalize(job.abs_path.as_ref())
5495 .await
5496 .ok()
5497 .map(|canonical| {
5498 let canonical: Arc<Path> = canonical.into();
5499 self.watcher.add(&canonical).log_err();
5500 canonical
5501 })
5502 } else {
5503 self.watcher.add(job.abs_path.as_ref()).log_err();
5504 Some(job.abs_path.clone())
5505 };
5506
5507 let mut state = self.state.lock().await;
5508 if let Some(watched_abs_path) = &watched_abs_path {
5509 if job.is_external {
5510 state
5511 .snapshot
5512 .external_canonical_to_relative
5513 .insert(watched_abs_path.clone(), job.path.clone());
5514 }
5515 if let Some(entry_id) = state
5516 .snapshot
5517 .entry_for_path(&job.path)
5518 .map(|entry| entry.id)
5519 {
5520 state
5521 .watched_dir_abs_paths_by_entry_id
5522 .insert(entry_id, watched_abs_path.clone());
5523 }
5524 }
5525
5526 for new_job in new_jobs.into_iter().flatten() {
5527 job.scan_queue
5528 .try_send(new_job)
5529 .expect("channel is unbounded");
5530 }
5531
5532 Ok(())
5533 }
5534
5535 /// All list arguments should be sorted before calling this function
5536 async fn reload_entries_for_paths(
5537 &self,
5538 root_abs_path: &SanitizedPath,
5539 root_canonical_path: &SanitizedPath,
5540 relative_paths: &[Arc<RelPath>],
5541 abs_paths: Vec<PathBuf>,
5542 scan_queue_tx: Option<Sender<ScanJob>>,
5543 ) {
5544 // grab metadata for all requested paths
5545 let metadata = futures::future::join_all(
5546 abs_paths
5547 .iter()
5548 .map(|abs_path| async move {
5549 let metadata = self.fs.metadata(abs_path).await?;
5550 if let Some(metadata) = metadata {
5551 let canonical_path = self.fs.canonicalize(abs_path).await?;
5552
5553 // If we're on a case-insensitive filesystem (default on macOS), we want
5554 // to only ignore metadata for non-symlink files if their absolute-path matches
5555 // the canonical-path.
5556 // Because if not, this might be a case-only-renaming (`mv test.txt TEST.TXT`)
5557 // and we want to ignore the metadata for the old path (`test.txt`) so it's
5558 // treated as removed.
5559 if !self.fs_case_sensitive && !metadata.is_symlink {
5560 let canonical_file_name = canonical_path.file_name();
5561 let file_name = abs_path.file_name();
5562 if canonical_file_name != file_name {
5563 return Ok(None);
5564 }
5565 }
5566
5567 anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path))))
5568 } else {
5569 Ok(None)
5570 }
5571 })
5572 .collect::<Vec<_>>(),
5573 )
5574 .await;
5575
5576 let mut new_ancestor_repo =
5577 if self.track_git_repositories && relative_paths.iter().any(|path| path.is_empty()) {
5578 Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await)
5579 } else {
5580 None
5581 };
5582
5583 let mut state = self.state.lock().await;
5584 let doing_recursive_update = scan_queue_tx.is_some();
5585
5586 // Remove any entries for paths that no longer exist or are being recursively
5587 // refreshed. Do this before adding any new entries, so that renames can be
5588 // detected regardless of the order of the paths.
5589 let mut paths_to_process = Vec::with_capacity(relative_paths.len());
5590 for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
5591 let path_was_removed = matches!(metadata, Ok(None));
5592 let removed_descendant_paths = if path_was_removed || doing_recursive_update {
5593 state.remove_path_from_snapshot(path, path_was_removed)
5594 } else {
5595 Vec::new()
5596 };
5597 paths_to_process.push((path, metadata, removed_descendant_paths));
5598 }
5599
5600 for (path, metadata, removed_descendant_abs_paths) in paths_to_process {
5601 let abs_path: Arc<Path> = root_abs_path.join(path.as_std_path()).into();
5602 match metadata {
5603 Ok(Some((metadata, canonical_path))) => {
5604 let ignore_stack = state
5605 .snapshot
5606 .ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref())
5607 .await;
5608 let is_external = !canonical_path.starts_with(&root_canonical_path);
5609 let entry_id = state.entry_id_for(self.next_entry_id.as_ref(), path, &metadata);
5610 let mut fs_entry = Entry::new(
5611 path.clone(),
5612 &metadata,
5613 entry_id,
5614 state.snapshot.root_char_bag,
5615 if metadata.is_symlink {
5616 Some(canonical_path.as_path().to_path_buf().into())
5617 } else {
5618 None
5619 },
5620 );
5621
5622 let is_dir = fs_entry.is_dir();
5623 fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
5624 fs_entry.is_external = is_external;
5625 fs_entry.is_private = self.is_path_private(path);
5626 fs_entry.is_always_included =
5627 self.settings.is_path_always_included(path, is_dir);
5628 fs_entry.is_hidden = self.settings.is_path_hidden(path);
5629
5630 if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
5631 if self.should_scan_directory(&state, &fs_entry)
5632 || (self.track_git_repositories
5633 && fs_entry.path.is_empty()
5634 && abs_path.file_name() == Some(OsStr::new(DOT_GIT)))
5635 {
5636 state
5637 .enqueue_scan_dir(
5638 abs_path,
5639 &fs_entry,
5640 scan_queue_tx,
5641 self.fs.as_ref(),
5642 )
5643 .await;
5644 } else {
5645 fs_entry.kind = EntryKind::UnloadedDir;
5646 }
5647 }
5648
5649 state
5650 .insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref())
5651 .await;
5652
5653 if path.is_empty()
5654 && let Some((ignores, exclude, repo)) = new_ancestor_repo.take()
5655 {
5656 log::trace!("updating ancestor git repository");
5657 state.snapshot.ignores_by_parent_abs_path.extend(ignores);
5658 if let Some((ancestor_dot_git, work_directory)) = repo {
5659 if let Some(exclude) = exclude {
5660 let work_directory_abs_path =
5661 state.snapshot.work_directory_abs_path(&work_directory);
5662
5663 state
5664 .snapshot
5665 .repo_exclude_by_work_dir_abs_path
5666 .insert(work_directory_abs_path.into(), (exclude, false));
5667 }
5668 state
5669 .insert_git_repository_for_path(
5670 work_directory,
5671 ancestor_dot_git.into(),
5672 self.fs.as_ref(),
5673 self.watcher.as_ref(),
5674 )
5675 .await
5676 .log_err();
5677 }
5678 }
5679 }
5680 Ok(None) => {
5681 self.remove_repo_path(path.clone(), &mut state.snapshot);
5682 state.unwatch_path(
5683 self.watcher.as_ref(),
5684 path,
5685 removed_descendant_abs_paths,
5686 false,
5687 );
5688 }
5689 Err(err) => {
5690 log::error!("error reading file {abs_path:?} on event: {err:#}");
5691 state.unwatch_path(
5692 self.watcher.as_ref(),
5693 path,
5694 removed_descendant_abs_paths,
5695 false,
5696 );
5697 }
5698 }
5699 }
5700
5701 util::extend_sorted(
5702 &mut state.changed_paths,
5703 relative_paths.iter().cloned(),
5704 usize::MAX,
5705 Ord::cmp,
5706 );
5707 }
5708
5709 fn remove_repo_path(&self, path: Arc<RelPath>, snapshot: &mut LocalSnapshot) -> Option<()> {
5710 if !path.components().any(|component| component == DOT_GIT)
5711 && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(&path)
5712 {
5713 let id = local_repo.work_directory_id;
5714 log::debug!("remove repo path: {:?}", path);
5715 snapshot.git_repositories.remove(&id);
5716 return Some(());
5717 }
5718
5719 Some(())
5720 }
5721
5722 async fn update_ignore_statuses_for_paths(
5723 &self,
5724 scan_job_tx: Sender<ScanJob>,
5725 prev_snapshot: LocalSnapshot,
5726 ignores_to_update: Vec<(Arc<Path>, IgnoreStack)>,
5727 ) {
5728 let (ignore_queue_tx, ignore_queue_rx) = async_channel::unbounded();
5729 {
5730 for (parent_abs_path, ignore_stack) in ignores_to_update {
5731 ignore_queue_tx
5732 .send_blocking(UpdateIgnoreStatusJob {
5733 abs_path: parent_abs_path,
5734 ignore_stack,
5735 ignore_queue: ignore_queue_tx.clone(),
5736 scan_queue: scan_job_tx.clone(),
5737 })
5738 .unwrap();
5739 }
5740 }
5741 drop(ignore_queue_tx);
5742
5743 self.executor
5744 .scoped(|scope| {
5745 for _ in 0..self.executor.num_cpus() {
5746 scope.spawn(async {
5747 loop {
5748 select_biased! {
5749 // Process any path refresh requests before moving on to process
5750 // the queue of ignore statuses.
5751 request = self.next_scan_request().fuse() => {
5752 let Ok(request) = request else { break };
5753 if !self.process_scan_request(request, true).await {
5754 return;
5755 }
5756 }
5757
5758 // Recursively process directories whose ignores have changed.
5759 job = ignore_queue_rx.recv().fuse() => {
5760 let Ok(job) = job else { break };
5761 self.update_ignore_status(job, &prev_snapshot).await;
5762 }
5763 }
5764 }
5765 });
5766 }
5767 })
5768 .await;
5769 }
5770
5771 async fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
5772 let mut ignores_to_update = Vec::new();
5773 let mut excludes_to_load: Vec<(Arc<Path>, PathBuf)> = Vec::new();
5774
5775 // First pass: collect updates and drop stale entries without awaiting.
5776 {
5777 let snapshot = &mut self.state.lock().await.snapshot;
5778 let abs_path = snapshot.abs_path.clone();
5779 let mut repo_exclude_keys_to_remove: Vec<Arc<Path>> = Vec::new();
5780
5781 for (work_dir_abs_path, (_, needs_update)) in
5782 snapshot.repo_exclude_by_work_dir_abs_path.iter_mut()
5783 {
5784 let repository = snapshot
5785 .git_repositories
5786 .iter()
5787 .find(|(_, repo)| &repo.work_directory_abs_path == work_dir_abs_path);
5788
5789 if *needs_update {
5790 *needs_update = false;
5791 if work_dir_abs_path.starts_with(abs_path.as_path()) {
5792 ignores_to_update.push(work_dir_abs_path.clone());
5793 } else {
5794 ignores_to_update.push(abs_path.as_path().into());
5795 }
5796
5797 if let Some((_, repository)) = repository {
5798 let exclude_abs_path = repository.common_dir_abs_path.join(REPO_EXCLUDE);
5799 excludes_to_load.push((work_dir_abs_path.clone(), exclude_abs_path));
5800 }
5801 }
5802
5803 if repository.is_none() {
5804 repo_exclude_keys_to_remove.push(work_dir_abs_path.clone());
5805 }
5806 }
5807
5808 for key in repo_exclude_keys_to_remove {
5809 snapshot.repo_exclude_by_work_dir_abs_path.remove(&key);
5810 }
5811
5812 snapshot
5813 .ignores_by_parent_abs_path
5814 .retain(|parent_abs_path, (_, needs_update)| {
5815 if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path())
5816 && let Some(parent_path) =
5817 RelPath::new(&parent_path, PathStyle::local()).log_err()
5818 {
5819 if *needs_update {
5820 *needs_update = false;
5821 if snapshot.snapshot.entry_for_path(&parent_path).is_some() {
5822 ignores_to_update.push(parent_abs_path.clone());
5823 }
5824 }
5825
5826 let ignore_path =
5827 parent_path.join(RelPath::from_unix_str(GITIGNORE).unwrap());
5828 if snapshot.snapshot.entry_for_path(&ignore_path).is_none() {
5829 return false;
5830 }
5831 }
5832 true
5833 });
5834 }
5835
5836 // Load gitignores asynchronously (outside the lock)
5837 let mut loaded_excludes: Vec<(Arc<Path>, Arc<Gitignore>)> = Vec::new();
5838 for (work_dir_abs_path, exclude_abs_path) in excludes_to_load {
5839 if let Ok(current_exclude) =
5840 build_gitignore_with_root(&exclude_abs_path, &work_dir_abs_path, self.fs.as_ref())
5841 .await
5842 {
5843 loaded_excludes.push((work_dir_abs_path, Arc::new(current_exclude)));
5844 }
5845 }
5846
5847 // Second pass: apply updates.
5848 if !loaded_excludes.is_empty() {
5849 let snapshot = &mut self.state.lock().await.snapshot;
5850
5851 for (work_dir_abs_path, exclude) in loaded_excludes {
5852 if let Some((existing_exclude, _)) = snapshot
5853 .repo_exclude_by_work_dir_abs_path
5854 .get_mut(&work_dir_abs_path)
5855 {
5856 *existing_exclude = exclude;
5857 }
5858 }
5859 }
5860
5861 ignores_to_update
5862 }
5863
5864 async fn order_ignores(&self, mut ignores: Vec<Arc<Path>>) -> Vec<(Arc<Path>, IgnoreStack)> {
5865 let fs = self.fs.clone();
5866 let snapshot = self.state.lock().await.snapshot.clone();
5867 ignores.sort_unstable();
5868 let mut ignores_to_update = ignores.into_iter().peekable();
5869
5870 let mut result = vec![];
5871 while let Some(parent_abs_path) = ignores_to_update.next() {
5872 while ignores_to_update
5873 .peek()
5874 .map_or(false, |p| p.starts_with(&parent_abs_path))
5875 {
5876 ignores_to_update.next().unwrap();
5877 }
5878 let ignore_stack = snapshot
5879 .ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref())
5880 .await;
5881 result.push((parent_abs_path, ignore_stack));
5882 }
5883
5884 result
5885 }
5886
5887 async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
5888 log::trace!("update ignore status {:?}", job.abs_path);
5889
5890 let mut ignore_stack = job.ignore_stack;
5891 if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
5892 ignore_stack =
5893 ignore_stack.append(IgnoreKind::Gitignore(job.abs_path.clone()), ignore.clone());
5894 }
5895
5896 let mut entries_by_id_edits = Vec::new();
5897 let mut entries_by_path_edits = Vec::new();
5898 let Some(path) = job
5899 .abs_path
5900 .strip_prefix(snapshot.abs_path.as_path())
5901 .map_err(|_| {
5902 anyhow::anyhow!(
5903 "Failed to strip prefix '{}' from path '{}'",
5904 snapshot.abs_path.as_path().display(),
5905 job.abs_path.display()
5906 )
5907 })
5908 .log_err()
5909 else {
5910 return;
5911 };
5912
5913 let Some(path) = RelPath::new(&path, PathStyle::local()).log_err() else {
5914 return;
5915 };
5916
5917 if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await
5918 && metadata.is_dir
5919 {
5920 ignore_stack.repo_root = Some(job.abs_path.clone());
5921 }
5922
5923 for mut entry in snapshot.child_entries(&path).cloned() {
5924 let was_ignored = entry.is_ignored;
5925 let abs_path: Arc<Path> = snapshot.absolutize(&entry.path).into();
5926 entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
5927
5928 if entry.is_dir() {
5929 let child_ignore_stack = if entry.is_ignored {
5930 IgnoreStack::all()
5931 } else {
5932 ignore_stack.clone()
5933 };
5934
5935 // Scan any directories that were previously ignored and weren't previously scanned.
5936 if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
5937 let state = self.state.lock().await;
5938 if self.should_scan_directory(&state, &entry) {
5939 state
5940 .enqueue_scan_dir(
5941 abs_path.clone(),
5942 &entry,
5943 &job.scan_queue,
5944 self.fs.as_ref(),
5945 )
5946 .await;
5947 }
5948 }
5949
5950 job.ignore_queue
5951 .send(UpdateIgnoreStatusJob {
5952 abs_path: abs_path.clone(),
5953 ignore_stack: child_ignore_stack,
5954 ignore_queue: job.ignore_queue.clone(),
5955 scan_queue: job.scan_queue.clone(),
5956 })
5957 .await
5958 .unwrap();
5959 }
5960
5961 if entry.is_ignored != was_ignored {
5962 let mut path_entry = snapshot.entries_by_id.get(&entry.id, ()).unwrap().clone();
5963 path_entry.scan_id = snapshot.scan_id;
5964 path_entry.is_ignored = entry.is_ignored;
5965 entries_by_id_edits.push(Edit::Insert(path_entry));
5966 entries_by_path_edits.push(Edit::Insert(entry));
5967 }
5968 }
5969
5970 let state = &mut self.state.lock().await;
5971 for edit in &entries_by_path_edits {
5972 if let Edit::Insert(entry) = edit
5973 && let Err(ix) = state.changed_paths.binary_search(&entry.path)
5974 {
5975 state.changed_paths.insert(ix, entry.path.clone());
5976 }
5977 }
5978
5979 state
5980 .snapshot
5981 .entries_by_path
5982 .edit(entries_by_path_edits, ());
5983 state.snapshot.entries_by_id.edit(entries_by_id_edits, ());
5984 }
5985
5986 async fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Vec<Arc<Path>> {
5987 log::trace!("reloading repositories: {dot_git_paths:?}");
5988 let mut state = self.state.lock().await;
5989 let scan_id = state.snapshot.scan_id;
5990 let mut affected_repo_roots = Vec::new();
5991 for dot_git_dir in dot_git_paths {
5992 // Several repositories can share a git directory: a linked worktree's
5993 // commondir is the main checkout's `.git`, so a ref update there must
5994 // refresh every repository that reads from it.
5995 let existing_work_directory_ids = state
5996 .snapshot
5997 .git_repositories
5998 .iter()
5999 .filter_map(|(&work_directory_id, repo)| {
6000 let dot_git_dir = SanitizedPath::new(&dot_git_dir);
6001 if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir
6002 || SanitizedPath::new(repo.repository_dir_abs_path.as_ref()) == dot_git_dir
6003 || SanitizedPath::new(repo.dot_git_abs_path.as_ref()) == dot_git_dir
6004 {
6005 Some(work_directory_id)
6006 } else {
6007 None
6008 }
6009 })
6010 .collect::<Vec<_>>();
6011
6012 if existing_work_directory_ids.is_empty() {
6013 let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else {
6014 // A `.git` path outside the worktree root is not
6015 // ours to register. This happens legitimately when
6016 // `.git` is a gitfile pointing outside the worktree
6017 // (linked worktrees and submodules), and also when
6018 // a rescan of a linked worktree's commondir arrives
6019 // after the worktree's repository has already been
6020 // unregistered.
6021 continue;
6022 };
6023 affected_repo_roots.push(dot_git_dir.parent().unwrap().into());
6024 state
6025 .insert_git_repository(
6026 RelPath::new(relative, PathStyle::local())
6027 .unwrap()
6028 .into_arc(),
6029 self.fs.as_ref(),
6030 self.watcher.as_ref(),
6031 )
6032 .await;
6033 } else {
6034 for work_directory_id in existing_work_directory_ids {
6035 state
6036 .snapshot
6037 .git_repositories
6038 .update(&work_directory_id, |entry| {
6039 entry.git_dir_scan_id = scan_id;
6040 });
6041 }
6042 }
6043 }
6044
6045 // Remove any git repositories whose .git entry no longer exists.
6046 let snapshot = &mut state.snapshot;
6047 let mut ids_to_preserve = HashSet::default();
6048 for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
6049 let exists_in_snapshot =
6050 snapshot
6051 .entry_for_id(work_directory_id)
6052 .is_some_and(|entry| {
6053 snapshot
6054 .entry_for_path(
6055 &entry.path.join(RelPath::from_unix_str(DOT_GIT).unwrap()),
6056 )
6057 .is_some()
6058 });
6059
6060 // Only drop a repository when we can positively confirm that its git
6061 // directory is gone. `metadata` returns `Ok(None)` for a confirmed
6062 // absence, but `Err(_)` for a transient failure (which can happen
6063 // under heavy filesystem churn). Treating an error as a deletion
6064 // makes the repository flap out of and back into the snapshot,
6065 // causing the GitStore to repeatedly tear it down and re-create it
6066 // with a fresh `RepositoryId`. So preserve the repository unless the
6067 // `.git` entry is confirmed absent.
6068 let dot_git_present =
6069 !matches!(self.fs.metadata(&entry.dot_git_abs_path).await, Ok(None));
6070
6071 if exists_in_snapshot || dot_git_present {
6072 ids_to_preserve.insert(work_directory_id);
6073 }
6074 }
6075
6076 snapshot
6077 .git_repositories
6078 .retain(|work_directory_id, entry| {
6079 let preserve = ids_to_preserve.contains(work_directory_id);
6080 if !preserve {
6081 affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into());
6082 snapshot
6083 .repo_exclude_by_work_dir_abs_path
6084 .remove(&entry.work_directory_abs_path);
6085 }
6086 preserve
6087 });
6088
6089 affected_repo_roots
6090 }
6091
6092 async fn progress_timer(&self, running: bool) {
6093 if !running {
6094 return futures::future::pending().await;
6095 }
6096
6097 #[cfg(feature = "test-support")]
6098 if self.fs.is_fake() {
6099 return self.executor.simulate_random_delay().await;
6100 }
6101
6102 self.executor.timer(FS_WATCH_LATENCY).await
6103 }
6104
6105 fn is_path_private(&self, path: &RelPath) -> bool {
6106 !self.share_private_files && self.settings.is_path_private(path)
6107 }
6108
6109 fn should_scan_directory(&self, state: &BackgroundScannerState, entry: &Entry) -> bool {
6110 let scannable = state.scanning_enabled
6111 && (!entry.is_external
6112 || self.settings.scan_symlinks == settings::ScanSymlinksSetting::Always)
6113 && (!entry.is_ignored || entry.is_always_included);
6114
6115 scannable
6116 || entry.path.file_name() == Some(DOT_GIT)
6117 || entry.path.file_name() == Some(local_settings_folder_name())
6118 || entry.path.file_name() == Some(local_vscode_folder_name())
6119 || state.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
6120 || state
6121 .paths_to_scan
6122 .iter()
6123 .any(|p| p.starts_with(&entry.path))
6124 || state
6125 .path_prefixes_to_scan
6126 .iter()
6127 .any(|p| entry.path.starts_with(p))
6128 }
6129
6130 async fn next_scan_request(&self) -> Result<ScanRequest> {
6131 let mut request = self.scan_requests_rx.recv().await?;
6132 while let Ok(next_request) = self.scan_requests_rx.try_recv() {
6133 request.relative_paths.extend(next_request.relative_paths);
6134 request.done.extend(next_request.done);
6135 }
6136 Ok(request)
6137 }
6138}
6139
6140async fn discover_ancestor_git_repo(
6141 fs: Arc<dyn Fs>,
6142 root_abs_path: &SanitizedPath,
6143) -> (
6144 HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
6145 Option<Arc<Gitignore>>,
6146 Option<(PathBuf, WorkDirectory)>,
6147) {
6148 let mut exclude = None;
6149 let mut ignores = HashMap::default();
6150 for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
6151 if index != 0 {
6152 if ancestor == paths::home_dir() {
6153 // Unless $HOME is itself the worktree root, don't consider it as a
6154 // containing git repository---expensive and likely unwanted.
6155 break;
6156 } else if let Ok(ignore) = build_gitignore(&ancestor.join(GITIGNORE), fs.as_ref()).await
6157 {
6158 ignores.insert(ancestor.into(), (ignore.into(), false));
6159 }
6160 }
6161
6162 let ancestor_dot_git = ancestor.join(DOT_GIT);
6163 log::trace!("considering ancestor: {ancestor_dot_git:?}");
6164 // Check whether the directory or file called `.git` exists (in the
6165 // case of worktrees it's a file.)
6166 if fs
6167 .metadata(&ancestor_dot_git)
6168 .await
6169 .is_ok_and(|metadata| metadata.is_some())
6170 {
6171 let dot_git_abs_path = if index != 0 {
6172 // We canonicalize, since the FS events use the canonicalized path.
6173 match fs.canonicalize(&ancestor_dot_git).await.log_err() {
6174 Some(path) => path,
6175 None => continue,
6176 }
6177 } else {
6178 ancestor_dot_git.clone()
6179 };
6180 let dot_git_abs_path: Arc<Path> = dot_git_abs_path.as_path().into();
6181 let (_, common_dir_abs_path) = discover_git_paths(&dot_git_abs_path, fs.as_ref()).await;
6182
6183 let repo_exclude_abs_path = common_dir_abs_path.join(REPO_EXCLUDE);
6184 if let Ok(repo_exclude) =
6185 build_gitignore_with_root(&repo_exclude_abs_path, ancestor, fs.as_ref()).await
6186 {
6187 exclude = Some(Arc::new(repo_exclude));
6188 }
6189
6190 if index != 0 {
6191 let location_in_repo = root_abs_path
6192 .as_path()
6193 .strip_prefix(ancestor)
6194 .unwrap()
6195 .into();
6196 log::info!("inserting parent git repo for this worktree: {location_in_repo:?}");
6197 // We associate the external git repo with our root folder and
6198 // also mark where in the git repo the root folder is located.
6199 return (
6200 ignores,
6201 exclude,
6202 Some((
6203 dot_git_abs_path.as_ref().into(),
6204 WorkDirectory::AboveProject {
6205 absolute_path: ancestor.into(),
6206 location_in_repo,
6207 },
6208 )),
6209 );
6210 }
6211
6212 break;
6213 }
6214 }
6215
6216 (ignores, exclude, None)
6217}
6218
6219fn merge_event_roots(changed_paths: &[Arc<RelPath>], event_roots: &[EventRoot]) -> Vec<EventRoot> {
6220 let mut merged_event_roots = Vec::with_capacity(changed_paths.len() + event_roots.len());
6221 let mut changed_paths = changed_paths.iter().peekable();
6222 let mut event_roots = event_roots.iter().peekable();
6223 while let (Some(path), Some(event_root)) = (changed_paths.peek(), event_roots.peek()) {
6224 match path.cmp(&&event_root.path) {
6225 Ordering::Less => {
6226 merged_event_roots.push(EventRoot {
6227 path: (*changed_paths.next().expect("peeked changed path")).clone(),
6228 was_rescanned: false,
6229 });
6230 }
6231 Ordering::Equal => {
6232 merged_event_roots.push((*event_roots.next().expect("peeked event root")).clone());
6233 changed_paths.next();
6234 }
6235 Ordering::Greater => {
6236 merged_event_roots.push((*event_roots.next().expect("peeked event root")).clone());
6237 }
6238 }
6239 }
6240 merged_event_roots.extend(changed_paths.map(|path| EventRoot {
6241 path: path.clone(),
6242 was_rescanned: false,
6243 }));
6244 merged_event_roots.extend(event_roots.cloned());
6245 merged_event_roots
6246}
6247
6248fn build_diff(
6249 phase: BackgroundScannerPhase,
6250 old_snapshot: &Snapshot,
6251 new_snapshot: &Snapshot,
6252 event_roots: &[EventRoot],
6253) -> UpdatedEntriesSet {
6254 use BackgroundScannerPhase::*;
6255 use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated};
6256
6257 // Identify which paths have changed. Use the known set of changed
6258 // parent paths to optimize the search.
6259 let mut changes = Vec::new();
6260
6261 let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>(());
6262 let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>(());
6263 let mut last_newly_loaded_dir_path = None;
6264 old_paths.next();
6265 new_paths.next();
6266 for event_root in event_roots {
6267 let path = PathKey(event_root.path.clone());
6268 if old_paths.item().is_some_and(|e| e.path < path.0) {
6269 old_paths.seek_forward(&path, Bias::Left);
6270 }
6271 if new_paths.item().is_some_and(|e| e.path < path.0) {
6272 new_paths.seek_forward(&path, Bias::Left);
6273 }
6274 loop {
6275 match (old_paths.item(), new_paths.item()) {
6276 (Some(old_entry), Some(new_entry)) => {
6277 if old_entry.path > path.0
6278 && new_entry.path > path.0
6279 && !old_entry.path.starts_with(&path.0)
6280 && !new_entry.path.starts_with(&path.0)
6281 {
6282 break;
6283 }
6284
6285 match Ord::cmp(&old_entry.path, &new_entry.path) {
6286 Ordering::Less => {
6287 changes.push((old_entry.path.clone(), old_entry.id, Removed));
6288 old_paths.next();
6289 }
6290 Ordering::Equal => {
6291 if phase == EventsReceivedDuringInitialScan {
6292 if old_entry.id != new_entry.id {
6293 changes.push((old_entry.path.clone(), old_entry.id, Removed));
6294 }
6295 // If the worktree was not fully initialized when this event was generated,
6296 // we can't know whether this entry was added during the scan or whether
6297 // it was merely updated.
6298 changes.push((
6299 new_entry.path.clone(),
6300 new_entry.id,
6301 AddedOrUpdated,
6302 ));
6303 } else if old_entry.id != new_entry.id {
6304 changes.push((old_entry.path.clone(), old_entry.id, Removed));
6305 changes.push((new_entry.path.clone(), new_entry.id, Added));
6306 } else if old_entry != new_entry {
6307 if old_entry.kind.is_unloaded() {
6308 last_newly_loaded_dir_path = Some(&new_entry.path);
6309 changes.push((new_entry.path.clone(), new_entry.id, Loaded));
6310 } else {
6311 changes.push((new_entry.path.clone(), new_entry.id, Updated));
6312 }
6313 } else if event_root.was_rescanned {
6314 changes.push((new_entry.path.clone(), new_entry.id, Updated));
6315 }
6316 old_paths.next();
6317 new_paths.next();
6318 }
6319 Ordering::Greater => {
6320 let is_newly_loaded = phase == InitialScan
6321 || last_newly_loaded_dir_path
6322 .as_ref()
6323 .is_some_and(|dir| new_entry.path.starts_with(dir));
6324 changes.push((
6325 new_entry.path.clone(),
6326 new_entry.id,
6327 if is_newly_loaded { Loaded } else { Added },
6328 ));
6329 new_paths.next();
6330 }
6331 }
6332 }
6333 (Some(old_entry), None) => {
6334 changes.push((old_entry.path.clone(), old_entry.id, Removed));
6335 old_paths.next();
6336 }
6337 (None, Some(new_entry)) => {
6338 let is_newly_loaded = phase == InitialScan
6339 || last_newly_loaded_dir_path
6340 .as_ref()
6341 .is_some_and(|dir| new_entry.path.starts_with(dir));
6342 changes.push((
6343 new_entry.path.clone(),
6344 new_entry.id,
6345 if is_newly_loaded { Loaded } else { Added },
6346 ));
6347 new_paths.next();
6348 }
6349 (None, None) => break,
6350 }
6351 }
6352 }
6353
6354 changes.into()
6355}
6356
6357fn swap_to_front(child_paths: &mut Vec<PathBuf>, file: &str) {
6358 let position = child_paths
6359 .iter()
6360 .position(|path| path.file_name().unwrap() == file);
6361 if let Some(position) = position {
6362 let temp = child_paths.remove(position);
6363 child_paths.insert(0, temp);
6364 }
6365}
6366
6367fn char_bag_for_path(root_char_bag: CharBag, path: &RelPath) -> CharBag {
6368 let mut result = root_char_bag;
6369 result.extend(path.as_unix_str().chars().map(|c| c.to_ascii_lowercase()));
6370 result
6371}
6372
6373#[derive(Debug)]
6374struct ScanJob {
6375 abs_path: Arc<Path>,
6376 path: Arc<RelPath>,
6377 ignore_stack: IgnoreStack,
6378 scan_queue: Sender<ScanJob>,
6379 ancestor_inodes: TreeSet<u64>,
6380 is_external: bool,
6381}
6382
6383struct UpdateIgnoreStatusJob {
6384 abs_path: Arc<Path>,
6385 ignore_stack: IgnoreStack,
6386 ignore_queue: Sender<UpdateIgnoreStatusJob>,
6387 scan_queue: Sender<ScanJob>,
6388}
6389
6390pub trait WorktreeModelHandle {
6391 #[cfg(feature = "test-support")]
6392 fn flush_fs_events<'a>(
6393 &self,
6394 cx: &'a mut gpui::TestAppContext,
6395 ) -> futures::future::LocalBoxFuture<'a, ()>;
6396
6397 #[cfg(feature = "test-support")]
6398 fn flush_fs_events_in_root_git_repository<'a>(
6399 &self,
6400 cx: &'a mut gpui::TestAppContext,
6401 ) -> futures::future::LocalBoxFuture<'a, ()>;
6402}
6403
6404impl WorktreeModelHandle for Entity<Worktree> {
6405 // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
6406 // occurred before the worktree was constructed. These events can cause the worktree to perform
6407 // extra directory scans, and emit extra scan-state notifications.
6408 //
6409 // This function mutates the worktree's directory and waits for those mutations to be picked up,
6410 // to ensure that all redundant FS events have already been processed.
6411 #[cfg(feature = "test-support")]
6412 fn flush_fs_events<'a>(
6413 &self,
6414 cx: &'a mut gpui::TestAppContext,
6415 ) -> futures::future::LocalBoxFuture<'a, ()> {
6416 let file_name = "fs-event-sentinel";
6417
6418 let tree = self.clone();
6419 let (fs, root_path) = self.read_with(cx, |tree, _| {
6420 let tree = tree.as_local().unwrap();
6421 (tree.fs.clone(), tree.abs_path.clone())
6422 });
6423
6424 async move {
6425 // Subscribe to events BEFORE creating the file to avoid race condition
6426 // where events fire before subscription is set up
6427 let mut events = cx.events(&tree);
6428
6429 fs.create_file(&root_path.join(file_name), Default::default())
6430 .await
6431 .unwrap();
6432
6433 // Check if condition is already met before waiting for events
6434 let file_exists = || {
6435 tree.read_with(cx, |tree, _| {
6436 tree.entry_for_path(RelPath::from_unix_str(file_name).unwrap())
6437 .is_some()
6438 })
6439 };
6440
6441 // Use select to avoid blocking indefinitely if events are delayed
6442 let mut ticks = 0;
6443 while !file_exists() {
6444 futures::select_biased! {
6445 _ = events.next() => {}
6446 _ = futures::FutureExt::fuse(cx.background_executor.timer(std::time::Duration::from_millis(10))) => {
6447 ticks += 1;
6448 if ticks % SENTINEL_RETRY_TICKS == 0 {
6449 retouch_sentinel(fs.as_ref(), &root_path.join(file_name)).await;
6450 }
6451 }
6452 }
6453 }
6454
6455 fs.remove_file(&root_path.join(file_name), Default::default())
6456 .await
6457 .unwrap();
6458
6459 // Check if condition is already met before waiting for events
6460 let file_gone = || {
6461 tree.read_with(cx, |tree, _| {
6462 tree.entry_for_path(RelPath::from_unix_str(file_name).unwrap())
6463 .is_none()
6464 })
6465 };
6466
6467 // Use select to avoid blocking indefinitely if events are delayed
6468 let mut ticks = 0;
6469 while !file_gone() {
6470 futures::select_biased! {
6471 _ = events.next() => {}
6472 _ = futures::FutureExt::fuse(cx.background_executor.timer(std::time::Duration::from_millis(10))) => {
6473 ticks += 1;
6474 if ticks % SENTINEL_RETRY_TICKS == 0 {
6475 retouch_and_remove_sentinel(fs.as_ref(), &root_path.join(file_name)).await;
6476 }
6477 }
6478 }
6479 }
6480
6481 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
6482 .await;
6483 }
6484 .boxed_local()
6485 }
6486
6487 // This function is similar to flush_fs_events, except that it waits for events to be flushed in
6488 // the .git folder of the root repository.
6489 // The reason for its existence is that a repository's .git folder might live *outside* of the
6490 // worktree and thus its FS events might go through a different path.
6491 // In order to flush those, we need to create artificial events in the .git folder and wait
6492 // for the repository to be reloaded.
6493 #[cfg(feature = "test-support")]
6494 fn flush_fs_events_in_root_git_repository<'a>(
6495 &self,
6496 cx: &'a mut gpui::TestAppContext,
6497 ) -> futures::future::LocalBoxFuture<'a, ()> {
6498 let file_name = "fs-event-sentinel";
6499
6500 let tree = self.clone();
6501 let (fs, root_path, mut git_dir_scan_id) = self.read_with(cx, |tree, _| {
6502 let tree = tree.as_local().unwrap();
6503 let local_repo_entry = tree
6504 .git_repositories
6505 .values()
6506 .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone())
6507 .unwrap();
6508 (
6509 tree.fs.clone(),
6510 local_repo_entry.common_dir_abs_path.clone(),
6511 local_repo_entry.git_dir_scan_id,
6512 )
6513 });
6514
6515 let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| {
6516 let tree = tree.as_local().unwrap();
6517 // let repository = tree.repositories.first().unwrap();
6518 let local_repo_entry = tree
6519 .git_repositories
6520 .values()
6521 .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone())
6522 .unwrap();
6523
6524 if local_repo_entry.git_dir_scan_id > *git_dir_scan_id {
6525 *git_dir_scan_id = local_repo_entry.git_dir_scan_id;
6526 true
6527 } else {
6528 false
6529 }
6530 };
6531
6532 async move {
6533 // Subscribe to events BEFORE creating the file to avoid race condition
6534 // where events fire before subscription is set up
6535 let mut events = cx.events(&tree);
6536
6537 fs.create_file(&root_path.join(file_name), Default::default())
6538 .await
6539 .unwrap();
6540
6541 // Use select to avoid blocking indefinitely if events are delayed
6542 let mut ticks = 0;
6543 while !tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
6544 futures::select_biased! {
6545 _ = events.next() => {}
6546 _ = futures::FutureExt::fuse(cx.background_executor.timer(std::time::Duration::from_millis(10))) => {
6547 ticks += 1;
6548 if ticks % SENTINEL_RETRY_TICKS == 0 {
6549 retouch_sentinel(fs.as_ref(), &root_path.join(file_name)).await;
6550 }
6551 }
6552 }
6553 }
6554
6555 fs.remove_file(&root_path.join(file_name), Default::default())
6556 .await
6557 .unwrap();
6558
6559 // Use select to avoid blocking indefinitely if events are delayed
6560 let mut ticks = 0;
6561 while !tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
6562 futures::select_biased! {
6563 _ = events.next() => {}
6564 _ = futures::FutureExt::fuse(cx.background_executor.timer(std::time::Duration::from_millis(10))) => {
6565 ticks += 1;
6566 if ticks % SENTINEL_RETRY_TICKS == 0 {
6567 retouch_and_remove_sentinel(fs.as_ref(), &root_path.join(file_name)).await;
6568 }
6569 }
6570 }
6571 }
6572
6573 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
6574 .await;
6575 }
6576 .boxed_local()
6577 }
6578}
6579
6580#[cfg(feature = "test-support")]
6581const SENTINEL_RETRY_TICKS: usize = 10;
6582
6583// On macOS an FS event can rarely be dropped while notify's shared FSEventStream
6584// is being swapped out by a concurrent watch/unwatch: events already queued for
6585// the old stream's callback are discarded when it is stopped. A dropped sentinel
6586// event would park the test forever, so touch the sentinel again to emit a fresh
6587// event on the current stream.
6588#[cfg(feature = "test-support")]
6589async fn retouch_sentinel(fs: &dyn Fs, abs_path: &std::path::Path) {
6590 fs.create_file(
6591 abs_path,
6592 fs::CreateOptions {
6593 overwrite: true,
6594 ignore_if_exists: false,
6595 },
6596 )
6597 .await
6598 .unwrap();
6599}
6600
6601#[cfg(feature = "test-support")]
6602async fn retouch_and_remove_sentinel(fs: &dyn Fs, abs_path: &std::path::Path) {
6603 retouch_sentinel(fs, abs_path).await;
6604 fs.remove_file(
6605 abs_path,
6606 RemoveOptions {
6607 recursive: false,
6608 ignore_if_not_exists: true,
6609 },
6610 )
6611 .await
6612 .unwrap();
6613}
6614
6615#[derive(Clone, Debug)]
6616struct TraversalProgress<'a> {
6617 max_path: &'a RelPath,
6618 count: usize,
6619 non_ignored_count: usize,
6620 file_count: usize,
6621 non_ignored_file_count: usize,
6622}
6623
6624impl TraversalProgress<'_> {
6625 fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
6626 match (include_files, include_dirs, include_ignored) {
6627 (true, true, true) => self.count,
6628 (true, true, false) => self.non_ignored_count,
6629 (true, false, true) => self.file_count,
6630 (true, false, false) => self.non_ignored_file_count,
6631 (false, true, true) => self.count - self.file_count,
6632 (false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
6633 (false, false, _) => 0,
6634 }
6635 }
6636}
6637
6638impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> {
6639 fn zero(_cx: ()) -> Self {
6640 Default::default()
6641 }
6642
6643 fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) {
6644 self.max_path = summary.max_path.as_ref();
6645 self.count += summary.count;
6646 self.non_ignored_count += summary.non_ignored_count;
6647 self.file_count += summary.file_count;
6648 self.non_ignored_file_count += summary.non_ignored_file_count;
6649 }
6650}
6651
6652impl Default for TraversalProgress<'_> {
6653 fn default() -> Self {
6654 Self {
6655 max_path: RelPath::empty(),
6656 count: 0,
6657 non_ignored_count: 0,
6658 file_count: 0,
6659 non_ignored_file_count: 0,
6660 }
6661 }
6662}
6663
6664#[derive(Debug)]
6665pub struct Traversal<'a> {
6666 snapshot: &'a Snapshot,
6667 cursor: sum_tree::Cursor<'a, 'static, Entry, TraversalProgress<'a>>,
6668 include_ignored: bool,
6669 include_files: bool,
6670 include_dirs: bool,
6671}
6672
6673impl<'a> Traversal<'a> {
6674 fn new(
6675 snapshot: &'a Snapshot,
6676 include_files: bool,
6677 include_dirs: bool,
6678 include_ignored: bool,
6679 start_path: &RelPath,
6680 ) -> Self {
6681 let mut cursor = snapshot.entries_by_path.cursor(());
6682 cursor.seek(&TraversalTarget::path(start_path), Bias::Left);
6683 let mut traversal = Self {
6684 snapshot,
6685 cursor,
6686 include_files,
6687 include_dirs,
6688 include_ignored,
6689 };
6690 if traversal.end_offset() == traversal.start_offset() {
6691 traversal.next();
6692 }
6693 traversal
6694 }
6695
6696 pub fn advance(&mut self) -> bool {
6697 self.advance_by(1)
6698 }
6699
6700 pub fn advance_by(&mut self, count: usize) -> bool {
6701 self.cursor.seek_forward(
6702 &TraversalTarget::Count {
6703 count: self.end_offset() + count,
6704 include_dirs: self.include_dirs,
6705 include_files: self.include_files,
6706 include_ignored: self.include_ignored,
6707 },
6708 Bias::Left,
6709 )
6710 }
6711
6712 pub fn advance_to_sibling(&mut self) -> bool {
6713 while let Some(entry) = self.cursor.item() {
6714 self.cursor
6715 .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left);
6716 if let Some(entry) = self.cursor.item()
6717 && (self.include_files || !entry.is_file())
6718 && (self.include_dirs || !entry.is_dir())
6719 && (self.include_ignored || !entry.is_ignored || entry.is_always_included)
6720 {
6721 return true;
6722 }
6723 }
6724 false
6725 }
6726
6727 pub fn back_to_parent(&mut self) -> bool {
6728 let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else {
6729 return false;
6730 };
6731 self.cursor
6732 .seek(&TraversalTarget::path(parent_path), Bias::Left)
6733 }
6734
6735 pub fn entry(&self) -> Option<&'a Entry> {
6736 self.cursor.item()
6737 }
6738
6739 pub fn snapshot(&self) -> &'a Snapshot {
6740 self.snapshot
6741 }
6742
6743 pub fn start_offset(&self) -> usize {
6744 self.cursor
6745 .start()
6746 .count(self.include_files, self.include_dirs, self.include_ignored)
6747 }
6748
6749 pub fn end_offset(&self) -> usize {
6750 self.cursor
6751 .end()
6752 .count(self.include_files, self.include_dirs, self.include_ignored)
6753 }
6754}
6755
6756impl<'a> Iterator for Traversal<'a> {
6757 type Item = &'a Entry;
6758
6759 fn next(&mut self) -> Option<Self::Item> {
6760 if let Some(item) = self.entry() {
6761 self.advance();
6762 Some(item)
6763 } else {
6764 None
6765 }
6766 }
6767}
6768
6769#[derive(Debug, Clone, Copy)]
6770pub enum PathTarget<'a> {
6771 Path(&'a RelPath),
6772 Successor(&'a RelPath),
6773}
6774
6775impl PathTarget<'_> {
6776 fn cmp_path(&self, other: &RelPath) -> Ordering {
6777 match self {
6778 PathTarget::Path(path) => path.cmp(&other),
6779 PathTarget::Successor(path) => {
6780 if other.starts_with(path) {
6781 Ordering::Greater
6782 } else {
6783 Ordering::Equal
6784 }
6785 }
6786 }
6787 }
6788}
6789
6790impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, PathProgress<'a>> for PathTarget<'_> {
6791 fn cmp(&self, cursor_location: &PathProgress<'a>, _: S::Context<'_>) -> Ordering {
6792 self.cmp_path(cursor_location.max_path)
6793 }
6794}
6795
6796impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> for PathTarget<'_> {
6797 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: S::Context<'_>) -> Ordering {
6798 self.cmp_path(cursor_location.max_path)
6799 }
6800}
6801
6802#[derive(Debug)]
6803enum TraversalTarget<'a> {
6804 Path(PathTarget<'a>),
6805 Count {
6806 count: usize,
6807 include_files: bool,
6808 include_ignored: bool,
6809 include_dirs: bool,
6810 },
6811}
6812
6813impl<'a> TraversalTarget<'a> {
6814 fn path(path: &'a RelPath) -> Self {
6815 Self::Path(PathTarget::Path(path))
6816 }
6817
6818 fn successor(path: &'a RelPath) -> Self {
6819 Self::Path(PathTarget::Successor(path))
6820 }
6821
6822 fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering {
6823 match self {
6824 TraversalTarget::Path(path) => path.cmp_path(progress.max_path),
6825 TraversalTarget::Count {
6826 count,
6827 include_files,
6828 include_dirs,
6829 include_ignored,
6830 } => Ord::cmp(
6831 count,
6832 &progress.count(*include_files, *include_dirs, *include_ignored),
6833 ),
6834 }
6835 }
6836}
6837
6838impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'_> {
6839 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering {
6840 self.cmp_progress(cursor_location)
6841 }
6842}
6843
6844impl<'a> SeekTarget<'a, PathSummary<sum_tree::NoSummary>, TraversalProgress<'a>>
6845 for TraversalTarget<'_>
6846{
6847 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering {
6848 self.cmp_progress(cursor_location)
6849 }
6850}
6851
6852pub struct ChildEntriesOptions {
6853 pub include_files: bool,
6854 pub include_dirs: bool,
6855 pub include_ignored: bool,
6856}
6857
6858pub struct ChildEntriesIter<'a> {
6859 parent_path: &'a RelPath,
6860 traversal: Traversal<'a>,
6861}
6862
6863impl<'a> Iterator for ChildEntriesIter<'a> {
6864 type Item = &'a Entry;
6865
6866 fn next(&mut self) -> Option<Self::Item> {
6867 if let Some(item) = self.traversal.entry()
6868 && item.path.starts_with(self.parent_path)
6869 {
6870 self.traversal.advance_to_sibling();
6871 return Some(item);
6872 }
6873 None
6874 }
6875}
6876
6877impl<'a> From<&'a Entry> for proto::Entry {
6878 fn from(entry: &'a Entry) -> Self {
6879 Self {
6880 id: entry.id.to_proto(),
6881 is_dir: entry.is_dir(),
6882 path: entry.path.as_ref().as_unix_str().to_owned(),
6883 inode: entry.inode,
6884 mtime: entry.mtime.map(|time| time.into()),
6885 is_ignored: entry.is_ignored,
6886 is_hidden: entry.is_hidden,
6887 is_external: entry.is_external,
6888 is_fifo: entry.is_fifo,
6889 size: Some(entry.size),
6890 canonical_path: entry
6891 .canonical_path
6892 .as_ref()
6893 .map(|path| path.to_string_lossy().into_owned()),
6894 }
6895 }
6896}
6897
6898impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry {
6899 type Error = anyhow::Error;
6900
6901 fn try_from(
6902 (root_char_bag, always_included, entry): (&CharBag, &PathMatcher, proto::Entry),
6903 ) -> Result<Self> {
6904 let kind = if entry.is_dir {
6905 EntryKind::Dir
6906 } else {
6907 EntryKind::File
6908 };
6909
6910 let path = RelPath::from_unix_str(&entry.path)
6911 .context("invalid relative path in proto message")?;
6912 let char_bag = char_bag_for_path(*root_char_bag, &path);
6913 let is_always_included = always_included.is_match(&path);
6914 Ok(Entry {
6915 id: ProjectEntryId::from_proto(entry.id),
6916 kind,
6917 path: path.into(),
6918 inode: entry.inode,
6919 mtime: entry.mtime.map(|time| time.into()),
6920 size: entry.size.unwrap_or(0),
6921 canonical_path: entry
6922 .canonical_path
6923 .map(|path_string| Arc::from(PathBuf::from(path_string))),
6924 is_ignored: entry.is_ignored,
6925 is_hidden: entry.is_hidden,
6926 is_always_included,
6927 is_external: entry.is_external,
6928 is_private: false,
6929 char_bag,
6930 is_fifo: entry.is_fifo,
6931 })
6932 }
6933}
6934
6935#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
6936pub struct ProjectEntryId(usize);
6937
6938impl ProjectEntryId {
6939 pub const MAX: Self = Self(usize::MAX);
6940 pub const MIN: Self = Self(usize::MIN);
6941
6942 pub fn new(counter: &AtomicUsize) -> Self {
6943 Self(counter.fetch_add(1, SeqCst))
6944 }
6945
6946 pub fn from_proto(id: u64) -> Self {
6947 Self(id as usize)
6948 }
6949
6950 pub fn to_proto(self) -> u64 {
6951 self.0 as u64
6952 }
6953
6954 pub fn from_usize(id: usize) -> Self {
6955 ProjectEntryId(id)
6956 }
6957
6958 pub fn to_usize(self) -> usize {
6959 self.0
6960 }
6961}
6962
6963#[cfg(feature = "test-support")]
6964impl CreatedEntry {
6965 pub fn into_included(self) -> Option<Entry> {
6966 match self {
6967 CreatedEntry::Included(entry) => Some(entry),
6968 CreatedEntry::Excluded { .. } => None,
6969 }
6970 }
6971}
6972
6973fn parse_gitfile(content: &str) -> anyhow::Result<&Path> {
6974 let path = content
6975 .strip_prefix("gitdir:")
6976 .with_context(|| format!("parsing gitfile content {content:?}"))?;
6977 Ok(Path::new(path.trim()))
6978}
6979
6980fn resolve_gitfile_path(dot_git_abs_path: &Path, gitfile_path: &Path) -> PathBuf {
6981 if gitfile_path.is_absolute() {
6982 gitfile_path.into()
6983 } else {
6984 dot_git_abs_path
6985 .parent()
6986 .unwrap_or_else(|| Path::new(""))
6987 .join(gitfile_path)
6988 }
6989}
6990
6991fn resolve_commondir_path(repository_dir_abs_path: &Path, commondir_path: &str) -> PathBuf {
6992 let commondir_path = Path::new(commondir_path.trim());
6993 if commondir_path.is_absolute() {
6994 commondir_path.into()
6995 } else {
6996 repository_dir_abs_path.join(commondir_path)
6997 }
6998}
6999
7000pub async fn discover_root_repo_common_dir(root_abs_path: &Path, fs: &dyn Fs) -> Option<Arc<Path>> {
7001 discover_root_repo_metadata(root_abs_path, fs)
7002 .await
7003 .map(|(common_dir, _)| common_dir)
7004}
7005
7006async fn discover_root_repo_metadata(
7007 root_abs_path: &Path,
7008 fs: &dyn Fs,
7009) -> Option<(Arc<Path>, bool)> {
7010 let root_dot_git = root_abs_path.join(DOT_GIT);
7011 if !fs.metadata(&root_dot_git).await.is_ok_and(|m| m.is_some()) {
7012 return None;
7013 }
7014 let dot_git_path: Arc<Path> = root_dot_git.into();
7015 let (repository_dir, common_dir) = discover_git_paths(&dot_git_path, fs).await;
7016 let is_linked_worktree = repository_dir != common_dir;
7017 Some((common_dir, is_linked_worktree))
7018}
7019
7020async fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, Arc<Path>) {
7021 let mut repository_dir_abs_path = dot_git_abs_path.clone();
7022 let mut common_dir_abs_path = dot_git_abs_path.clone();
7023
7024 if let Some(path) = fs
7025 .load(dot_git_abs_path)
7026 .await
7027 .ok()
7028 .as_ref()
7029 .and_then(|contents| parse_gitfile(contents).log_err())
7030 {
7031 let path = resolve_gitfile_path(dot_git_abs_path, path);
7032 if let Some(path) = fs.canonicalize(&path).await.log_err() {
7033 repository_dir_abs_path = Path::new(&path).into();
7034 common_dir_abs_path = repository_dir_abs_path.clone();
7035
7036 if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok()
7037 && let Some(commondir_path) = fs
7038 .canonicalize(&resolve_commondir_path(&path, &commondir_contents))
7039 .await
7040 .log_err()
7041 {
7042 common_dir_abs_path = commondir_path.as_path().into();
7043 }
7044 }
7045 };
7046 (repository_dir_abs_path, common_dir_abs_path)
7047}
7048
7049struct NullWatcher;
7050
7051impl fs::Watcher for NullWatcher {
7052 fn add(&self, _path: &Path) -> Result<()> {
7053 Ok(())
7054 }
7055
7056 fn remove(&self, _path: &Path) -> Result<()> {
7057 Ok(())
7058 }
7059}
7060
7061async fn decode_file_text(
7062 fs: &dyn Fs,
7063 abs_path: &Path,
7064) -> Result<(String, &'static Encoding, bool)> {
7065 let mut file = fs
7066 .open_sync(&abs_path)
7067 .await
7068 .with_context(|| format!("opening file {abs_path:?}"))?;
7069
7070 // First, read the beginning of the file to determine its kind and encoding.
7071 // We do not want to load an entire large blob into memory only to discard it.
7072 let mut file_first_bytes = Vec::with_capacity(FILE_ANALYSIS_BYTES);
7073 let mut buf = [0u8; FILE_ANALYSIS_BYTES];
7074 let mut reached_eof = false;
7075 loop {
7076 if file_first_bytes.len() >= FILE_ANALYSIS_BYTES {
7077 break;
7078 }
7079 let n = file
7080 .read(&mut buf)
7081 .with_context(|| format!("reading bytes of the file {abs_path:?}"))?;
7082 if n == 0 {
7083 reached_eof = true;
7084 break;
7085 }
7086 file_first_bytes.extend_from_slice(&buf[..n]);
7087 }
7088 let (bom_encoding, byte_content) = decode_byte_header(&file_first_bytes);
7089 anyhow::ensure!(
7090 byte_content != ByteContent::Binary,
7091 "Binary files are not supported"
7092 );
7093
7094 // If the file is eligible for opening, read the rest of the file.
7095 let mut content = file_first_bytes;
7096 if !reached_eof {
7097 let mut buf = [0u8; 8 * 1024];
7098 loop {
7099 let n = file
7100 .read(&mut buf)
7101 .with_context(|| format!("reading remaining bytes of the file {abs_path:?}"))?;
7102 if n == 0 {
7103 break;
7104 }
7105 content.extend_from_slice(&buf[..n]);
7106 }
7107 }
7108 decode_byte_full(content, bom_encoding, byte_content)
7109}
7110
7111fn decode_byte_header(prefix: &[u8]) -> (Option<&'static Encoding>, ByteContent) {
7112 if let Some((encoding, _bom_len)) = Encoding::for_bom(prefix) {
7113 return (Some(encoding), ByteContent::Unknown);
7114 }
7115 (None, analyze_byte_content(prefix))
7116}
7117
7118fn decode_byte_full(
7119 bytes: Vec<u8>,
7120 bom_encoding: Option<&'static Encoding>,
7121 byte_content: ByteContent,
7122) -> Result<(String, &'static Encoding, bool)> {
7123 if let Some(encoding) = bom_encoding {
7124 let (cow, _) = encoding.decode_with_bom_removal(&bytes);
7125 return Ok((cow.into_owned(), encoding, true));
7126 }
7127
7128 match byte_content {
7129 ByteContent::Utf16Le => {
7130 let encoding = encoding_rs::UTF_16LE;
7131 let (cow, _, _) = encoding.decode(&bytes);
7132 return Ok((cow.into_owned(), encoding, false));
7133 }
7134 ByteContent::Utf16Be => {
7135 let encoding = encoding_rs::UTF_16BE;
7136 let (cow, _, _) = encoding.decode(&bytes);
7137 return Ok((cow.into_owned(), encoding, false));
7138 }
7139 ByteContent::Binary => {
7140 anyhow::bail!("Binary files are not supported");
7141 }
7142 ByteContent::Unknown => {}
7143 }
7144
7145 fn detect_encoding(bytes: Vec<u8>) -> (String, &'static Encoding) {
7146 let mut detector = EncodingDetector::new();
7147 detector.feed(&bytes, true);
7148
7149 let encoding = detector.guess(None, true); // Use None for TLD hint to ensure neutral detection logic.
7150
7151 let (cow, _, _) = encoding.decode(&bytes);
7152 (cow.into_owned(), encoding)
7153 }
7154
7155 match String::from_utf8(bytes) {
7156 Ok(text) => {
7157 // ISO-2022-JP (and other ISO-2022 variants) consists entirely of 7-bit ASCII bytes,
7158 // so it is valid UTF-8. However, it contains escape sequences starting with '\x1b'.
7159 // If we find an escape character, we double-check the encoding to prevent
7160 // displaying raw escape sequences instead of the correct characters.
7161 if text.contains('\x1b') {
7162 let (s, enc) = detect_encoding(text.into_bytes());
7163 Ok((s, enc, false))
7164 } else {
7165 Ok((text, encoding_rs::UTF_8, false))
7166 }
7167 }
7168 Err(e) => {
7169 let (s, enc) = detect_encoding(e.into_bytes());
7170 Ok((s, enc, false))
7171 }
7172 }
7173}
7174
7175#[cfg(test)]
7176mod tests {
7177 use super::*;
7178
7179 /// reproduction of issue #50785
7180 fn build_pcm16_wav_bytes() -> Vec<u8> {
7181 let header: Vec<u8> = vec![
7182 /* RIFF header */
7183 0x52, 0x49, 0x46, 0x46, // "RIFF"
7184 0xc6, 0xcf, 0x00, 0x00, // file size: 8
7185 0x57, 0x41, 0x56, 0x45, // "WAVE"
7186 /* fmt chunk */
7187 0x66, 0x6d, 0x74, 0x20, // "fmt "
7188 0x10, 0x00, 0x00, 0x00, // chunk size: 16
7189 0x01, 0x00, // format: PCM (1)
7190 0x01, 0x00, // channels: 1 (mono)
7191 0x80, 0x3e, 0x00, 0x00, // sample rate: 16000
7192 0x00, 0x7d, 0x00, 0x00, // byte rate: 32000
7193 0x02, 0x00, // block align: 2
7194 0x10, 0x00, // bits per sample: 16
7195 /* LIST chunk */
7196 0x4c, 0x49, 0x53, 0x54, // "LIST"
7197 0x1a, 0x00, 0x00, 0x00, // chunk size: 26
7198 0x49, 0x4e, 0x46, 0x4f, // "INFO"
7199 0x49, 0x53, 0x46, 0x54, // "ISFT"
7200 0x0d, 0x00, 0x00, 0x00, // sub-chunk size: 13
7201 0x4c, 0x61, 0x76, 0x66, 0x36, 0x32, 0x2e, 0x33, // "Lavf62.3"
7202 0x2e, 0x31, 0x30, 0x30, 0x00, // ".100\0"
7203 /* padding byte for word alignment */
7204 0x00, // data chunk header
7205 0x64, 0x61, 0x74, 0x61, // "data"
7206 0x80, 0xcf, 0x00, 0x00, // chunk size
7207 ];
7208
7209 let mut bytes = header;
7210
7211 // fill remaining space up to `FILE_ANALYSIS_BYTES` with synthetic PCM
7212 let audio_bytes_needed = FILE_ANALYSIS_BYTES - bytes.len();
7213 for i in 0..(audio_bytes_needed / 2) {
7214 let sample = (i & 0xFF) as u8;
7215 bytes.push(sample); // low byte: varies
7216 bytes.push(0x00); // high byte: zero for small values
7217 }
7218
7219 bytes
7220 }
7221
7222 #[test]
7223 fn test_pcm16_wav_detected_as_binary() {
7224 let wav_bytes = build_pcm16_wav_bytes();
7225 assert_eq!(wav_bytes.len(), FILE_ANALYSIS_BYTES);
7226
7227 let result = analyze_byte_content(&wav_bytes);
7228 assert_eq!(
7229 result,
7230 ByteContent::Binary,
7231 "PCM 16-bit WAV should be detected as Binary via RIFF header"
7232 );
7233 }
7234
7235 #[test]
7236 fn test_le16_binary_not_misdetected_as_utf16le() {
7237 let mut bytes = b"FAKE".to_vec();
7238 while bytes.len() < FILE_ANALYSIS_BYTES {
7239 let sample = (bytes.len() & 0xFF) as u8;
7240 bytes.push(sample);
7241 bytes.push(0x00);
7242 }
7243 bytes.truncate(FILE_ANALYSIS_BYTES);
7244
7245 let result = analyze_byte_content(&bytes);
7246 assert_eq!(
7247 result,
7248 ByteContent::Binary,
7249 "LE 16-bit binary with control characters should be detected as Binary"
7250 );
7251 }
7252
7253 #[test]
7254 fn test_be16_binary_not_misdetected_as_utf16be() {
7255 let mut bytes = b"FAKE".to_vec();
7256 while bytes.len() < FILE_ANALYSIS_BYTES {
7257 bytes.push(0x00);
7258 let sample = (bytes.len() & 0xFF) as u8;
7259 bytes.push(sample);
7260 }
7261 bytes.truncate(FILE_ANALYSIS_BYTES);
7262
7263 let result = analyze_byte_content(&bytes);
7264 assert_eq!(
7265 result,
7266 ByteContent::Binary,
7267 "BE 16-bit binary with control characters should be detected as Binary"
7268 );
7269 }
7270
7271 #[test]
7272 fn test_utf16le_text_detected_as_utf16le() {
7273 let text = "Hello, world! This is a UTF-16 test string. ";
7274 let mut bytes = Vec::new();
7275 while bytes.len() < FILE_ANALYSIS_BYTES {
7276 bytes.extend(text.encode_utf16().flat_map(|u| u.to_le_bytes()));
7277 }
7278 bytes.truncate(FILE_ANALYSIS_BYTES);
7279
7280 assert_eq!(analyze_byte_content(&bytes), ByteContent::Utf16Le);
7281 }
7282
7283 #[test]
7284 fn test_utf16be_text_detected_as_utf16be() {
7285 let text = "Hello, world! This is a UTF-16 test string. ";
7286 let mut bytes = Vec::new();
7287 while bytes.len() < FILE_ANALYSIS_BYTES {
7288 bytes.extend(text.encode_utf16().flat_map(|u| u.to_be_bytes()));
7289 }
7290 bytes.truncate(FILE_ANALYSIS_BYTES);
7291
7292 assert_eq!(analyze_byte_content(&bytes), ByteContent::Utf16Be);
7293 }
7294
7295 #[test]
7296 fn test_known_binary_headers() {
7297 let cases: &[(&[u8], &str)] = &[
7298 (b"RIFF\x00\x00\x00\x00WAVE", "WAV"),
7299 (b"RIFF\x00\x00\x00\x00AVI ", "AVI"),
7300 (b"OggS\x00\x02", "OGG"),
7301 (b"fLaC\x00\x00", "FLAC"),
7302 (b"ID3\x03\x00", "MP3 ID3v2"),
7303 (b"\xFF\xFB\x90\x00", "MP3 MPEG1 Layer3"),
7304 (b"\xFF\xF3\x90\x00", "MP3 MPEG2 Layer3"),
7305 ];
7306
7307 for (header, label) in cases {
7308 let mut bytes = header.to_vec();
7309 bytes.resize(FILE_ANALYSIS_BYTES, 0x41); // pad with 'A'
7310 assert_eq!(
7311 analyze_byte_content(&bytes),
7312 ByteContent::Binary,
7313 "{label} should be detected as Binary"
7314 );
7315 }
7316 }
7317}
7318