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