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