1use std::{
2 path::{Path, PathBuf},
3 pin::pin,
4 sync::{atomic::AtomicUsize, Arc},
5};
6
7use anyhow::{anyhow, Context as _, Result};
8use collections::{HashMap, HashSet};
9use fs::Fs;
10use futures::{
11 future::{BoxFuture, Shared},
12 FutureExt, SinkExt,
13};
14use gpui::{
15 AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, Task, WeakModel,
16};
17use postage::oneshot;
18use rpc::{
19 proto::{self, SSH_PROJECT_ID},
20 AnyProtoClient, ErrorExt, TypedEnvelope,
21};
22use smol::{
23 channel::{Receiver, Sender},
24 stream::StreamExt,
25};
26use text::ReplicaId;
27use util::{paths::SanitizedPath, ResultExt};
28use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
29
30use crate::{search::SearchQuery, ProjectPath};
31
32struct MatchingEntry {
33 worktree_path: Arc<Path>,
34 path: ProjectPath,
35 respond: oneshot::Sender<ProjectPath>,
36}
37
38enum WorktreeStoreState {
39 Local {
40 fs: Arc<dyn Fs>,
41 },
42 Remote {
43 upstream_client: AnyProtoClient,
44 upstream_project_id: u64,
45 },
46}
47
48pub struct WorktreeStore {
49 next_entry_id: Arc<AtomicUsize>,
50 downstream_client: Option<(AnyProtoClient, u64)>,
51 retain_worktrees: bool,
52 worktrees: Vec<WorktreeHandle>,
53 worktrees_reordered: bool,
54 #[allow(clippy::type_complexity)]
55 loading_worktrees:
56 HashMap<SanitizedPath, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
57 state: WorktreeStoreState,
58}
59
60pub enum WorktreeStoreEvent {
61 WorktreeAdded(Model<Worktree>),
62 WorktreeRemoved(EntityId, WorktreeId),
63 WorktreeReleased(EntityId, WorktreeId),
64 WorktreeOrderChanged,
65 WorktreeUpdateSent(Model<Worktree>),
66 WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
67 WorktreeUpdatedGitRepositories(WorktreeId),
68 WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
69}
70
71impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
72
73impl WorktreeStore {
74 pub fn init(client: &AnyProtoClient) {
75 client.add_model_request_handler(Self::handle_create_project_entry);
76 client.add_model_request_handler(Self::handle_copy_project_entry);
77 client.add_model_request_handler(Self::handle_delete_project_entry);
78 client.add_model_request_handler(Self::handle_expand_project_entry);
79 client.add_model_request_handler(Self::handle_git_branches);
80 client.add_model_request_handler(Self::handle_update_branch);
81 }
82
83 pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
84 Self {
85 next_entry_id: Default::default(),
86 loading_worktrees: Default::default(),
87 downstream_client: None,
88 worktrees: Vec::new(),
89 worktrees_reordered: false,
90 retain_worktrees,
91 state: WorktreeStoreState::Local { fs },
92 }
93 }
94
95 pub fn remote(
96 retain_worktrees: bool,
97 upstream_client: AnyProtoClient,
98 upstream_project_id: u64,
99 ) -> Self {
100 Self {
101 next_entry_id: Default::default(),
102 loading_worktrees: Default::default(),
103 downstream_client: None,
104 worktrees: Vec::new(),
105 worktrees_reordered: false,
106 retain_worktrees,
107 state: WorktreeStoreState::Remote {
108 upstream_client,
109 upstream_project_id,
110 },
111 }
112 }
113
114 /// Iterates through all worktrees, including ones that don't appear in the project panel
115 pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Model<Worktree>> {
116 self.worktrees
117 .iter()
118 .filter_map(move |worktree| worktree.upgrade())
119 }
120
121 /// Iterates through all user-visible worktrees, the ones that appear in the project panel.
122 pub fn visible_worktrees<'a>(
123 &'a self,
124 cx: &'a AppContext,
125 ) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
126 self.worktrees()
127 .filter(|worktree| worktree.read(cx).is_visible())
128 }
129
130 pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
131 self.worktrees()
132 .find(|worktree| worktree.read(cx).id() == id)
133 }
134
135 pub fn current_branch(&self, repository: ProjectPath, cx: &AppContext) -> Option<Arc<str>> {
136 self.worktree_for_id(repository.worktree_id, cx)?
137 .read(cx)
138 .git_entry(repository.path)?
139 .branch()
140 }
141
142 pub fn worktree_for_entry(
143 &self,
144 entry_id: ProjectEntryId,
145 cx: &AppContext,
146 ) -> Option<Model<Worktree>> {
147 self.worktrees()
148 .find(|worktree| worktree.read(cx).contains_entry(entry_id))
149 }
150
151 pub fn find_worktree(
152 &self,
153 abs_path: impl Into<SanitizedPath>,
154 cx: &AppContext,
155 ) -> Option<(Model<Worktree>, PathBuf)> {
156 let abs_path: SanitizedPath = abs_path.into();
157 for tree in self.worktrees() {
158 if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {
159 return Some((tree.clone(), relative_path.into()));
160 }
161 }
162 None
163 }
164
165 pub fn find_or_create_worktree(
166 &mut self,
167 abs_path: impl AsRef<Path>,
168 visible: bool,
169 cx: &mut ModelContext<Self>,
170 ) -> Task<Result<(Model<Worktree>, PathBuf)>> {
171 let abs_path = abs_path.as_ref();
172 if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
173 Task::ready(Ok((tree, relative_path)))
174 } else {
175 let worktree = self.create_worktree(abs_path, visible, cx);
176 cx.background_executor()
177 .spawn(async move { Ok((worktree.await?, PathBuf::new())) })
178 }
179 }
180
181 pub fn entry_for_id<'a>(
182 &'a self,
183 entry_id: ProjectEntryId,
184 cx: &'a AppContext,
185 ) -> Option<&'a Entry> {
186 self.worktrees()
187 .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))
188 }
189
190 pub fn worktree_and_entry_for_id<'a>(
191 &'a self,
192 entry_id: ProjectEntryId,
193 cx: &'a AppContext,
194 ) -> Option<(Model<Worktree>, &'a Entry)> {
195 self.worktrees().find_map(|worktree| {
196 worktree
197 .read(cx)
198 .entry_for_id(entry_id)
199 .map(|e| (worktree.clone(), e))
200 })
201 }
202
203 pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
204 self.worktree_for_id(path.worktree_id, cx)?
205 .read(cx)
206 .entry_for_path(&path.path)
207 .cloned()
208 }
209
210 pub fn create_worktree(
211 &mut self,
212 abs_path: impl Into<SanitizedPath>,
213 visible: bool,
214 cx: &mut ModelContext<Self>,
215 ) -> Task<Result<Model<Worktree>>> {
216 let abs_path: SanitizedPath = abs_path.into();
217 if !self.loading_worktrees.contains_key(&abs_path) {
218 let task = match &self.state {
219 WorktreeStoreState::Remote {
220 upstream_client, ..
221 } => {
222 if upstream_client.is_via_collab() {
223 Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
224 } else {
225 self.create_ssh_worktree(
226 upstream_client.clone(),
227 abs_path.clone(),
228 visible,
229 cx,
230 )
231 }
232 }
233 WorktreeStoreState::Local { fs } => {
234 self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx)
235 }
236 };
237
238 self.loading_worktrees
239 .insert(abs_path.clone(), task.shared());
240 }
241 let task = self.loading_worktrees.get(&abs_path).unwrap().clone();
242 cx.spawn(|this, mut cx| async move {
243 let result = task.await;
244 this.update(&mut cx, |this, _| this.loading_worktrees.remove(&abs_path))
245 .ok();
246 match result {
247 Ok(worktree) => Ok(worktree),
248 Err(err) => Err((*err).cloned()),
249 }
250 })
251 }
252
253 fn create_ssh_worktree(
254 &mut self,
255 client: AnyProtoClient,
256 abs_path: impl Into<SanitizedPath>,
257 visible: bool,
258 cx: &mut ModelContext<Self>,
259 ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
260 let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
261 // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
262 // in which case want to strip the leading the `/`.
263 // On the host-side, the `~` will get expanded.
264 // That's what git does too: https://github.com/libgit2/libgit2/issues/3345#issuecomment-127050850
265 if abs_path.starts_with("/~") {
266 abs_path = abs_path[1..].to_string();
267 }
268 if abs_path.is_empty() || abs_path == "/" {
269 abs_path = "~/".to_string();
270 }
271 cx.spawn(|this, mut cx| async move {
272 let this = this.upgrade().context("Dropped worktree store")?;
273
274 let response = client
275 .request(proto::AddWorktree {
276 project_id: SSH_PROJECT_ID,
277 path: abs_path.clone(),
278 visible,
279 })
280 .await?;
281
282 if let Some(existing_worktree) = this.read_with(&cx, |this, cx| {
283 this.worktree_for_id(WorktreeId::from_proto(response.worktree_id), cx)
284 })? {
285 return Ok(existing_worktree);
286 }
287
288 let root_name = PathBuf::from(&response.canonicalized_path)
289 .file_name()
290 .map(|n| n.to_string_lossy().to_string())
291 .unwrap_or(response.canonicalized_path.to_string());
292
293 let worktree = cx.update(|cx| {
294 Worktree::remote(
295 SSH_PROJECT_ID,
296 0,
297 proto::WorktreeMetadata {
298 id: response.worktree_id,
299 root_name,
300 visible,
301 abs_path: response.canonicalized_path,
302 },
303 client,
304 cx,
305 )
306 })?;
307
308 this.update(&mut cx, |this, cx| {
309 this.add(&worktree, cx);
310 })?;
311 Ok(worktree)
312 })
313 }
314
315 fn create_local_worktree(
316 &mut self,
317 fs: Arc<dyn Fs>,
318 abs_path: impl Into<SanitizedPath>,
319 visible: bool,
320 cx: &mut ModelContext<Self>,
321 ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
322 let next_entry_id = self.next_entry_id.clone();
323 let path: SanitizedPath = abs_path.into();
324
325 cx.spawn(move |this, mut cx| async move {
326 let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
327
328 let worktree = worktree?;
329
330 this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
331
332 if visible {
333 cx.update(|cx| {
334 cx.add_recent_document(path.as_path());
335 })
336 .log_err();
337 }
338
339 Ok(worktree)
340 })
341 }
342
343 pub fn add(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
344 let worktree_id = worktree.read(cx).id();
345 debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id));
346
347 let push_strong_handle = self.retain_worktrees || worktree.read(cx).is_visible();
348 let handle = if push_strong_handle {
349 WorktreeHandle::Strong(worktree.clone())
350 } else {
351 WorktreeHandle::Weak(worktree.downgrade())
352 };
353 if self.worktrees_reordered {
354 self.worktrees.push(handle);
355 } else {
356 let i = match self
357 .worktrees
358 .binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {
359 other.upgrade().map(|worktree| worktree.read(cx).abs_path())
360 }) {
361 Ok(i) | Err(i) => i,
362 };
363 self.worktrees.insert(i, handle);
364 }
365
366 cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));
367 self.send_project_updates(cx);
368
369 let handle_id = worktree.entity_id();
370 cx.subscribe(worktree, |_, worktree, event, cx| {
371 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
372 match event {
373 worktree::Event::UpdatedEntries(changes) => {
374 cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(
375 worktree.read(cx).id(),
376 changes.clone(),
377 ));
378 }
379 worktree::Event::UpdatedGitRepositories(_) => {
380 cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
381 worktree_id,
382 ));
383 }
384 worktree::Event::DeletedEntry(id) => {
385 cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id))
386 }
387 }
388 })
389 .detach();
390 cx.observe_release(worktree, move |this, worktree, cx| {
391 cx.emit(WorktreeStoreEvent::WorktreeReleased(
392 handle_id,
393 worktree.id(),
394 ));
395 cx.emit(WorktreeStoreEvent::WorktreeRemoved(
396 handle_id,
397 worktree.id(),
398 ));
399 this.send_project_updates(cx);
400 })
401 .detach();
402 }
403
404 pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
405 self.worktrees.retain(|worktree| {
406 if let Some(worktree) = worktree.upgrade() {
407 if worktree.read(cx).id() == id_to_remove {
408 cx.emit(WorktreeStoreEvent::WorktreeRemoved(
409 worktree.entity_id(),
410 id_to_remove,
411 ));
412 false
413 } else {
414 true
415 }
416 } else {
417 false
418 }
419 });
420 self.send_project_updates(cx);
421 }
422
423 pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
424 self.worktrees_reordered = worktrees_reordered;
425 }
426
427 fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
428 match &self.state {
429 WorktreeStoreState::Remote {
430 upstream_client,
431 upstream_project_id,
432 ..
433 } => Some((upstream_client.clone(), *upstream_project_id)),
434 WorktreeStoreState::Local { .. } => None,
435 }
436 }
437
438 pub fn set_worktrees_from_proto(
439 &mut self,
440 worktrees: Vec<proto::WorktreeMetadata>,
441 replica_id: ReplicaId,
442 cx: &mut ModelContext<Self>,
443 ) -> Result<()> {
444 let mut old_worktrees_by_id = self
445 .worktrees
446 .drain(..)
447 .filter_map(|worktree| {
448 let worktree = worktree.upgrade()?;
449 Some((worktree.read(cx).id(), worktree))
450 })
451 .collect::<HashMap<_, _>>();
452
453 let (client, project_id) = self
454 .upstream_client()
455 .clone()
456 .ok_or_else(|| anyhow!("invalid project"))?;
457
458 for worktree in worktrees {
459 if let Some(old_worktree) =
460 old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
461 {
462 let push_strong_handle =
463 self.retain_worktrees || old_worktree.read(cx).is_visible();
464 let handle = if push_strong_handle {
465 WorktreeHandle::Strong(old_worktree.clone())
466 } else {
467 WorktreeHandle::Weak(old_worktree.downgrade())
468 };
469 self.worktrees.push(handle);
470 } else {
471 self.add(
472 &Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),
473 cx,
474 );
475 }
476 }
477 self.send_project_updates(cx);
478
479 Ok(())
480 }
481
482 pub fn move_worktree(
483 &mut self,
484 source: WorktreeId,
485 destination: WorktreeId,
486 cx: &mut ModelContext<Self>,
487 ) -> Result<()> {
488 if source == destination {
489 return Ok(());
490 }
491
492 let mut source_index = None;
493 let mut destination_index = None;
494 for (i, worktree) in self.worktrees.iter().enumerate() {
495 if let Some(worktree) = worktree.upgrade() {
496 let worktree_id = worktree.read(cx).id();
497 if worktree_id == source {
498 source_index = Some(i);
499 if destination_index.is_some() {
500 break;
501 }
502 } else if worktree_id == destination {
503 destination_index = Some(i);
504 if source_index.is_some() {
505 break;
506 }
507 }
508 }
509 }
510
511 let source_index =
512 source_index.with_context(|| format!("Missing worktree for id {source}"))?;
513 let destination_index =
514 destination_index.with_context(|| format!("Missing worktree for id {destination}"))?;
515
516 if source_index == destination_index {
517 return Ok(());
518 }
519
520 let worktree_to_move = self.worktrees.remove(source_index);
521 self.worktrees.insert(destination_index, worktree_to_move);
522 self.worktrees_reordered = true;
523 cx.emit(WorktreeStoreEvent::WorktreeOrderChanged);
524 cx.notify();
525 Ok(())
526 }
527
528 pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
529 for worktree in &self.worktrees {
530 if let Some(worktree) = worktree.upgrade() {
531 worktree.update(cx, |worktree, _| {
532 if let Some(worktree) = worktree.as_remote_mut() {
533 worktree.disconnected_from_host();
534 }
535 });
536 }
537 }
538 }
539
540 pub fn send_project_updates(&mut self, cx: &mut ModelContext<Self>) {
541 let Some((downstream_client, project_id)) = self.downstream_client.clone() else {
542 return;
543 };
544
545 let update = proto::UpdateProject {
546 project_id,
547 worktrees: self.worktree_metadata_protos(cx),
548 };
549
550 // collab has bad concurrency guarantees, so we send requests in serial.
551 let update_project = if downstream_client.is_via_collab() {
552 Some(downstream_client.request(update))
553 } else {
554 downstream_client.send(update).log_err();
555 None
556 };
557 cx.spawn(|this, mut cx| async move {
558 if let Some(update_project) = update_project {
559 update_project.await?;
560 }
561
562 this.update(&mut cx, |this, cx| {
563 let worktrees = this.worktrees().collect::<Vec<_>>();
564
565 for worktree in worktrees {
566 worktree.update(cx, |worktree, cx| {
567 let client = downstream_client.clone();
568 worktree.observe_updates(project_id, cx, {
569 move |update| {
570 let client = client.clone();
571 async move {
572 if client.is_via_collab() {
573 client
574 .request(update)
575 .map(|result| result.log_err().is_some())
576 .await
577 } else {
578 client.send(update).log_err().is_some()
579 }
580 }
581 }
582 });
583 });
584
585 cx.emit(WorktreeStoreEvent::WorktreeUpdateSent(worktree.clone()))
586 }
587
588 anyhow::Ok(())
589 })
590 })
591 .detach_and_log_err(cx);
592 }
593
594 pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec<proto::WorktreeMetadata> {
595 self.worktrees()
596 .map(|worktree| {
597 let worktree = worktree.read(cx);
598 proto::WorktreeMetadata {
599 id: worktree.id().to_proto(),
600 root_name: worktree.root_name().into(),
601 visible: worktree.is_visible(),
602 abs_path: worktree.abs_path().to_string_lossy().into(),
603 }
604 })
605 .collect()
606 }
607
608 pub fn shared(
609 &mut self,
610 remote_id: u64,
611 downstream_client: AnyProtoClient,
612 cx: &mut ModelContext<Self>,
613 ) {
614 self.retain_worktrees = true;
615 self.downstream_client = Some((downstream_client, remote_id));
616
617 // When shared, retain all worktrees
618 for worktree_handle in self.worktrees.iter_mut() {
619 match worktree_handle {
620 WorktreeHandle::Strong(_) => {}
621 WorktreeHandle::Weak(worktree) => {
622 if let Some(worktree) = worktree.upgrade() {
623 *worktree_handle = WorktreeHandle::Strong(worktree);
624 }
625 }
626 }
627 }
628 self.send_project_updates(cx);
629 }
630
631 pub fn unshared(&mut self, cx: &mut ModelContext<Self>) {
632 self.retain_worktrees = false;
633 self.downstream_client.take();
634
635 // When not shared, only retain the visible worktrees
636 for worktree_handle in self.worktrees.iter_mut() {
637 if let WorktreeHandle::Strong(worktree) = worktree_handle {
638 let is_visible = worktree.update(cx, |worktree, _| {
639 worktree.stop_observing_updates();
640 worktree.is_visible()
641 });
642 if !is_visible {
643 *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
644 }
645 }
646 }
647 }
648
649 /// search over all worktrees and return buffers that *might* match the search.
650 pub fn find_search_candidates(
651 &self,
652 query: SearchQuery,
653 limit: usize,
654 open_entries: HashSet<ProjectEntryId>,
655 fs: Arc<dyn Fs>,
656 cx: &ModelContext<Self>,
657 ) -> Receiver<ProjectPath> {
658 let snapshots = self
659 .visible_worktrees(cx)
660 .filter_map(|tree| {
661 let tree = tree.read(cx);
662 Some((tree.snapshot(), tree.as_local()?.settings()))
663 })
664 .collect::<Vec<_>>();
665
666 let executor = cx.background_executor().clone();
667
668 // We want to return entries in the order they are in the worktrees, so we have one
669 // thread that iterates over the worktrees (and ignored directories) as necessary,
670 // and pushes a oneshot::Receiver to the output channel and a oneshot::Sender to the filter
671 // channel.
672 // We spawn a number of workers that take items from the filter channel and check the query
673 // against the version of the file on disk.
674 let (filter_tx, filter_rx) = smol::channel::bounded(64);
675 let (output_tx, output_rx) = smol::channel::bounded(64);
676 let (matching_paths_tx, matching_paths_rx) = smol::channel::unbounded();
677
678 let input = cx.background_executor().spawn({
679 let fs = fs.clone();
680 let query = query.clone();
681 async move {
682 Self::find_candidate_paths(
683 fs,
684 snapshots,
685 open_entries,
686 query,
687 filter_tx,
688 output_tx,
689 )
690 .await
691 .log_err();
692 }
693 });
694 const MAX_CONCURRENT_FILE_SCANS: usize = 64;
695 let filters = cx.background_executor().spawn(async move {
696 let fs = &fs;
697 let query = &query;
698 executor
699 .scoped(move |scope| {
700 for _ in 0..MAX_CONCURRENT_FILE_SCANS {
701 let filter_rx = filter_rx.clone();
702 scope.spawn(async move {
703 Self::filter_paths(fs, filter_rx, query).await.log_err();
704 })
705 }
706 })
707 .await;
708 });
709 cx.background_executor()
710 .spawn(async move {
711 let mut matched = 0;
712 while let Ok(mut receiver) = output_rx.recv().await {
713 let Some(path) = receiver.next().await else {
714 continue;
715 };
716 let Ok(_) = matching_paths_tx.send(path).await else {
717 break;
718 };
719 matched += 1;
720 if matched == limit {
721 break;
722 }
723 }
724 drop(input);
725 drop(filters);
726 })
727 .detach();
728 matching_paths_rx
729 }
730
731 fn scan_ignored_dir<'a>(
732 fs: &'a Arc<dyn Fs>,
733 snapshot: &'a worktree::Snapshot,
734 path: &'a Path,
735 query: &'a SearchQuery,
736 include_root: bool,
737 filter_tx: &'a Sender<MatchingEntry>,
738 output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
739 ) -> BoxFuture<'a, Result<()>> {
740 async move {
741 let abs_path = snapshot.abs_path().join(path);
742 let Some(mut files) = fs
743 .read_dir(&abs_path)
744 .await
745 .with_context(|| format!("listing ignored path {abs_path:?}"))
746 .log_err()
747 else {
748 return Ok(());
749 };
750
751 let mut results = Vec::new();
752
753 while let Some(Ok(file)) = files.next().await {
754 let Some(metadata) = fs
755 .metadata(&file)
756 .await
757 .with_context(|| format!("fetching fs metadata for {abs_path:?}"))
758 .log_err()
759 .flatten()
760 else {
761 continue;
762 };
763 if metadata.is_symlink || metadata.is_fifo {
764 continue;
765 }
766 results.push((
767 file.strip_prefix(snapshot.abs_path())?.to_path_buf(),
768 !metadata.is_dir,
769 ))
770 }
771 results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path));
772 for (path, is_file) in results {
773 if is_file {
774 if query.filters_path() {
775 let matched_path = if include_root {
776 let mut full_path = PathBuf::from(snapshot.root_name());
777 full_path.push(&path);
778 query.file_matches(&full_path)
779 } else {
780 query.file_matches(&path)
781 };
782 if !matched_path {
783 continue;
784 }
785 }
786 let (tx, rx) = oneshot::channel();
787 output_tx.send(rx).await?;
788 filter_tx
789 .send(MatchingEntry {
790 respond: tx,
791 worktree_path: snapshot.abs_path().clone(),
792 path: ProjectPath {
793 worktree_id: snapshot.id(),
794 path: Arc::from(path),
795 },
796 })
797 .await?;
798 } else {
799 Self::scan_ignored_dir(
800 fs,
801 snapshot,
802 &path,
803 query,
804 include_root,
805 filter_tx,
806 output_tx,
807 )
808 .await?;
809 }
810 }
811 Ok(())
812 }
813 .boxed()
814 }
815
816 async fn find_candidate_paths(
817 fs: Arc<dyn Fs>,
818 snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>,
819 open_entries: HashSet<ProjectEntryId>,
820 query: SearchQuery,
821 filter_tx: Sender<MatchingEntry>,
822 output_tx: Sender<oneshot::Receiver<ProjectPath>>,
823 ) -> Result<()> {
824 let include_root = snapshots.len() > 1;
825 for (snapshot, settings) in snapshots {
826 for entry in snapshot.entries(query.include_ignored(), 0) {
827 if entry.is_dir() && entry.is_ignored {
828 if !settings.is_path_excluded(&entry.path) {
829 Self::scan_ignored_dir(
830 &fs,
831 &snapshot,
832 &entry.path,
833 &query,
834 include_root,
835 &filter_tx,
836 &output_tx,
837 )
838 .await?;
839 }
840 continue;
841 }
842
843 if entry.is_fifo || !entry.is_file() {
844 continue;
845 }
846
847 if query.filters_path() {
848 let matched_path = if include_root {
849 let mut full_path = PathBuf::from(snapshot.root_name());
850 full_path.push(&entry.path);
851 query.file_matches(&full_path)
852 } else {
853 query.file_matches(&entry.path)
854 };
855 if !matched_path {
856 continue;
857 }
858 }
859
860 let (mut tx, rx) = oneshot::channel();
861
862 if open_entries.contains(&entry.id) {
863 tx.send(ProjectPath {
864 worktree_id: snapshot.id(),
865 path: entry.path.clone(),
866 })
867 .await?;
868 } else {
869 filter_tx
870 .send(MatchingEntry {
871 respond: tx,
872 worktree_path: snapshot.abs_path().clone(),
873 path: ProjectPath {
874 worktree_id: snapshot.id(),
875 path: entry.path.clone(),
876 },
877 })
878 .await?;
879 }
880
881 output_tx.send(rx).await?;
882 }
883 }
884 Ok(())
885 }
886
887 pub fn branches(
888 &self,
889 project_path: ProjectPath,
890 cx: &AppContext,
891 ) -> Task<Result<Vec<git::repository::Branch>>> {
892 let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
893 return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
894 };
895
896 match worktree.read(cx) {
897 Worktree::Local(local_worktree) => {
898 let branches = util::maybe!({
899 let worktree_error = |error| {
900 format!(
901 "{} for worktree {}",
902 error,
903 local_worktree.abs_path().to_string_lossy()
904 )
905 };
906
907 let entry = local_worktree
908 .git_entry(project_path.path)
909 .with_context(|| worktree_error("No git entry found"))?;
910
911 let repo = local_worktree
912 .get_local_repo(&entry)
913 .with_context(|| worktree_error("No repository found"))?
914 .repo()
915 .clone();
916
917 repo.branches()
918 });
919
920 Task::ready(branches)
921 }
922 Worktree::Remote(remote_worktree) => {
923 let request = remote_worktree.client().request(proto::GitBranches {
924 project_id: remote_worktree.project_id(),
925 repository: Some(proto::ProjectPath {
926 worktree_id: project_path.worktree_id.to_proto(),
927 path: project_path.path.to_string_lossy().to_string(), // Root path
928 }),
929 });
930
931 cx.background_executor().spawn(async move {
932 let response = request.await?;
933
934 let branches = response
935 .branches
936 .into_iter()
937 .map(|proto_branch| git::repository::Branch {
938 is_head: proto_branch.is_head,
939 name: proto_branch.name.into(),
940 unix_timestamp: proto_branch
941 .unix_timestamp
942 .map(|timestamp| timestamp as i64),
943 })
944 .collect();
945
946 Ok(branches)
947 })
948 }
949 }
950 }
951
952 pub fn update_or_create_branch(
953 &self,
954 repository: ProjectPath,
955 new_branch: String,
956 cx: &AppContext,
957 ) -> Task<Result<()>> {
958 let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
959 return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
960 };
961
962 match worktree.read(cx) {
963 Worktree::Local(local_worktree) => {
964 let result = util::maybe!({
965 let worktree_error = |error| {
966 format!(
967 "{} for worktree {}",
968 error,
969 local_worktree.abs_path().to_string_lossy()
970 )
971 };
972
973 let entry = local_worktree
974 .git_entry(repository.path)
975 .with_context(|| worktree_error("No git entry found"))?;
976
977 let repo = local_worktree
978 .get_local_repo(&entry)
979 .with_context(|| worktree_error("No repository found"))?
980 .repo()
981 .clone();
982
983 if !repo.branch_exits(&new_branch)? {
984 repo.create_branch(&new_branch)?;
985 }
986
987 repo.change_branch(&new_branch)?;
988
989 Ok(())
990 });
991
992 Task::ready(result)
993 }
994 Worktree::Remote(remote_worktree) => {
995 let request = remote_worktree.client().request(proto::UpdateGitBranch {
996 project_id: remote_worktree.project_id(),
997 repository: Some(proto::ProjectPath {
998 worktree_id: repository.worktree_id.to_proto(),
999 path: repository.path.to_string_lossy().to_string(), // Root path
1000 }),
1001 branch_name: new_branch,
1002 });
1003
1004 cx.background_executor().spawn(async move {
1005 request.await?;
1006 Ok(())
1007 })
1008 }
1009 }
1010 }
1011
1012 async fn filter_paths(
1013 fs: &Arc<dyn Fs>,
1014 mut input: Receiver<MatchingEntry>,
1015 query: &SearchQuery,
1016 ) -> Result<()> {
1017 let mut input = pin!(input);
1018 while let Some(mut entry) = input.next().await {
1019 let abs_path = entry.worktree_path.join(&entry.path.path);
1020 let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
1021 continue;
1022 };
1023 if query.detect(file).unwrap_or(false) {
1024 entry.respond.send(entry.path).await?
1025 }
1026 }
1027
1028 Ok(())
1029 }
1030
1031 pub async fn handle_create_project_entry(
1032 this: Model<Self>,
1033 envelope: TypedEnvelope<proto::CreateProjectEntry>,
1034 mut cx: AsyncAppContext,
1035 ) -> Result<proto::ProjectEntryResponse> {
1036 let worktree = this.update(&mut cx, |this, cx| {
1037 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1038 this.worktree_for_id(worktree_id, cx)
1039 .ok_or_else(|| anyhow!("worktree not found"))
1040 })??;
1041 Worktree::handle_create_entry(worktree, envelope.payload, cx).await
1042 }
1043
1044 pub async fn handle_copy_project_entry(
1045 this: Model<Self>,
1046 envelope: TypedEnvelope<proto::CopyProjectEntry>,
1047 mut cx: AsyncAppContext,
1048 ) -> Result<proto::ProjectEntryResponse> {
1049 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1050 let worktree = this.update(&mut cx, |this, cx| {
1051 this.worktree_for_entry(entry_id, cx)
1052 .ok_or_else(|| anyhow!("worktree not found"))
1053 })??;
1054 Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
1055 }
1056
1057 pub async fn handle_delete_project_entry(
1058 this: Model<Self>,
1059 envelope: TypedEnvelope<proto::DeleteProjectEntry>,
1060 mut cx: AsyncAppContext,
1061 ) -> Result<proto::ProjectEntryResponse> {
1062 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1063 let worktree = this.update(&mut cx, |this, cx| {
1064 this.worktree_for_entry(entry_id, cx)
1065 .ok_or_else(|| anyhow!("worktree not found"))
1066 })??;
1067 Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
1068 }
1069
1070 pub async fn handle_expand_project_entry(
1071 this: Model<Self>,
1072 envelope: TypedEnvelope<proto::ExpandProjectEntry>,
1073 mut cx: AsyncAppContext,
1074 ) -> Result<proto::ExpandProjectEntryResponse> {
1075 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1076 let worktree = this
1077 .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
1078 .ok_or_else(|| anyhow!("invalid request"))?;
1079 Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
1080 }
1081
1082 pub async fn handle_git_branches(
1083 this: Model<Self>,
1084 branches: TypedEnvelope<proto::GitBranches>,
1085 cx: AsyncAppContext,
1086 ) -> Result<proto::GitBranchesResponse> {
1087 let project_path = branches
1088 .payload
1089 .repository
1090 .clone()
1091 .context("Invalid GitBranches call")?;
1092 let project_path = ProjectPath {
1093 worktree_id: WorktreeId::from_proto(project_path.worktree_id),
1094 path: Path::new(&project_path.path).into(),
1095 };
1096
1097 let branches = this
1098 .read_with(&cx, |this, cx| this.branches(project_path, cx))?
1099 .await?;
1100
1101 Ok(proto::GitBranchesResponse {
1102 branches: branches
1103 .into_iter()
1104 .map(|branch| proto::Branch {
1105 is_head: branch.is_head,
1106 name: branch.name.to_string(),
1107 unix_timestamp: branch.unix_timestamp.map(|timestamp| timestamp as u64),
1108 })
1109 .collect(),
1110 })
1111 }
1112
1113 pub async fn handle_update_branch(
1114 this: Model<Self>,
1115 update_branch: TypedEnvelope<proto::UpdateGitBranch>,
1116 cx: AsyncAppContext,
1117 ) -> Result<proto::Ack> {
1118 let project_path = update_branch
1119 .payload
1120 .repository
1121 .clone()
1122 .context("Invalid GitBranches call")?;
1123 let project_path = ProjectPath {
1124 worktree_id: WorktreeId::from_proto(project_path.worktree_id),
1125 path: Path::new(&project_path.path).into(),
1126 };
1127 let new_branch = update_branch.payload.branch_name;
1128
1129 this.read_with(&cx, |this, cx| {
1130 this.update_or_create_branch(project_path, new_branch, cx)
1131 })?
1132 .await?;
1133
1134 Ok(proto::Ack {})
1135 }
1136}
1137
1138#[derive(Clone, Debug)]
1139enum WorktreeHandle {
1140 Strong(Model<Worktree>),
1141 Weak(WeakModel<Worktree>),
1142}
1143
1144impl WorktreeHandle {
1145 fn upgrade(&self) -> Option<Model<Worktree>> {
1146 match self {
1147 WorktreeHandle::Strong(handle) => Some(handle.clone()),
1148 WorktreeHandle::Weak(handle) => handle.upgrade(),
1149 }
1150 }
1151}