1use crate::{
2 worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, WorkDirectory,
3 Worktree, WorktreeModelHandle,
4};
5use anyhow::Result;
6use fs::{FakeFs, Fs, RealFs, RemoveOptions};
7use git::{
8 repository::RepoPath,
9 status::{FileStatus, StatusCode, TrackedStatus},
10 GITIGNORE,
11};
12use git2::RepositoryInitOptions;
13use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
14use parking_lot::Mutex;
15use postage::stream::Stream;
16use pretty_assertions::assert_eq;
17use rand::prelude::*;
18use rpc::proto::WorktreeRelatedMessage;
19use serde_json::json;
20use settings::{Settings, SettingsStore};
21use std::{
22 env,
23 fmt::Write,
24 mem,
25 path::{Path, PathBuf},
26 sync::Arc,
27};
28use util::{path, test::TempTree, ResultExt};
29
30#[gpui::test]
31async fn test_traversal(cx: &mut TestAppContext) {
32 init_test(cx);
33 let fs = FakeFs::new(cx.background_executor.clone());
34 fs.insert_tree(
35 "/root",
36 json!({
37 ".gitignore": "a/b\n",
38 "a": {
39 "b": "",
40 "c": "",
41 }
42 }),
43 )
44 .await;
45
46 let tree = Worktree::local(
47 Path::new("/root"),
48 true,
49 fs,
50 Default::default(),
51 &mut cx.to_async(),
52 )
53 .await
54 .unwrap();
55 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
56 .await;
57
58 tree.read_with(cx, |tree, _| {
59 assert_eq!(
60 tree.entries(false, 0)
61 .map(|entry| entry.path.as_ref())
62 .collect::<Vec<_>>(),
63 vec![
64 Path::new(""),
65 Path::new(".gitignore"),
66 Path::new("a"),
67 Path::new("a/c"),
68 ]
69 );
70 assert_eq!(
71 tree.entries(true, 0)
72 .map(|entry| entry.path.as_ref())
73 .collect::<Vec<_>>(),
74 vec![
75 Path::new(""),
76 Path::new(".gitignore"),
77 Path::new("a"),
78 Path::new("a/b"),
79 Path::new("a/c"),
80 ]
81 );
82 })
83}
84
85#[gpui::test(iterations = 10)]
86async fn test_circular_symlinks(cx: &mut TestAppContext) {
87 init_test(cx);
88 let fs = FakeFs::new(cx.background_executor.clone());
89 fs.insert_tree(
90 "/root",
91 json!({
92 "lib": {
93 "a": {
94 "a.txt": ""
95 },
96 "b": {
97 "b.txt": ""
98 }
99 }
100 }),
101 )
102 .await;
103 fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
104 .await
105 .unwrap();
106 fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
107 .await
108 .unwrap();
109
110 let tree = Worktree::local(
111 Path::new("/root"),
112 true,
113 fs.clone(),
114 Default::default(),
115 &mut cx.to_async(),
116 )
117 .await
118 .unwrap();
119
120 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
121 .await;
122
123 tree.read_with(cx, |tree, _| {
124 assert_eq!(
125 tree.entries(false, 0)
126 .map(|entry| entry.path.as_ref())
127 .collect::<Vec<_>>(),
128 vec![
129 Path::new(""),
130 Path::new("lib"),
131 Path::new("lib/a"),
132 Path::new("lib/a/a.txt"),
133 Path::new("lib/a/lib"),
134 Path::new("lib/b"),
135 Path::new("lib/b/b.txt"),
136 Path::new("lib/b/lib"),
137 ]
138 );
139 });
140
141 fs.rename(
142 Path::new("/root/lib/a/lib"),
143 Path::new("/root/lib/a/lib-2"),
144 Default::default(),
145 )
146 .await
147 .unwrap();
148 cx.executor().run_until_parked();
149 tree.read_with(cx, |tree, _| {
150 assert_eq!(
151 tree.entries(false, 0)
152 .map(|entry| entry.path.as_ref())
153 .collect::<Vec<_>>(),
154 vec![
155 Path::new(""),
156 Path::new("lib"),
157 Path::new("lib/a"),
158 Path::new("lib/a/a.txt"),
159 Path::new("lib/a/lib-2"),
160 Path::new("lib/b"),
161 Path::new("lib/b/b.txt"),
162 Path::new("lib/b/lib"),
163 ]
164 );
165 });
166}
167
168#[gpui::test]
169async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
170 init_test(cx);
171 let fs = FakeFs::new(cx.background_executor.clone());
172 fs.insert_tree(
173 "/root",
174 json!({
175 "dir1": {
176 "deps": {
177 // symlinks here
178 },
179 "src": {
180 "a.rs": "",
181 "b.rs": "",
182 },
183 },
184 "dir2": {
185 "src": {
186 "c.rs": "",
187 "d.rs": "",
188 }
189 },
190 "dir3": {
191 "deps": {},
192 "src": {
193 "e.rs": "",
194 "f.rs": "",
195 },
196 }
197 }),
198 )
199 .await;
200
201 // These symlinks point to directories outside of the worktree's root, dir1.
202 fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
203 .await
204 .unwrap();
205 fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
206 .await
207 .unwrap();
208
209 let tree = Worktree::local(
210 Path::new("/root/dir1"),
211 true,
212 fs.clone(),
213 Default::default(),
214 &mut cx.to_async(),
215 )
216 .await
217 .unwrap();
218
219 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
220 .await;
221
222 let tree_updates = Arc::new(Mutex::new(Vec::new()));
223 tree.update(cx, |_, cx| {
224 let tree_updates = tree_updates.clone();
225 cx.subscribe(&tree, move |_, _, event, _| {
226 if let Event::UpdatedEntries(update) = event {
227 tree_updates.lock().extend(
228 update
229 .iter()
230 .map(|(path, _, change)| (path.clone(), *change)),
231 );
232 }
233 })
234 .detach();
235 });
236
237 // The symlinked directories are not scanned by default.
238 tree.read_with(cx, |tree, _| {
239 assert_eq!(
240 tree.entries(true, 0)
241 .map(|entry| (entry.path.as_ref(), entry.is_external))
242 .collect::<Vec<_>>(),
243 vec![
244 (Path::new(""), false),
245 (Path::new("deps"), false),
246 (Path::new("deps/dep-dir2"), true),
247 (Path::new("deps/dep-dir3"), true),
248 (Path::new("src"), false),
249 (Path::new("src/a.rs"), false),
250 (Path::new("src/b.rs"), false),
251 ]
252 );
253
254 assert_eq!(
255 tree.entry_for_path("deps/dep-dir2").unwrap().kind,
256 EntryKind::UnloadedDir
257 );
258 });
259
260 // Expand one of the symlinked directories.
261 tree.read_with(cx, |tree, _| {
262 tree.as_local()
263 .unwrap()
264 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
265 })
266 .recv()
267 .await;
268
269 // The expanded directory's contents are loaded. Subdirectories are
270 // not scanned yet.
271 tree.read_with(cx, |tree, _| {
272 assert_eq!(
273 tree.entries(true, 0)
274 .map(|entry| (entry.path.as_ref(), entry.is_external))
275 .collect::<Vec<_>>(),
276 vec![
277 (Path::new(""), false),
278 (Path::new("deps"), false),
279 (Path::new("deps/dep-dir2"), true),
280 (Path::new("deps/dep-dir3"), true),
281 (Path::new("deps/dep-dir3/deps"), true),
282 (Path::new("deps/dep-dir3/src"), true),
283 (Path::new("src"), false),
284 (Path::new("src/a.rs"), false),
285 (Path::new("src/b.rs"), false),
286 ]
287 );
288 });
289 assert_eq!(
290 mem::take(&mut *tree_updates.lock()),
291 &[
292 (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
293 (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
294 (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
295 ]
296 );
297
298 // Expand a subdirectory of one of the symlinked directories.
299 tree.read_with(cx, |tree, _| {
300 tree.as_local()
301 .unwrap()
302 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
303 })
304 .recv()
305 .await;
306
307 // The expanded subdirectory's contents are loaded.
308 tree.read_with(cx, |tree, _| {
309 assert_eq!(
310 tree.entries(true, 0)
311 .map(|entry| (entry.path.as_ref(), entry.is_external))
312 .collect::<Vec<_>>(),
313 vec![
314 (Path::new(""), false),
315 (Path::new("deps"), false),
316 (Path::new("deps/dep-dir2"), true),
317 (Path::new("deps/dep-dir3"), true),
318 (Path::new("deps/dep-dir3/deps"), true),
319 (Path::new("deps/dep-dir3/src"), true),
320 (Path::new("deps/dep-dir3/src/e.rs"), true),
321 (Path::new("deps/dep-dir3/src/f.rs"), true),
322 (Path::new("src"), false),
323 (Path::new("src/a.rs"), false),
324 (Path::new("src/b.rs"), false),
325 ]
326 );
327 });
328
329 assert_eq!(
330 mem::take(&mut *tree_updates.lock()),
331 &[
332 (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
333 (
334 Path::new("deps/dep-dir3/src/e.rs").into(),
335 PathChange::Loaded
336 ),
337 (
338 Path::new("deps/dep-dir3/src/f.rs").into(),
339 PathChange::Loaded
340 )
341 ]
342 );
343}
344
345#[cfg(target_os = "macos")]
346#[gpui::test]
347async fn test_renaming_case_only(cx: &mut TestAppContext) {
348 cx.executor().allow_parking();
349 init_test(cx);
350
351 const OLD_NAME: &str = "aaa.rs";
352 const NEW_NAME: &str = "AAA.rs";
353
354 let fs = Arc::new(RealFs::default());
355 let temp_root = TempTree::new(json!({
356 OLD_NAME: "",
357 }));
358
359 let tree = Worktree::local(
360 temp_root.path(),
361 true,
362 fs.clone(),
363 Default::default(),
364 &mut cx.to_async(),
365 )
366 .await
367 .unwrap();
368
369 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
370 .await;
371 tree.read_with(cx, |tree, _| {
372 assert_eq!(
373 tree.entries(true, 0)
374 .map(|entry| entry.path.as_ref())
375 .collect::<Vec<_>>(),
376 vec![Path::new(""), Path::new(OLD_NAME)]
377 );
378 });
379
380 fs.rename(
381 &temp_root.path().join(OLD_NAME),
382 &temp_root.path().join(NEW_NAME),
383 fs::RenameOptions {
384 overwrite: true,
385 ignore_if_exists: true,
386 },
387 )
388 .await
389 .unwrap();
390
391 tree.flush_fs_events(cx).await;
392
393 tree.read_with(cx, |tree, _| {
394 assert_eq!(
395 tree.entries(true, 0)
396 .map(|entry| entry.path.as_ref())
397 .collect::<Vec<_>>(),
398 vec![Path::new(""), Path::new(NEW_NAME)]
399 );
400 });
401}
402
403#[gpui::test]
404async fn test_open_gitignored_files(cx: &mut TestAppContext) {
405 init_test(cx);
406 let fs = FakeFs::new(cx.background_executor.clone());
407 fs.insert_tree(
408 "/root",
409 json!({
410 ".gitignore": "node_modules\n",
411 "one": {
412 "node_modules": {
413 "a": {
414 "a1.js": "a1",
415 "a2.js": "a2",
416 },
417 "b": {
418 "b1.js": "b1",
419 "b2.js": "b2",
420 },
421 "c": {
422 "c1.js": "c1",
423 "c2.js": "c2",
424 }
425 },
426 },
427 "two": {
428 "x.js": "",
429 "y.js": "",
430 },
431 }),
432 )
433 .await;
434
435 let tree = Worktree::local(
436 Path::new("/root"),
437 true,
438 fs.clone(),
439 Default::default(),
440 &mut cx.to_async(),
441 )
442 .await
443 .unwrap();
444
445 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
446 .await;
447
448 tree.read_with(cx, |tree, _| {
449 assert_eq!(
450 tree.entries(true, 0)
451 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
452 .collect::<Vec<_>>(),
453 vec![
454 (Path::new(""), false),
455 (Path::new(".gitignore"), false),
456 (Path::new("one"), false),
457 (Path::new("one/node_modules"), true),
458 (Path::new("two"), false),
459 (Path::new("two/x.js"), false),
460 (Path::new("two/y.js"), false),
461 ]
462 );
463 });
464
465 // Open a file that is nested inside of a gitignored directory that
466 // has not yet been expanded.
467 let prev_read_dir_count = fs.read_dir_call_count();
468 let loaded = tree
469 .update(cx, |tree, cx| {
470 tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
471 })
472 .await
473 .unwrap();
474
475 tree.read_with(cx, |tree, _| {
476 assert_eq!(
477 tree.entries(true, 0)
478 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
479 .collect::<Vec<_>>(),
480 vec![
481 (Path::new(""), false),
482 (Path::new(".gitignore"), false),
483 (Path::new("one"), false),
484 (Path::new("one/node_modules"), true),
485 (Path::new("one/node_modules/a"), true),
486 (Path::new("one/node_modules/b"), true),
487 (Path::new("one/node_modules/b/b1.js"), true),
488 (Path::new("one/node_modules/b/b2.js"), true),
489 (Path::new("one/node_modules/c"), true),
490 (Path::new("two"), false),
491 (Path::new("two/x.js"), false),
492 (Path::new("two/y.js"), false),
493 ]
494 );
495
496 assert_eq!(
497 loaded.file.path.as_ref(),
498 Path::new("one/node_modules/b/b1.js")
499 );
500
501 // Only the newly-expanded directories are scanned.
502 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
503 });
504
505 // Open another file in a different subdirectory of the same
506 // gitignored directory.
507 let prev_read_dir_count = fs.read_dir_call_count();
508 let loaded = tree
509 .update(cx, |tree, cx| {
510 tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
511 })
512 .await
513 .unwrap();
514
515 tree.read_with(cx, |tree, _| {
516 assert_eq!(
517 tree.entries(true, 0)
518 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
519 .collect::<Vec<_>>(),
520 vec![
521 (Path::new(""), false),
522 (Path::new(".gitignore"), false),
523 (Path::new("one"), false),
524 (Path::new("one/node_modules"), true),
525 (Path::new("one/node_modules/a"), true),
526 (Path::new("one/node_modules/a/a1.js"), true),
527 (Path::new("one/node_modules/a/a2.js"), true),
528 (Path::new("one/node_modules/b"), true),
529 (Path::new("one/node_modules/b/b1.js"), true),
530 (Path::new("one/node_modules/b/b2.js"), true),
531 (Path::new("one/node_modules/c"), true),
532 (Path::new("two"), false),
533 (Path::new("two/x.js"), false),
534 (Path::new("two/y.js"), false),
535 ]
536 );
537
538 assert_eq!(
539 loaded.file.path.as_ref(),
540 Path::new("one/node_modules/a/a2.js")
541 );
542
543 // Only the newly-expanded directory is scanned.
544 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
545 });
546
547 let path = PathBuf::from("/root/one/node_modules/c/lib");
548
549 // No work happens when files and directories change within an unloaded directory.
550 let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
551 // When we open a directory, we check each ancestor whether it's a git
552 // repository. That means we have an fs.metadata call per ancestor that we
553 // need to subtract here.
554 let ancestors = path.ancestors().count();
555
556 fs.create_dir(path.as_ref()).await.unwrap();
557 cx.executor().run_until_parked();
558
559 assert_eq!(
560 fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
561 0
562 );
563}
564
565#[gpui::test]
566async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
567 init_test(cx);
568 let fs = FakeFs::new(cx.background_executor.clone());
569 fs.insert_tree(
570 "/root",
571 json!({
572 ".gitignore": "node_modules\n",
573 "a": {
574 "a.js": "",
575 },
576 "b": {
577 "b.js": "",
578 },
579 "node_modules": {
580 "c": {
581 "c.js": "",
582 },
583 "d": {
584 "d.js": "",
585 "e": {
586 "e1.js": "",
587 "e2.js": "",
588 },
589 "f": {
590 "f1.js": "",
591 "f2.js": "",
592 }
593 },
594 },
595 }),
596 )
597 .await;
598
599 let tree = Worktree::local(
600 Path::new("/root"),
601 true,
602 fs.clone(),
603 Default::default(),
604 &mut cx.to_async(),
605 )
606 .await
607 .unwrap();
608
609 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
610 .await;
611
612 // Open a file within the gitignored directory, forcing some of its
613 // subdirectories to be read, but not all.
614 let read_dir_count_1 = fs.read_dir_call_count();
615 tree.read_with(cx, |tree, _| {
616 tree.as_local()
617 .unwrap()
618 .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
619 })
620 .recv()
621 .await;
622
623 // Those subdirectories are now loaded.
624 tree.read_with(cx, |tree, _| {
625 assert_eq!(
626 tree.entries(true, 0)
627 .map(|e| (e.path.as_ref(), e.is_ignored))
628 .collect::<Vec<_>>(),
629 &[
630 (Path::new(""), false),
631 (Path::new(".gitignore"), false),
632 (Path::new("a"), false),
633 (Path::new("a/a.js"), false),
634 (Path::new("b"), false),
635 (Path::new("b/b.js"), false),
636 (Path::new("node_modules"), true),
637 (Path::new("node_modules/c"), true),
638 (Path::new("node_modules/d"), true),
639 (Path::new("node_modules/d/d.js"), true),
640 (Path::new("node_modules/d/e"), true),
641 (Path::new("node_modules/d/f"), true),
642 ]
643 );
644 });
645 let read_dir_count_2 = fs.read_dir_call_count();
646 assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
647
648 // Update the gitignore so that node_modules is no longer ignored,
649 // but a subdirectory is ignored
650 fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
651 .await
652 .unwrap();
653 cx.executor().run_until_parked();
654
655 // All of the directories that are no longer ignored are now loaded.
656 tree.read_with(cx, |tree, _| {
657 assert_eq!(
658 tree.entries(true, 0)
659 .map(|e| (e.path.as_ref(), e.is_ignored))
660 .collect::<Vec<_>>(),
661 &[
662 (Path::new(""), false),
663 (Path::new(".gitignore"), false),
664 (Path::new("a"), false),
665 (Path::new("a/a.js"), false),
666 (Path::new("b"), false),
667 (Path::new("b/b.js"), false),
668 // This directory is no longer ignored
669 (Path::new("node_modules"), false),
670 (Path::new("node_modules/c"), false),
671 (Path::new("node_modules/c/c.js"), false),
672 (Path::new("node_modules/d"), false),
673 (Path::new("node_modules/d/d.js"), false),
674 // This subdirectory is now ignored
675 (Path::new("node_modules/d/e"), true),
676 (Path::new("node_modules/d/f"), false),
677 (Path::new("node_modules/d/f/f1.js"), false),
678 (Path::new("node_modules/d/f/f2.js"), false),
679 ]
680 );
681 });
682
683 // Each of the newly-loaded directories is scanned only once.
684 let read_dir_count_3 = fs.read_dir_call_count();
685 assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
686}
687
688#[gpui::test(iterations = 10)]
689async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
690 init_test(cx);
691 cx.update(|cx| {
692 cx.update_global::<SettingsStore, _>(|store, cx| {
693 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
694 project_settings.file_scan_exclusions = Some(Vec::new());
695 });
696 });
697 });
698 let fs = FakeFs::new(cx.background_executor.clone());
699 fs.insert_tree(
700 path!("/root"),
701 json!({
702 ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
703 "tree": {
704 ".git": {},
705 ".gitignore": "ignored-dir\n",
706 "tracked-dir": {
707 "tracked-file1": "",
708 "ancestor-ignored-file1": "",
709 },
710 "ignored-dir": {
711 "ignored-file1": ""
712 }
713 }
714 }),
715 )
716 .await;
717 fs.set_head_and_index_for_repo(
718 path!("/root/tree/.git").as_ref(),
719 &[
720 (".gitignore".into(), "ignored-dir\n".into()),
721 ("tracked-dir/tracked-file1".into(), "".into()),
722 ],
723 );
724
725 let tree = Worktree::local(
726 path!("/root/tree").as_ref(),
727 true,
728 fs.clone(),
729 Default::default(),
730 &mut cx.to_async(),
731 )
732 .await
733 .unwrap();
734 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
735 .await;
736
737 tree.read_with(cx, |tree, _| {
738 tree.as_local()
739 .unwrap()
740 .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
741 })
742 .recv()
743 .await;
744
745 cx.read(|cx| {
746 let tree = tree.read(cx);
747 assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
748 assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
749 assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
750 });
751
752 fs.create_file(
753 path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
754 Default::default(),
755 )
756 .await
757 .unwrap();
758 fs.set_index_for_repo(
759 path!("/root/tree/.git").as_ref(),
760 &[
761 (".gitignore".into(), "ignored-dir\n".into()),
762 ("tracked-dir/tracked-file1".into(), "".into()),
763 ("tracked-dir/tracked-file2".into(), "".into()),
764 ],
765 );
766 fs.create_file(
767 path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
768 Default::default(),
769 )
770 .await
771 .unwrap();
772 fs.create_file(
773 path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
774 Default::default(),
775 )
776 .await
777 .unwrap();
778
779 cx.executor().run_until_parked();
780 cx.read(|cx| {
781 let tree = tree.read(cx);
782 assert_entry_git_state(
783 tree,
784 "tracked-dir/tracked-file2",
785 Some(StatusCode::Added),
786 false,
787 );
788 assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
789 assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
790 assert!(tree.entry_for_path(".git").unwrap().is_ignored);
791 });
792}
793
794#[gpui::test]
795async fn test_update_gitignore(cx: &mut TestAppContext) {
796 init_test(cx);
797 let fs = FakeFs::new(cx.background_executor.clone());
798 fs.insert_tree(
799 path!("/root"),
800 json!({
801 ".git": {},
802 ".gitignore": "*.txt\n",
803 "a.xml": "<a></a>",
804 "b.txt": "Some text"
805 }),
806 )
807 .await;
808
809 fs.set_head_and_index_for_repo(
810 path!("/root/.git").as_ref(),
811 &[
812 (".gitignore".into(), "*.txt\n".into()),
813 ("a.xml".into(), "<a></a>".into()),
814 ],
815 );
816
817 let tree = Worktree::local(
818 path!("/root").as_ref(),
819 true,
820 fs.clone(),
821 Default::default(),
822 &mut cx.to_async(),
823 )
824 .await
825 .unwrap();
826 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
827 .await;
828
829 tree.read_with(cx, |tree, _| {
830 tree.as_local()
831 .unwrap()
832 .refresh_entries_for_paths(vec![Path::new("").into()])
833 })
834 .recv()
835 .await;
836
837 // One file is unmodified, the other is ignored.
838 cx.read(|cx| {
839 let tree = tree.read(cx);
840 assert_entry_git_state(tree, "a.xml", None, false);
841 assert_entry_git_state(tree, "b.txt", None, true);
842 });
843
844 // Change the gitignore, and stage the newly non-ignored file.
845 fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
846 .await
847 .unwrap();
848 fs.set_index_for_repo(
849 Path::new(path!("/root/.git")),
850 &[
851 (".gitignore".into(), "*.txt\n".into()),
852 ("a.xml".into(), "<a></a>".into()),
853 ("b.txt".into(), "Some text".into()),
854 ],
855 );
856
857 cx.executor().run_until_parked();
858 cx.read(|cx| {
859 let tree = tree.read(cx);
860 assert_entry_git_state(tree, "a.xml", None, true);
861 assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
862 });
863}
864
865#[gpui::test]
866async fn test_write_file(cx: &mut TestAppContext) {
867 init_test(cx);
868 cx.executor().allow_parking();
869 let dir = TempTree::new(json!({
870 ".git": {},
871 ".gitignore": "ignored-dir\n",
872 "tracked-dir": {},
873 "ignored-dir": {}
874 }));
875
876 let worktree = Worktree::local(
877 dir.path(),
878 true,
879 Arc::new(RealFs::default()),
880 Default::default(),
881 &mut cx.to_async(),
882 )
883 .await
884 .unwrap();
885
886 #[cfg(not(target_os = "macos"))]
887 fs::fs_watcher::global(|_| {}).unwrap();
888
889 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
890 .await;
891 worktree.flush_fs_events(cx).await;
892
893 worktree
894 .update(cx, |tree, cx| {
895 tree.write_file(
896 Path::new("tracked-dir/file.txt"),
897 "hello".into(),
898 Default::default(),
899 cx,
900 )
901 })
902 .await
903 .unwrap();
904 worktree
905 .update(cx, |tree, cx| {
906 tree.write_file(
907 Path::new("ignored-dir/file.txt"),
908 "world".into(),
909 Default::default(),
910 cx,
911 )
912 })
913 .await
914 .unwrap();
915
916 worktree.read_with(cx, |tree, _| {
917 let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
918 let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
919 assert!(!tracked.is_ignored);
920 assert!(ignored.is_ignored);
921 });
922}
923
924#[gpui::test]
925async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
926 init_test(cx);
927 cx.executor().allow_parking();
928 let dir = TempTree::new(json!({
929 ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
930 "target": {
931 "index": "blah2"
932 },
933 "node_modules": {
934 ".DS_Store": "",
935 "prettier": {
936 "package.json": "{}",
937 },
938 },
939 "src": {
940 ".DS_Store": "",
941 "foo": {
942 "foo.rs": "mod another;\n",
943 "another.rs": "// another",
944 },
945 "bar": {
946 "bar.rs": "// bar",
947 },
948 "lib.rs": "mod foo;\nmod bar;\n",
949 },
950 "top_level.txt": "top level file",
951 ".DS_Store": "",
952 }));
953 cx.update(|cx| {
954 cx.update_global::<SettingsStore, _>(|store, cx| {
955 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
956 project_settings.file_scan_exclusions = Some(vec![]);
957 project_settings.file_scan_inclusions = Some(vec![
958 "node_modules/**/package.json".to_string(),
959 "**/.DS_Store".to_string(),
960 ]);
961 });
962 });
963 });
964
965 let tree = Worktree::local(
966 dir.path(),
967 true,
968 Arc::new(RealFs::default()),
969 Default::default(),
970 &mut cx.to_async(),
971 )
972 .await
973 .unwrap();
974 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
975 .await;
976 tree.flush_fs_events(cx).await;
977 tree.read_with(cx, |tree, _| {
978 // Assert that file_scan_inclusions overrides file_scan_exclusions.
979 check_worktree_entries(
980 tree,
981 &[],
982 &["target", "node_modules"],
983 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
984 &[
985 "node_modules/prettier/package.json",
986 ".DS_Store",
987 "node_modules/.DS_Store",
988 "src/.DS_Store",
989 ],
990 )
991 });
992}
993
994#[gpui::test]
995async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
996 init_test(cx);
997 cx.executor().allow_parking();
998 let dir = TempTree::new(json!({
999 ".gitignore": "**/target\n/node_modules\n",
1000 "target": {
1001 "index": "blah2"
1002 },
1003 "node_modules": {
1004 ".DS_Store": "",
1005 "prettier": {
1006 "package.json": "{}",
1007 },
1008 },
1009 "src": {
1010 ".DS_Store": "",
1011 "foo": {
1012 "foo.rs": "mod another;\n",
1013 "another.rs": "// another",
1014 },
1015 },
1016 ".DS_Store": "",
1017 }));
1018
1019 cx.update(|cx| {
1020 cx.update_global::<SettingsStore, _>(|store, cx| {
1021 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1022 project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]);
1023 project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]);
1024 });
1025 });
1026 });
1027
1028 let tree = Worktree::local(
1029 dir.path(),
1030 true,
1031 Arc::new(RealFs::default()),
1032 Default::default(),
1033 &mut cx.to_async(),
1034 )
1035 .await
1036 .unwrap();
1037 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1038 .await;
1039 tree.flush_fs_events(cx).await;
1040 tree.read_with(cx, |tree, _| {
1041 // Assert that file_scan_inclusions overrides file_scan_exclusions.
1042 check_worktree_entries(
1043 tree,
1044 &[".DS_Store, src/.DS_Store"],
1045 &["target", "node_modules"],
1046 &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
1047 &[],
1048 )
1049 });
1050}
1051
1052#[gpui::test]
1053async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
1054 init_test(cx);
1055 cx.executor().allow_parking();
1056 let dir = TempTree::new(json!({
1057 ".gitignore": "**/target\n/node_modules/\n",
1058 "target": {
1059 "index": "blah2"
1060 },
1061 "node_modules": {
1062 ".DS_Store": "",
1063 "prettier": {
1064 "package.json": "{}",
1065 },
1066 },
1067 "src": {
1068 ".DS_Store": "",
1069 "foo": {
1070 "foo.rs": "mod another;\n",
1071 "another.rs": "// another",
1072 },
1073 },
1074 ".DS_Store": "",
1075 }));
1076
1077 cx.update(|cx| {
1078 cx.update_global::<SettingsStore, _>(|store, cx| {
1079 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1080 project_settings.file_scan_exclusions = Some(vec![]);
1081 project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]);
1082 });
1083 });
1084 });
1085 let tree = Worktree::local(
1086 dir.path(),
1087 true,
1088 Arc::new(RealFs::default()),
1089 Default::default(),
1090 &mut cx.to_async(),
1091 )
1092 .await
1093 .unwrap();
1094 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1095 .await;
1096 tree.flush_fs_events(cx).await;
1097
1098 tree.read_with(cx, |tree, _| {
1099 assert!(tree
1100 .entry_for_path("node_modules")
1101 .is_some_and(|f| f.is_always_included));
1102 assert!(tree
1103 .entry_for_path("node_modules/prettier/package.json")
1104 .is_some_and(|f| f.is_always_included));
1105 });
1106
1107 cx.update(|cx| {
1108 cx.update_global::<SettingsStore, _>(|store, cx| {
1109 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1110 project_settings.file_scan_exclusions = Some(vec![]);
1111 project_settings.file_scan_inclusions = Some(vec![]);
1112 });
1113 });
1114 });
1115 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1116 .await;
1117 tree.flush_fs_events(cx).await;
1118
1119 tree.read_with(cx, |tree, _| {
1120 assert!(tree
1121 .entry_for_path("node_modules")
1122 .is_some_and(|f| !f.is_always_included));
1123 assert!(tree
1124 .entry_for_path("node_modules/prettier/package.json")
1125 .is_some_and(|f| !f.is_always_included));
1126 });
1127}
1128
1129#[gpui::test]
1130async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1131 init_test(cx);
1132 cx.executor().allow_parking();
1133 let dir = TempTree::new(json!({
1134 ".gitignore": "**/target\n/node_modules\n",
1135 "target": {
1136 "index": "blah2"
1137 },
1138 "node_modules": {
1139 ".DS_Store": "",
1140 "prettier": {
1141 "package.json": "{}",
1142 },
1143 },
1144 "src": {
1145 ".DS_Store": "",
1146 "foo": {
1147 "foo.rs": "mod another;\n",
1148 "another.rs": "// another",
1149 },
1150 "bar": {
1151 "bar.rs": "// bar",
1152 },
1153 "lib.rs": "mod foo;\nmod bar;\n",
1154 },
1155 ".DS_Store": "",
1156 }));
1157 cx.update(|cx| {
1158 cx.update_global::<SettingsStore, _>(|store, cx| {
1159 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1160 project_settings.file_scan_exclusions =
1161 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1162 });
1163 });
1164 });
1165
1166 let tree = Worktree::local(
1167 dir.path(),
1168 true,
1169 Arc::new(RealFs::default()),
1170 Default::default(),
1171 &mut cx.to_async(),
1172 )
1173 .await
1174 .unwrap();
1175 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1176 .await;
1177 tree.flush_fs_events(cx).await;
1178 tree.read_with(cx, |tree, _| {
1179 check_worktree_entries(
1180 tree,
1181 &[
1182 "src/foo/foo.rs",
1183 "src/foo/another.rs",
1184 "node_modules/.DS_Store",
1185 "src/.DS_Store",
1186 ".DS_Store",
1187 ],
1188 &["target", "node_modules"],
1189 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1190 &[],
1191 )
1192 });
1193
1194 cx.update(|cx| {
1195 cx.update_global::<SettingsStore, _>(|store, cx| {
1196 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1197 project_settings.file_scan_exclusions =
1198 Some(vec!["**/node_modules/**".to_string()]);
1199 });
1200 });
1201 });
1202 tree.flush_fs_events(cx).await;
1203 cx.executor().run_until_parked();
1204 tree.read_with(cx, |tree, _| {
1205 check_worktree_entries(
1206 tree,
1207 &[
1208 "node_modules/prettier/package.json",
1209 "node_modules/.DS_Store",
1210 "node_modules",
1211 ],
1212 &["target"],
1213 &[
1214 ".gitignore",
1215 "src/lib.rs",
1216 "src/bar/bar.rs",
1217 "src/foo/foo.rs",
1218 "src/foo/another.rs",
1219 "src/.DS_Store",
1220 ".DS_Store",
1221 ],
1222 &[],
1223 )
1224 });
1225}
1226
1227#[gpui::test]
1228async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1229 init_test(cx);
1230 cx.executor().allow_parking();
1231 let dir = TempTree::new(json!({
1232 ".git": {
1233 "HEAD": "ref: refs/heads/main\n",
1234 "foo": "bar",
1235 },
1236 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1237 "target": {
1238 "index": "blah2"
1239 },
1240 "node_modules": {
1241 ".DS_Store": "",
1242 "prettier": {
1243 "package.json": "{}",
1244 },
1245 },
1246 "src": {
1247 ".DS_Store": "",
1248 "foo": {
1249 "foo.rs": "mod another;\n",
1250 "another.rs": "// another",
1251 },
1252 "bar": {
1253 "bar.rs": "// bar",
1254 },
1255 "lib.rs": "mod foo;\nmod bar;\n",
1256 },
1257 ".DS_Store": "",
1258 }));
1259 cx.update(|cx| {
1260 cx.update_global::<SettingsStore, _>(|store, cx| {
1261 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1262 project_settings.file_scan_exclusions = Some(vec![
1263 "**/.git".to_string(),
1264 "node_modules/".to_string(),
1265 "build_output".to_string(),
1266 ]);
1267 });
1268 });
1269 });
1270
1271 let tree = Worktree::local(
1272 dir.path(),
1273 true,
1274 Arc::new(RealFs::default()),
1275 Default::default(),
1276 &mut cx.to_async(),
1277 )
1278 .await
1279 .unwrap();
1280 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1281 .await;
1282 tree.flush_fs_events(cx).await;
1283 tree.read_with(cx, |tree, _| {
1284 check_worktree_entries(
1285 tree,
1286 &[
1287 ".git/HEAD",
1288 ".git/foo",
1289 "node_modules",
1290 "node_modules/.DS_Store",
1291 "node_modules/prettier",
1292 "node_modules/prettier/package.json",
1293 ],
1294 &["target"],
1295 &[
1296 ".DS_Store",
1297 "src/.DS_Store",
1298 "src/lib.rs",
1299 "src/foo/foo.rs",
1300 "src/foo/another.rs",
1301 "src/bar/bar.rs",
1302 ".gitignore",
1303 ],
1304 &[],
1305 )
1306 });
1307
1308 let new_excluded_dir = dir.path().join("build_output");
1309 let new_ignored_dir = dir.path().join("test_output");
1310 std::fs::create_dir_all(&new_excluded_dir)
1311 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1312 std::fs::create_dir_all(&new_ignored_dir)
1313 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1314 let node_modules_dir = dir.path().join("node_modules");
1315 let dot_git_dir = dir.path().join(".git");
1316 let src_dir = dir.path().join("src");
1317 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1318 assert!(
1319 existing_dir.is_dir(),
1320 "Expect {existing_dir:?} to be present in the FS already"
1321 );
1322 }
1323
1324 for directory_for_new_file in [
1325 new_excluded_dir,
1326 new_ignored_dir,
1327 node_modules_dir,
1328 dot_git_dir,
1329 src_dir,
1330 ] {
1331 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1332 .unwrap_or_else(|e| {
1333 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1334 });
1335 }
1336 tree.flush_fs_events(cx).await;
1337
1338 tree.read_with(cx, |tree, _| {
1339 check_worktree_entries(
1340 tree,
1341 &[
1342 ".git/HEAD",
1343 ".git/foo",
1344 ".git/new_file",
1345 "node_modules",
1346 "node_modules/.DS_Store",
1347 "node_modules/prettier",
1348 "node_modules/prettier/package.json",
1349 "node_modules/new_file",
1350 "build_output",
1351 "build_output/new_file",
1352 "test_output/new_file",
1353 ],
1354 &["target", "test_output"],
1355 &[
1356 ".DS_Store",
1357 "src/.DS_Store",
1358 "src/lib.rs",
1359 "src/foo/foo.rs",
1360 "src/foo/another.rs",
1361 "src/bar/bar.rs",
1362 "src/new_file",
1363 ".gitignore",
1364 ],
1365 &[],
1366 )
1367 });
1368}
1369
1370#[gpui::test]
1371async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1372 init_test(cx);
1373 cx.executor().allow_parking();
1374 let dir = TempTree::new(json!({
1375 ".git": {
1376 "HEAD": "ref: refs/heads/main\n",
1377 "foo": "foo contents",
1378 },
1379 }));
1380 let dot_git_worktree_dir = dir.path().join(".git");
1381
1382 let tree = Worktree::local(
1383 dot_git_worktree_dir.clone(),
1384 true,
1385 Arc::new(RealFs::default()),
1386 Default::default(),
1387 &mut cx.to_async(),
1388 )
1389 .await
1390 .unwrap();
1391 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1392 .await;
1393 tree.flush_fs_events(cx).await;
1394 tree.read_with(cx, |tree, _| {
1395 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1396 });
1397
1398 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1399 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1400 tree.flush_fs_events(cx).await;
1401 tree.read_with(cx, |tree, _| {
1402 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1403 });
1404}
1405
1406#[gpui::test(iterations = 30)]
1407async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1408 init_test(cx);
1409 let fs = FakeFs::new(cx.background_executor.clone());
1410 fs.insert_tree(
1411 "/root",
1412 json!({
1413 "b": {},
1414 "c": {},
1415 "d": {},
1416 }),
1417 )
1418 .await;
1419
1420 let tree = Worktree::local(
1421 "/root".as_ref(),
1422 true,
1423 fs,
1424 Default::default(),
1425 &mut cx.to_async(),
1426 )
1427 .await
1428 .unwrap();
1429
1430 let snapshot1 = tree.update(cx, |tree, cx| {
1431 let tree = tree.as_local_mut().unwrap();
1432 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1433 tree.observe_updates(0, cx, {
1434 let snapshot = snapshot.clone();
1435 let settings = tree.settings().clone();
1436 move |update| {
1437 snapshot
1438 .lock()
1439 .apply_remote_update(update, &settings.file_scan_inclusions)
1440 .unwrap();
1441 async { true }
1442 }
1443 });
1444 snapshot
1445 });
1446
1447 let entry = tree
1448 .update(cx, |tree, cx| {
1449 tree.as_local_mut()
1450 .unwrap()
1451 .create_entry("a/e".as_ref(), true, cx)
1452 })
1453 .await
1454 .unwrap()
1455 .to_included()
1456 .unwrap();
1457 assert!(entry.is_dir());
1458
1459 cx.executor().run_until_parked();
1460 tree.read_with(cx, |tree, _| {
1461 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1462 });
1463
1464 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1465 assert_eq!(
1466 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1467 snapshot2.entries(true, 0).collect::<Vec<_>>()
1468 );
1469}
1470
1471#[gpui::test]
1472async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1473 init_test(cx);
1474 cx.executor().allow_parking();
1475
1476 let fs_fake = FakeFs::new(cx.background_executor.clone());
1477 fs_fake
1478 .insert_tree(
1479 "/root",
1480 json!({
1481 "a": {},
1482 }),
1483 )
1484 .await;
1485
1486 let tree_fake = Worktree::local(
1487 "/root".as_ref(),
1488 true,
1489 fs_fake,
1490 Default::default(),
1491 &mut cx.to_async(),
1492 )
1493 .await
1494 .unwrap();
1495
1496 let entry = tree_fake
1497 .update(cx, |tree, cx| {
1498 tree.as_local_mut()
1499 .unwrap()
1500 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1501 })
1502 .await
1503 .unwrap()
1504 .to_included()
1505 .unwrap();
1506 assert!(entry.is_file());
1507
1508 cx.executor().run_until_parked();
1509 tree_fake.read_with(cx, |tree, _| {
1510 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1511 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1512 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1513 });
1514
1515 let fs_real = Arc::new(RealFs::default());
1516 let temp_root = TempTree::new(json!({
1517 "a": {}
1518 }));
1519
1520 let tree_real = Worktree::local(
1521 temp_root.path(),
1522 true,
1523 fs_real,
1524 Default::default(),
1525 &mut cx.to_async(),
1526 )
1527 .await
1528 .unwrap();
1529
1530 let entry = tree_real
1531 .update(cx, |tree, cx| {
1532 tree.as_local_mut()
1533 .unwrap()
1534 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1535 })
1536 .await
1537 .unwrap()
1538 .to_included()
1539 .unwrap();
1540 assert!(entry.is_file());
1541
1542 cx.executor().run_until_parked();
1543 tree_real.read_with(cx, |tree, _| {
1544 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1545 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1546 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1547 });
1548
1549 // Test smallest change
1550 let entry = tree_real
1551 .update(cx, |tree, cx| {
1552 tree.as_local_mut()
1553 .unwrap()
1554 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1555 })
1556 .await
1557 .unwrap()
1558 .to_included()
1559 .unwrap();
1560 assert!(entry.is_file());
1561
1562 cx.executor().run_until_parked();
1563 tree_real.read_with(cx, |tree, _| {
1564 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1565 });
1566
1567 // Test largest change
1568 let entry = tree_real
1569 .update(cx, |tree, cx| {
1570 tree.as_local_mut()
1571 .unwrap()
1572 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1573 })
1574 .await
1575 .unwrap()
1576 .to_included()
1577 .unwrap();
1578 assert!(entry.is_file());
1579
1580 cx.executor().run_until_parked();
1581 tree_real.read_with(cx, |tree, _| {
1582 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1583 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1584 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1585 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1586 });
1587}
1588
1589#[gpui::test(iterations = 100)]
1590async fn test_random_worktree_operations_during_initial_scan(
1591 cx: &mut TestAppContext,
1592 mut rng: StdRng,
1593) {
1594 init_test(cx);
1595 let operations = env::var("OPERATIONS")
1596 .map(|o| o.parse().unwrap())
1597 .unwrap_or(5);
1598 let initial_entries = env::var("INITIAL_ENTRIES")
1599 .map(|o| o.parse().unwrap())
1600 .unwrap_or(20);
1601
1602 let root_dir = Path::new(path!("/test"));
1603 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1604 fs.as_fake().insert_tree(root_dir, json!({})).await;
1605 for _ in 0..initial_entries {
1606 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1607 }
1608 log::info!("generated initial tree");
1609
1610 let worktree = Worktree::local(
1611 root_dir,
1612 true,
1613 fs.clone(),
1614 Default::default(),
1615 &mut cx.to_async(),
1616 )
1617 .await
1618 .unwrap();
1619
1620 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1621 let updates = Arc::new(Mutex::new(Vec::new()));
1622 worktree.update(cx, |tree, cx| {
1623 check_worktree_change_events(tree, cx);
1624
1625 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1626 let updates = updates.clone();
1627 move |update| {
1628 updates.lock().push(update);
1629 async { true }
1630 }
1631 });
1632 });
1633
1634 for _ in 0..operations {
1635 worktree
1636 .update(cx, |worktree, cx| {
1637 randomly_mutate_worktree(worktree, &mut rng, cx)
1638 })
1639 .await
1640 .log_err();
1641 worktree.read_with(cx, |tree, _| {
1642 tree.as_local().unwrap().snapshot().check_invariants(true)
1643 });
1644
1645 if rng.gen_bool(0.6) {
1646 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1647 }
1648 }
1649
1650 worktree
1651 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1652 .await;
1653
1654 cx.executor().run_until_parked();
1655
1656 let final_snapshot = worktree.read_with(cx, |tree, _| {
1657 let tree = tree.as_local().unwrap();
1658 let snapshot = tree.snapshot();
1659 snapshot.check_invariants(true);
1660 snapshot
1661 });
1662
1663 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1664
1665 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1666 let mut updated_snapshot = snapshot.clone();
1667 for update in updates.lock().iter() {
1668 let scan_id = match update {
1669 WorktreeRelatedMessage::UpdateWorktree(update) => update.scan_id,
1670 WorktreeRelatedMessage::UpdateRepository(update) => update.scan_id,
1671 WorktreeRelatedMessage::RemoveRepository(_) => u64::MAX,
1672 };
1673 if scan_id >= updated_snapshot.scan_id() as u64 {
1674 updated_snapshot
1675 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1676 .unwrap();
1677 }
1678 }
1679
1680 assert_eq!(
1681 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1682 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1683 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1684 );
1685 }
1686}
1687
1688#[gpui::test(iterations = 100)]
1689async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1690 init_test(cx);
1691 let operations = env::var("OPERATIONS")
1692 .map(|o| o.parse().unwrap())
1693 .unwrap_or(40);
1694 let initial_entries = env::var("INITIAL_ENTRIES")
1695 .map(|o| o.parse().unwrap())
1696 .unwrap_or(20);
1697
1698 let root_dir = Path::new(path!("/test"));
1699 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1700 fs.as_fake().insert_tree(root_dir, json!({})).await;
1701 for _ in 0..initial_entries {
1702 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1703 }
1704 log::info!("generated initial tree");
1705
1706 let worktree = Worktree::local(
1707 root_dir,
1708 true,
1709 fs.clone(),
1710 Default::default(),
1711 &mut cx.to_async(),
1712 )
1713 .await
1714 .unwrap();
1715
1716 let updates = Arc::new(Mutex::new(Vec::new()));
1717 worktree.update(cx, |tree, cx| {
1718 check_worktree_change_events(tree, cx);
1719
1720 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1721 let updates = updates.clone();
1722 move |update| {
1723 updates.lock().push(update);
1724 async { true }
1725 }
1726 });
1727 });
1728
1729 worktree
1730 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1731 .await;
1732
1733 fs.as_fake().pause_events();
1734 let mut snapshots = Vec::new();
1735 let mut mutations_len = operations;
1736 while mutations_len > 1 {
1737 if rng.gen_bool(0.2) {
1738 worktree
1739 .update(cx, |worktree, cx| {
1740 randomly_mutate_worktree(worktree, &mut rng, cx)
1741 })
1742 .await
1743 .log_err();
1744 } else {
1745 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1746 }
1747
1748 let buffered_event_count = fs.as_fake().buffered_event_count();
1749 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1750 let len = rng.gen_range(0..=buffered_event_count);
1751 log::info!("flushing {} events", len);
1752 fs.as_fake().flush_events(len);
1753 } else {
1754 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1755 mutations_len -= 1;
1756 }
1757
1758 cx.executor().run_until_parked();
1759 if rng.gen_bool(0.2) {
1760 log::info!("storing snapshot {}", snapshots.len());
1761 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1762 snapshots.push(snapshot);
1763 }
1764 }
1765
1766 log::info!("quiescing");
1767 fs.as_fake().flush_events(usize::MAX);
1768 cx.executor().run_until_parked();
1769
1770 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1771 snapshot.check_invariants(true);
1772 let expanded_paths = snapshot
1773 .expanded_entries()
1774 .map(|e| e.path.clone())
1775 .collect::<Vec<_>>();
1776
1777 {
1778 let new_worktree = Worktree::local(
1779 root_dir,
1780 true,
1781 fs.clone(),
1782 Default::default(),
1783 &mut cx.to_async(),
1784 )
1785 .await
1786 .unwrap();
1787 new_worktree
1788 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1789 .await;
1790 new_worktree
1791 .update(cx, |tree, _| {
1792 tree.as_local_mut()
1793 .unwrap()
1794 .refresh_entries_for_paths(expanded_paths)
1795 })
1796 .recv()
1797 .await;
1798 let new_snapshot =
1799 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1800 assert_eq!(
1801 snapshot.entries_without_ids(true),
1802 new_snapshot.entries_without_ids(true)
1803 );
1804 }
1805
1806 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1807
1808 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1809 for update in updates.lock().iter() {
1810 let scan_id = match update {
1811 WorktreeRelatedMessage::UpdateWorktree(update) => update.scan_id,
1812 WorktreeRelatedMessage::UpdateRepository(update) => update.scan_id,
1813 WorktreeRelatedMessage::RemoveRepository(_) => u64::MAX,
1814 };
1815 if scan_id >= prev_snapshot.scan_id() as u64 {
1816 prev_snapshot
1817 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1818 .unwrap();
1819 }
1820 }
1821
1822 assert_eq!(
1823 prev_snapshot
1824 .entries(true, 0)
1825 .map(ignore_pending_dir)
1826 .collect::<Vec<_>>(),
1827 snapshot
1828 .entries(true, 0)
1829 .map(ignore_pending_dir)
1830 .collect::<Vec<_>>(),
1831 "wrong updates after snapshot {i}: {updates:#?}",
1832 );
1833 }
1834
1835 fn ignore_pending_dir(entry: &Entry) -> Entry {
1836 let mut entry = entry.clone();
1837 if entry.kind.is_dir() {
1838 entry.kind = EntryKind::Dir
1839 }
1840 entry
1841 }
1842}
1843
1844// The worktree's `UpdatedEntries` event can be used to follow along with
1845// all changes to the worktree's snapshot.
1846fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1847 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1848 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1849 if let Event::UpdatedEntries(changes) = event {
1850 for (path, _, change_type) in changes.iter() {
1851 let entry = tree.entry_for_path(path).cloned();
1852 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1853 Ok(ix) | Err(ix) => ix,
1854 };
1855 match change_type {
1856 PathChange::Added => entries.insert(ix, entry.unwrap()),
1857 PathChange::Removed => drop(entries.remove(ix)),
1858 PathChange::Updated => {
1859 let entry = entry.unwrap();
1860 let existing_entry = entries.get_mut(ix).unwrap();
1861 assert_eq!(existing_entry.path, entry.path);
1862 *existing_entry = entry;
1863 }
1864 PathChange::AddedOrUpdated | PathChange::Loaded => {
1865 let entry = entry.unwrap();
1866 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1867 *entries.get_mut(ix).unwrap() = entry;
1868 } else {
1869 entries.insert(ix, entry);
1870 }
1871 }
1872 }
1873 }
1874
1875 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1876 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1877 }
1878 })
1879 .detach();
1880}
1881
1882fn randomly_mutate_worktree(
1883 worktree: &mut Worktree,
1884 rng: &mut impl Rng,
1885 cx: &mut Context<Worktree>,
1886) -> Task<Result<()>> {
1887 log::info!("mutating worktree");
1888 let worktree = worktree.as_local_mut().unwrap();
1889 let snapshot = worktree.snapshot();
1890 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1891
1892 match rng.gen_range(0_u32..100) {
1893 0..=33 if entry.path.as_ref() != Path::new("") => {
1894 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1895 worktree.delete_entry(entry.id, false, cx).unwrap()
1896 }
1897 ..=66 if entry.path.as_ref() != Path::new("") => {
1898 let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1899 let new_parent_path = if other_entry.is_dir() {
1900 other_entry.path.clone()
1901 } else {
1902 other_entry.path.parent().unwrap().into()
1903 };
1904 let mut new_path = new_parent_path.join(random_filename(rng));
1905 if new_path.starts_with(&entry.path) {
1906 new_path = random_filename(rng).into();
1907 }
1908
1909 log::info!(
1910 "renaming entry {:?} ({}) to {:?}",
1911 entry.path,
1912 entry.id.0,
1913 new_path
1914 );
1915 let task = worktree.rename_entry(entry.id, new_path, cx);
1916 cx.background_spawn(async move {
1917 task.await?.to_included().unwrap();
1918 Ok(())
1919 })
1920 }
1921 _ => {
1922 if entry.is_dir() {
1923 let child_path = entry.path.join(random_filename(rng));
1924 let is_dir = rng.gen_bool(0.3);
1925 log::info!(
1926 "creating {} at {:?}",
1927 if is_dir { "dir" } else { "file" },
1928 child_path,
1929 );
1930 let task = worktree.create_entry(child_path, is_dir, cx);
1931 cx.background_spawn(async move {
1932 task.await?;
1933 Ok(())
1934 })
1935 } else {
1936 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1937 let task =
1938 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1939 cx.background_spawn(async move {
1940 task.await?;
1941 Ok(())
1942 })
1943 }
1944 }
1945 }
1946}
1947
1948async fn randomly_mutate_fs(
1949 fs: &Arc<dyn Fs>,
1950 root_path: &Path,
1951 insertion_probability: f64,
1952 rng: &mut impl Rng,
1953) {
1954 log::info!("mutating fs");
1955 let mut files = Vec::new();
1956 let mut dirs = Vec::new();
1957 for path in fs.as_fake().paths(false) {
1958 if path.starts_with(root_path) {
1959 if fs.is_file(&path).await {
1960 files.push(path);
1961 } else {
1962 dirs.push(path);
1963 }
1964 }
1965 }
1966
1967 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1968 let path = dirs.choose(rng).unwrap();
1969 let new_path = path.join(random_filename(rng));
1970
1971 if rng.gen() {
1972 log::info!(
1973 "creating dir {:?}",
1974 new_path.strip_prefix(root_path).unwrap()
1975 );
1976 fs.create_dir(&new_path).await.unwrap();
1977 } else {
1978 log::info!(
1979 "creating file {:?}",
1980 new_path.strip_prefix(root_path).unwrap()
1981 );
1982 fs.create_file(&new_path, Default::default()).await.unwrap();
1983 }
1984 } else if rng.gen_bool(0.05) {
1985 let ignore_dir_path = dirs.choose(rng).unwrap();
1986 let ignore_path = ignore_dir_path.join(*GITIGNORE);
1987
1988 let subdirs = dirs
1989 .iter()
1990 .filter(|d| d.starts_with(ignore_dir_path))
1991 .cloned()
1992 .collect::<Vec<_>>();
1993 let subfiles = files
1994 .iter()
1995 .filter(|d| d.starts_with(ignore_dir_path))
1996 .cloned()
1997 .collect::<Vec<_>>();
1998 let files_to_ignore = {
1999 let len = rng.gen_range(0..=subfiles.len());
2000 subfiles.choose_multiple(rng, len)
2001 };
2002 let dirs_to_ignore = {
2003 let len = rng.gen_range(0..subdirs.len());
2004 subdirs.choose_multiple(rng, len)
2005 };
2006
2007 let mut ignore_contents = String::new();
2008 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
2009 writeln!(
2010 ignore_contents,
2011 "{}",
2012 path_to_ignore
2013 .strip_prefix(ignore_dir_path)
2014 .unwrap()
2015 .to_str()
2016 .unwrap()
2017 )
2018 .unwrap();
2019 }
2020 log::info!(
2021 "creating gitignore {:?} with contents:\n{}",
2022 ignore_path.strip_prefix(root_path).unwrap(),
2023 ignore_contents
2024 );
2025 fs.save(
2026 &ignore_path,
2027 &ignore_contents.as_str().into(),
2028 Default::default(),
2029 )
2030 .await
2031 .unwrap();
2032 } else {
2033 let old_path = {
2034 let file_path = files.choose(rng);
2035 let dir_path = dirs[1..].choose(rng);
2036 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2037 };
2038
2039 let is_rename = rng.gen();
2040 if is_rename {
2041 let new_path_parent = dirs
2042 .iter()
2043 .filter(|d| !d.starts_with(old_path))
2044 .choose(rng)
2045 .unwrap();
2046
2047 let overwrite_existing_dir =
2048 !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
2049 let new_path = if overwrite_existing_dir {
2050 fs.remove_dir(
2051 new_path_parent,
2052 RemoveOptions {
2053 recursive: true,
2054 ignore_if_not_exists: true,
2055 },
2056 )
2057 .await
2058 .unwrap();
2059 new_path_parent.to_path_buf()
2060 } else {
2061 new_path_parent.join(random_filename(rng))
2062 };
2063
2064 log::info!(
2065 "renaming {:?} to {}{:?}",
2066 old_path.strip_prefix(root_path).unwrap(),
2067 if overwrite_existing_dir {
2068 "overwrite "
2069 } else {
2070 ""
2071 },
2072 new_path.strip_prefix(root_path).unwrap()
2073 );
2074 fs.rename(
2075 old_path,
2076 &new_path,
2077 fs::RenameOptions {
2078 overwrite: true,
2079 ignore_if_exists: true,
2080 },
2081 )
2082 .await
2083 .unwrap();
2084 } else if fs.is_file(old_path).await {
2085 log::info!(
2086 "deleting file {:?}",
2087 old_path.strip_prefix(root_path).unwrap()
2088 );
2089 fs.remove_file(old_path, Default::default()).await.unwrap();
2090 } else {
2091 log::info!(
2092 "deleting dir {:?}",
2093 old_path.strip_prefix(root_path).unwrap()
2094 );
2095 fs.remove_dir(
2096 old_path,
2097 RemoveOptions {
2098 recursive: true,
2099 ignore_if_not_exists: true,
2100 },
2101 )
2102 .await
2103 .unwrap();
2104 }
2105 }
2106}
2107
2108fn random_filename(rng: &mut impl Rng) -> String {
2109 (0..6)
2110 .map(|_| rng.sample(rand::distributions::Alphanumeric))
2111 .map(char::from)
2112 .collect()
2113}
2114
2115// NOTE:
2116// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
2117// a directory which some program has already open.
2118// This is a limitation of the Windows.
2119// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2120#[gpui::test]
2121#[cfg_attr(target_os = "windows", ignore)]
2122async fn test_rename_work_directory(cx: &mut TestAppContext) {
2123 init_test(cx);
2124 cx.executor().allow_parking();
2125 let root = TempTree::new(json!({
2126 "projects": {
2127 "project1": {
2128 "a": "",
2129 "b": "",
2130 }
2131 },
2132
2133 }));
2134 let root_path = root.path();
2135
2136 let tree = Worktree::local(
2137 root_path,
2138 true,
2139 Arc::new(RealFs::default()),
2140 Default::default(),
2141 &mut cx.to_async(),
2142 )
2143 .await
2144 .unwrap();
2145
2146 let repo = git_init(&root_path.join("projects/project1"));
2147 git_add("a", &repo);
2148 git_commit("init", &repo);
2149 std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
2150
2151 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2152 .await;
2153
2154 tree.flush_fs_events(cx).await;
2155
2156 cx.read(|cx| {
2157 let tree = tree.read(cx);
2158 let repo = tree.repositories.iter().next().unwrap();
2159 assert_eq!(
2160 repo.work_directory,
2161 WorkDirectory::in_project("projects/project1")
2162 );
2163 assert_eq!(
2164 tree.status_for_file(Path::new("projects/project1/a")),
2165 Some(StatusCode::Modified.worktree()),
2166 );
2167 assert_eq!(
2168 tree.status_for_file(Path::new("projects/project1/b")),
2169 Some(FileStatus::Untracked),
2170 );
2171 });
2172
2173 std::fs::rename(
2174 root_path.join("projects/project1"),
2175 root_path.join("projects/project2"),
2176 )
2177 .unwrap();
2178 tree.flush_fs_events(cx).await;
2179
2180 cx.read(|cx| {
2181 let tree = tree.read(cx);
2182 let repo = tree.repositories.iter().next().unwrap();
2183 assert_eq!(
2184 repo.work_directory,
2185 WorkDirectory::in_project("projects/project2")
2186 );
2187 assert_eq!(
2188 tree.status_for_file(Path::new("projects/project2/a")),
2189 Some(StatusCode::Modified.worktree()),
2190 );
2191 assert_eq!(
2192 tree.status_for_file(Path::new("projects/project2/b")),
2193 Some(FileStatus::Untracked),
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_home_dir_as_git_repository(cx: &mut TestAppContext) {
2200 init_test(cx);
2201 cx.executor().allow_parking();
2202 let fs = FakeFs::new(cx.background_executor.clone());
2203 fs.insert_tree(
2204 "/root",
2205 json!({
2206 "home": {
2207 ".git": {},
2208 "project": {
2209 "a.txt": "A"
2210 },
2211 },
2212 }),
2213 )
2214 .await;
2215 fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
2216
2217 let tree = Worktree::local(
2218 Path::new(path!("/root/home/project")),
2219 true,
2220 fs.clone(),
2221 Default::default(),
2222 &mut cx.to_async(),
2223 )
2224 .await
2225 .unwrap();
2226
2227 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2228 .await;
2229 tree.flush_fs_events(cx).await;
2230
2231 tree.read_with(cx, |tree, _cx| {
2232 let tree = tree.as_local().unwrap();
2233
2234 let repo = tree.repository_for_path(path!("a.txt").as_ref());
2235 assert!(repo.is_none());
2236 });
2237
2238 let home_tree = Worktree::local(
2239 Path::new(path!("/root/home")),
2240 true,
2241 fs.clone(),
2242 Default::default(),
2243 &mut cx.to_async(),
2244 )
2245 .await
2246 .unwrap();
2247
2248 cx.read(|cx| home_tree.read(cx).as_local().unwrap().scan_complete())
2249 .await;
2250 home_tree.flush_fs_events(cx).await;
2251
2252 home_tree.read_with(cx, |home_tree, _cx| {
2253 let home_tree = home_tree.as_local().unwrap();
2254
2255 let repo = home_tree.repository_for_path(path!("project/a.txt").as_ref());
2256 assert_eq!(
2257 repo.map(|repo| &repo.work_directory),
2258 Some(&WorkDirectory::InProject {
2259 relative_path: Path::new("").into()
2260 })
2261 );
2262 })
2263}
2264
2265#[gpui::test]
2266async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2267 init_test(cx);
2268 cx.executor().allow_parking();
2269 let root = TempTree::new(json!({
2270 "c.txt": "",
2271 "dir1": {
2272 ".git": {},
2273 "deps": {
2274 "dep1": {
2275 ".git": {},
2276 "src": {
2277 "a.txt": ""
2278 }
2279 }
2280 },
2281 "src": {
2282 "b.txt": ""
2283 }
2284 },
2285 }));
2286
2287 let tree = Worktree::local(
2288 root.path(),
2289 true,
2290 Arc::new(RealFs::default()),
2291 Default::default(),
2292 &mut cx.to_async(),
2293 )
2294 .await
2295 .unwrap();
2296
2297 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2298 .await;
2299 tree.flush_fs_events(cx).await;
2300
2301 tree.read_with(cx, |tree, _cx| {
2302 let tree = tree.as_local().unwrap();
2303
2304 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2305
2306 let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2307 assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1"));
2308
2309 let repo = tree
2310 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2311 .unwrap();
2312 assert_eq!(
2313 repo.work_directory,
2314 WorkDirectory::in_project("dir1/deps/dep1")
2315 );
2316
2317 let entries = tree.files(false, 0);
2318
2319 let paths_with_repos = tree
2320 .entries_with_repositories(entries)
2321 .map(|(entry, repo)| {
2322 (
2323 entry.path.as_ref(),
2324 repo.map(|repo| repo.work_directory.clone()),
2325 )
2326 })
2327 .collect::<Vec<_>>();
2328
2329 assert_eq!(
2330 paths_with_repos,
2331 &[
2332 (Path::new("c.txt"), None),
2333 (
2334 Path::new("dir1/deps/dep1/src/a.txt"),
2335 Some(WorkDirectory::in_project("dir1/deps/dep1"))
2336 ),
2337 (
2338 Path::new("dir1/src/b.txt"),
2339 Some(WorkDirectory::in_project("dir1"))
2340 ),
2341 ]
2342 );
2343 });
2344
2345 let repo_update_events = Arc::new(Mutex::new(vec![]));
2346 tree.update(cx, |_, cx| {
2347 let repo_update_events = repo_update_events.clone();
2348 cx.subscribe(&tree, move |_, _, event, _| {
2349 if let Event::UpdatedGitRepositories(update) = event {
2350 repo_update_events.lock().push(update.clone());
2351 }
2352 })
2353 .detach();
2354 });
2355
2356 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2357 tree.flush_fs_events(cx).await;
2358
2359 assert_eq!(
2360 repo_update_events.lock()[0]
2361 .iter()
2362 .map(|(entry, _)| entry.path.clone())
2363 .collect::<Vec<Arc<Path>>>(),
2364 vec![Path::new("dir1").into()]
2365 );
2366
2367 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2368 tree.flush_fs_events(cx).await;
2369
2370 tree.read_with(cx, |tree, _cx| {
2371 let tree = tree.as_local().unwrap();
2372
2373 assert!(tree
2374 .repository_for_path("dir1/src/b.txt".as_ref())
2375 .is_none());
2376 });
2377}
2378
2379// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
2380// you can't rename a directory which some program has already open. This is a
2381// limitation of the Windows. See:
2382// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2383#[gpui::test]
2384#[cfg_attr(target_os = "windows", ignore)]
2385async fn test_file_status(cx: &mut TestAppContext) {
2386 init_test(cx);
2387 cx.executor().allow_parking();
2388 const IGNORE_RULE: &str = "**/target";
2389
2390 let root = TempTree::new(json!({
2391 "project": {
2392 "a.txt": "a",
2393 "b.txt": "bb",
2394 "c": {
2395 "d": {
2396 "e.txt": "eee"
2397 }
2398 },
2399 "f.txt": "ffff",
2400 "target": {
2401 "build_file": "???"
2402 },
2403 ".gitignore": IGNORE_RULE
2404 },
2405
2406 }));
2407
2408 const A_TXT: &str = "a.txt";
2409 const B_TXT: &str = "b.txt";
2410 const E_TXT: &str = "c/d/e.txt";
2411 const F_TXT: &str = "f.txt";
2412 const DOTGITIGNORE: &str = ".gitignore";
2413 const BUILD_FILE: &str = "target/build_file";
2414 let project_path = Path::new("project");
2415
2416 // Set up git repository before creating the worktree.
2417 let work_dir = root.path().join("project");
2418 let mut repo = git_init(work_dir.as_path());
2419 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2420 git_add(A_TXT, &repo);
2421 git_add(E_TXT, &repo);
2422 git_add(DOTGITIGNORE, &repo);
2423 git_commit("Initial commit", &repo);
2424
2425 let tree = Worktree::local(
2426 root.path(),
2427 true,
2428 Arc::new(RealFs::default()),
2429 Default::default(),
2430 &mut cx.to_async(),
2431 )
2432 .await
2433 .unwrap();
2434
2435 tree.flush_fs_events(cx).await;
2436 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2437 .await;
2438 cx.executor().run_until_parked();
2439
2440 // Check that the right git state is observed on startup
2441 tree.read_with(cx, |tree, _cx| {
2442 let snapshot = tree.snapshot();
2443 assert_eq!(snapshot.repositories.iter().count(), 1);
2444 let repo_entry = snapshot.repositories.iter().next().unwrap();
2445 assert_eq!(
2446 repo_entry.work_directory,
2447 WorkDirectory::in_project("project")
2448 );
2449
2450 assert_eq!(
2451 snapshot.status_for_file(project_path.join(B_TXT)),
2452 Some(FileStatus::Untracked),
2453 );
2454 assert_eq!(
2455 snapshot.status_for_file(project_path.join(F_TXT)),
2456 Some(FileStatus::Untracked),
2457 );
2458 });
2459
2460 // Modify a file in the working copy.
2461 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2462 tree.flush_fs_events(cx).await;
2463 cx.executor().run_until_parked();
2464
2465 // The worktree detects that the file's git status has changed.
2466 tree.read_with(cx, |tree, _cx| {
2467 let snapshot = tree.snapshot();
2468 assert_eq!(
2469 snapshot.status_for_file(project_path.join(A_TXT)),
2470 Some(StatusCode::Modified.worktree()),
2471 );
2472 });
2473
2474 // Create a commit in the git repository.
2475 git_add(A_TXT, &repo);
2476 git_add(B_TXT, &repo);
2477 git_commit("Committing modified and added", &repo);
2478 tree.flush_fs_events(cx).await;
2479 cx.executor().run_until_parked();
2480
2481 // The worktree detects that the files' git status have changed.
2482 tree.read_with(cx, |tree, _cx| {
2483 let snapshot = tree.snapshot();
2484 assert_eq!(
2485 snapshot.status_for_file(project_path.join(F_TXT)),
2486 Some(FileStatus::Untracked),
2487 );
2488 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2489 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2490 });
2491
2492 // Modify files in the working copy and perform git operations on other files.
2493 git_reset(0, &repo);
2494 git_remove_index(Path::new(B_TXT), &repo);
2495 git_stash(&mut repo);
2496 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2497 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2498 tree.flush_fs_events(cx).await;
2499 cx.executor().run_until_parked();
2500
2501 // Check that more complex repo changes are tracked
2502 tree.read_with(cx, |tree, _cx| {
2503 let snapshot = tree.snapshot();
2504
2505 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2506 assert_eq!(
2507 snapshot.status_for_file(project_path.join(B_TXT)),
2508 Some(FileStatus::Untracked),
2509 );
2510 assert_eq!(
2511 snapshot.status_for_file(project_path.join(E_TXT)),
2512 Some(StatusCode::Modified.worktree()),
2513 );
2514 });
2515
2516 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2517 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2518 std::fs::write(
2519 work_dir.join(DOTGITIGNORE),
2520 [IGNORE_RULE, "f.txt"].join("\n"),
2521 )
2522 .unwrap();
2523
2524 git_add(Path::new(DOTGITIGNORE), &repo);
2525 git_commit("Committing modified git ignore", &repo);
2526
2527 tree.flush_fs_events(cx).await;
2528 cx.executor().run_until_parked();
2529
2530 let mut renamed_dir_name = "first_directory/second_directory";
2531 const RENAMED_FILE: &str = "rf.txt";
2532
2533 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2534 std::fs::write(
2535 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2536 "new-contents",
2537 )
2538 .unwrap();
2539
2540 tree.flush_fs_events(cx).await;
2541 cx.executor().run_until_parked();
2542
2543 tree.read_with(cx, |tree, _cx| {
2544 let snapshot = tree.snapshot();
2545 assert_eq!(
2546 snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2547 Some(FileStatus::Untracked),
2548 );
2549 });
2550
2551 renamed_dir_name = "new_first_directory/second_directory";
2552
2553 std::fs::rename(
2554 work_dir.join("first_directory"),
2555 work_dir.join("new_first_directory"),
2556 )
2557 .unwrap();
2558
2559 tree.flush_fs_events(cx).await;
2560 cx.executor().run_until_parked();
2561
2562 tree.read_with(cx, |tree, _cx| {
2563 let snapshot = tree.snapshot();
2564
2565 assert_eq!(
2566 snapshot.status_for_file(
2567 project_path
2568 .join(Path::new(renamed_dir_name))
2569 .join(RENAMED_FILE)
2570 ),
2571 Some(FileStatus::Untracked),
2572 );
2573 });
2574}
2575
2576#[gpui::test]
2577async fn test_git_repository_status(cx: &mut TestAppContext) {
2578 init_test(cx);
2579 cx.executor().allow_parking();
2580
2581 let root = TempTree::new(json!({
2582 "project": {
2583 "a.txt": "a", // Modified
2584 "b.txt": "bb", // Added
2585 "c.txt": "ccc", // Unchanged
2586 "d.txt": "dddd", // Deleted
2587 },
2588
2589 }));
2590
2591 // Set up git repository before creating the worktree.
2592 let work_dir = root.path().join("project");
2593 let repo = git_init(work_dir.as_path());
2594 git_add("a.txt", &repo);
2595 git_add("c.txt", &repo);
2596 git_add("d.txt", &repo);
2597 git_commit("Initial commit", &repo);
2598 std::fs::remove_file(work_dir.join("d.txt")).unwrap();
2599 std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
2600
2601 let tree = Worktree::local(
2602 root.path(),
2603 true,
2604 Arc::new(RealFs::default()),
2605 Default::default(),
2606 &mut cx.to_async(),
2607 )
2608 .await
2609 .unwrap();
2610
2611 tree.flush_fs_events(cx).await;
2612 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2613 .await;
2614 cx.executor().run_until_parked();
2615
2616 // Check that the right git state is observed on startup
2617 tree.read_with(cx, |tree, _cx| {
2618 let snapshot = tree.snapshot();
2619 let repo = snapshot.repositories.iter().next().unwrap();
2620 let entries = repo.status().collect::<Vec<_>>();
2621
2622 assert_eq!(entries.len(), 3);
2623 assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2624 assert_eq!(entries[0].status, StatusCode::Modified.worktree());
2625 assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2626 assert_eq!(entries[1].status, FileStatus::Untracked);
2627 assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
2628 assert_eq!(entries[2].status, StatusCode::Deleted.worktree());
2629 });
2630
2631 std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
2632 eprintln!("File c.txt has been modified");
2633
2634 tree.flush_fs_events(cx).await;
2635 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2636 .await;
2637 cx.executor().run_until_parked();
2638
2639 tree.read_with(cx, |tree, _cx| {
2640 let snapshot = tree.snapshot();
2641 let repository = snapshot.repositories.iter().next().unwrap();
2642 let entries = repository.status().collect::<Vec<_>>();
2643
2644 std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
2645 assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2646 assert_eq!(entries[0].status, StatusCode::Modified.worktree());
2647 assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2648 assert_eq!(entries[1].status, FileStatus::Untracked);
2649 // Status updated
2650 assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
2651 assert_eq!(entries[2].status, StatusCode::Modified.worktree());
2652 assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
2653 assert_eq!(entries[3].status, StatusCode::Deleted.worktree());
2654 });
2655
2656 git_add("a.txt", &repo);
2657 git_add("c.txt", &repo);
2658 git_remove_index(Path::new("d.txt"), &repo);
2659 git_commit("Another commit", &repo);
2660 tree.flush_fs_events(cx).await;
2661 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2662 .await;
2663 cx.executor().run_until_parked();
2664
2665 std::fs::remove_file(work_dir.join("a.txt")).unwrap();
2666 std::fs::remove_file(work_dir.join("b.txt")).unwrap();
2667 tree.flush_fs_events(cx).await;
2668 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2669 .await;
2670 cx.executor().run_until_parked();
2671
2672 tree.read_with(cx, |tree, _cx| {
2673 let snapshot = tree.snapshot();
2674 let repo = snapshot.repositories.iter().next().unwrap();
2675 let entries = repo.status().collect::<Vec<_>>();
2676
2677 // Deleting an untracked entry, b.txt, should leave no status
2678 // a.txt was tracked, and so should have a status
2679 assert_eq!(
2680 entries.len(),
2681 1,
2682 "Entries length was incorrect\n{:#?}",
2683 &entries
2684 );
2685 assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2686 assert_eq!(entries[0].status, StatusCode::Deleted.worktree());
2687 });
2688}
2689
2690#[gpui::test]
2691async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
2692 init_test(cx);
2693 cx.executor().allow_parking();
2694
2695 let root = TempTree::new(json!({
2696 "project": {
2697 "sub": {},
2698 "a.txt": "",
2699 },
2700 }));
2701
2702 let work_dir = root.path().join("project");
2703 let repo = git_init(work_dir.as_path());
2704 // a.txt exists in HEAD and the working copy but is deleted in the index.
2705 git_add("a.txt", &repo);
2706 git_commit("Initial commit", &repo);
2707 git_remove_index("a.txt".as_ref(), &repo);
2708 // `sub` is a nested git repository.
2709 let _sub = git_init(&work_dir.join("sub"));
2710
2711 let tree = Worktree::local(
2712 root.path(),
2713 true,
2714 Arc::new(RealFs::default()),
2715 Default::default(),
2716 &mut cx.to_async(),
2717 )
2718 .await
2719 .unwrap();
2720
2721 tree.flush_fs_events(cx).await;
2722 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2723 .await;
2724 cx.executor().run_until_parked();
2725
2726 tree.read_with(cx, |tree, _cx| {
2727 let snapshot = tree.snapshot();
2728 let repo = snapshot.repositories.iter().next().unwrap();
2729 let entries = repo.status().collect::<Vec<_>>();
2730
2731 // `sub` doesn't appear in our computed statuses.
2732 assert_eq!(entries.len(), 1);
2733 assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2734 // a.txt appears with a combined `DA` status.
2735 assert_eq!(
2736 entries[0].status,
2737 TrackedStatus {
2738 index_status: StatusCode::Deleted,
2739 worktree_status: StatusCode::Added
2740 }
2741 .into()
2742 );
2743 });
2744}
2745
2746#[gpui::test]
2747async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2748 init_test(cx);
2749 cx.executor().allow_parking();
2750
2751 let root = TempTree::new(json!({
2752 "my-repo": {
2753 // .git folder will go here
2754 "a.txt": "a",
2755 "sub-folder-1": {
2756 "sub-folder-2": {
2757 "c.txt": "cc",
2758 "d": {
2759 "e.txt": "eee"
2760 }
2761 },
2762 }
2763 },
2764
2765 }));
2766
2767 const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2768 const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2769
2770 // Set up git repository before creating the worktree.
2771 let git_repo_work_dir = root.path().join("my-repo");
2772 let repo = git_init(git_repo_work_dir.as_path());
2773 git_add(C_TXT, &repo);
2774 git_commit("Initial commit", &repo);
2775
2776 // Open the worktree in subfolder
2777 let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2778 let tree = Worktree::local(
2779 root.path().join(project_root),
2780 true,
2781 Arc::new(RealFs::default()),
2782 Default::default(),
2783 &mut cx.to_async(),
2784 )
2785 .await
2786 .unwrap();
2787
2788 tree.flush_fs_events(cx).await;
2789 tree.flush_fs_events_in_root_git_repository(cx).await;
2790 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2791 .await;
2792 cx.executor().run_until_parked();
2793
2794 // Ensure that the git status is loaded correctly
2795 tree.read_with(cx, |tree, _cx| {
2796 let snapshot = tree.snapshot();
2797 assert_eq!(snapshot.repositories.iter().count(), 1);
2798 let repo = snapshot.repositories.iter().next().unwrap();
2799 assert_eq!(
2800 repo.work_directory.canonicalize(),
2801 WorkDirectory::AboveProject {
2802 absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()),
2803 location_in_repo: Arc::from(Path::new(util::separator!(
2804 "sub-folder-1/sub-folder-2"
2805 )))
2806 }
2807 );
2808
2809 assert_eq!(snapshot.status_for_file("c.txt"), None);
2810 assert_eq!(
2811 snapshot.status_for_file("d/e.txt"),
2812 Some(FileStatus::Untracked)
2813 );
2814 });
2815
2816 // Now we simulate FS events, but ONLY in the .git folder that's outside
2817 // of out project root.
2818 // Meaning: we don't produce any FS events for files inside the project.
2819 git_add(E_TXT, &repo);
2820 git_commit("Second commit", &repo);
2821 tree.flush_fs_events_in_root_git_repository(cx).await;
2822 cx.executor().run_until_parked();
2823
2824 tree.read_with(cx, |tree, _cx| {
2825 let snapshot = tree.snapshot();
2826
2827 assert!(snapshot.repositories.iter().next().is_some());
2828
2829 assert_eq!(snapshot.status_for_file("c.txt"), None);
2830 assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2831 });
2832}
2833
2834#[gpui::test]
2835async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
2836 init_test(cx);
2837 cx.executor().allow_parking();
2838
2839 let root = TempTree::new(json!({
2840 "project": {
2841 "a.txt": "a",
2842 },
2843 }));
2844 let root_path = root.path();
2845
2846 let tree = Worktree::local(
2847 root_path,
2848 true,
2849 Arc::new(RealFs::default()),
2850 Default::default(),
2851 &mut cx.to_async(),
2852 )
2853 .await
2854 .unwrap();
2855
2856 let repo = git_init(&root_path.join("project"));
2857 git_add("a.txt", &repo);
2858 git_commit("init", &repo);
2859
2860 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2861 .await;
2862
2863 tree.flush_fs_events(cx).await;
2864
2865 git_branch("other-branch", &repo);
2866 git_checkout("refs/heads/other-branch", &repo);
2867 std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
2868 git_add("a.txt", &repo);
2869 git_commit("capitalize", &repo);
2870 let commit = repo
2871 .head()
2872 .expect("Failed to get HEAD")
2873 .peel_to_commit()
2874 .expect("HEAD is not a commit");
2875 git_checkout("refs/heads/main", &repo);
2876 std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
2877 git_add("a.txt", &repo);
2878 git_commit("improve letter", &repo);
2879 git_cherry_pick(&commit, &repo);
2880 std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
2881 .expect("No CHERRY_PICK_HEAD");
2882 pretty_assertions::assert_eq!(
2883 git_status(&repo),
2884 collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
2885 );
2886 tree.flush_fs_events(cx).await;
2887 let conflicts = tree.update(cx, |tree, _| {
2888 let entry = tree.repositories.first().expect("No git entry").clone();
2889 entry
2890 .current_merge_conflicts
2891 .iter()
2892 .cloned()
2893 .collect::<Vec<_>>()
2894 });
2895 pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
2896
2897 git_add("a.txt", &repo);
2898 // Attempt to manually simulate what `git cherry-pick --continue` would do.
2899 git_commit("whatevs", &repo);
2900 std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
2901 .expect("Failed to remove CHERRY_PICK_HEAD");
2902 pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
2903 tree.flush_fs_events(cx).await;
2904 let conflicts = tree.update(cx, |tree, _| {
2905 let entry = tree.repositories.first().expect("No git entry").clone();
2906 entry
2907 .current_merge_conflicts
2908 .iter()
2909 .cloned()
2910 .collect::<Vec<_>>()
2911 });
2912 pretty_assertions::assert_eq!(conflicts, []);
2913}
2914
2915#[gpui::test]
2916async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2917 init_test(cx);
2918 let fs = FakeFs::new(cx.background_executor.clone());
2919 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2920 .await;
2921 let tree = Worktree::local(
2922 Path::new("/.env"),
2923 true,
2924 fs.clone(),
2925 Default::default(),
2926 &mut cx.to_async(),
2927 )
2928 .await
2929 .unwrap();
2930 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2931 .await;
2932 tree.read_with(cx, |tree, _| {
2933 let entry = tree.entry_for_path("").unwrap();
2934 assert!(entry.is_private);
2935 });
2936}
2937
2938#[gpui::test]
2939fn test_unrelativize() {
2940 let work_directory = WorkDirectory::in_project("");
2941 pretty_assertions::assert_eq!(
2942 work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2943 Some(Path::new("crates/gpui/gpui.rs").into())
2944 );
2945
2946 let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2947 pretty_assertions::assert_eq!(
2948 work_directory.try_unrelativize(&"src/thing.c".into()),
2949 Some(Path::new("vendor/some-submodule/src/thing.c").into())
2950 );
2951
2952 let work_directory = WorkDirectory::AboveProject {
2953 absolute_path: Path::new("/projects/zed").into(),
2954 location_in_repo: Path::new("crates/gpui").into(),
2955 };
2956
2957 pretty_assertions::assert_eq!(
2958 work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2959 None,
2960 );
2961
2962 pretty_assertions::assert_eq!(
2963 work_directory.unrelativize(&"crates/util/util.rs".into()),
2964 Path::new("../util/util.rs").into()
2965 );
2966
2967 pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2968
2969 pretty_assertions::assert_eq!(
2970 work_directory.unrelativize(&"README.md".into()),
2971 Path::new("../../README.md").into()
2972 );
2973}
2974
2975#[track_caller]
2976fn git_init(path: &Path) -> git2::Repository {
2977 let mut init_opts = RepositoryInitOptions::new();
2978 init_opts.initial_head("main");
2979 git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
2980}
2981
2982#[track_caller]
2983fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2984 let path = path.as_ref();
2985 let mut index = repo.index().expect("Failed to get index");
2986 index.add_path(path).expect("Failed to add file");
2987 index.write().expect("Failed to write index");
2988}
2989
2990#[track_caller]
2991fn git_remove_index(path: &Path, repo: &git2::Repository) {
2992 let mut index = repo.index().expect("Failed to get index");
2993 index.remove_path(path).expect("Failed to add file");
2994 index.write().expect("Failed to write index");
2995}
2996
2997#[track_caller]
2998fn git_commit(msg: &'static str, repo: &git2::Repository) {
2999 use git2::Signature;
3000
3001 let signature = Signature::now("test", "test@zed.dev").unwrap();
3002 let oid = repo.index().unwrap().write_tree().unwrap();
3003 let tree = repo.find_tree(oid).unwrap();
3004 if let Ok(head) = repo.head() {
3005 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
3006
3007 let parent_commit = parent_obj.as_commit().unwrap();
3008
3009 repo.commit(
3010 Some("HEAD"),
3011 &signature,
3012 &signature,
3013 msg,
3014 &tree,
3015 &[parent_commit],
3016 )
3017 .expect("Failed to commit with parent");
3018 } else {
3019 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
3020 .expect("Failed to commit");
3021 }
3022}
3023
3024#[track_caller]
3025fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
3026 repo.cherrypick(commit, None).expect("Failed to cherrypick");
3027}
3028
3029#[track_caller]
3030fn git_stash(repo: &mut git2::Repository) {
3031 use git2::Signature;
3032
3033 let signature = Signature::now("test", "test@zed.dev").unwrap();
3034 repo.stash_save(&signature, "N/A", None)
3035 .expect("Failed to stash");
3036}
3037
3038#[track_caller]
3039fn git_reset(offset: usize, repo: &git2::Repository) {
3040 let head = repo.head().expect("Couldn't get repo head");
3041 let object = head.peel(git2::ObjectType::Commit).unwrap();
3042 let commit = object.as_commit().unwrap();
3043 let new_head = commit
3044 .parents()
3045 .inspect(|parnet| {
3046 parnet.message();
3047 })
3048 .nth(offset)
3049 .expect("Not enough history");
3050 repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
3051 .expect("Could not reset");
3052}
3053
3054#[track_caller]
3055fn git_branch(name: &str, repo: &git2::Repository) {
3056 let head = repo
3057 .head()
3058 .expect("Couldn't get repo head")
3059 .peel_to_commit()
3060 .expect("HEAD is not a commit");
3061 repo.branch(name, &head, false).expect("Failed to commit");
3062}
3063
3064#[track_caller]
3065fn git_checkout(name: &str, repo: &git2::Repository) {
3066 repo.set_head(name).expect("Failed to set head");
3067 repo.checkout_head(None).expect("Failed to check out head");
3068}
3069
3070#[track_caller]
3071fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
3072 repo.statuses(None)
3073 .unwrap()
3074 .iter()
3075 .map(|status| (status.path().unwrap().to_string(), status.status()))
3076 .collect()
3077}
3078
3079#[track_caller]
3080fn check_worktree_entries(
3081 tree: &Worktree,
3082 expected_excluded_paths: &[&str],
3083 expected_ignored_paths: &[&str],
3084 expected_tracked_paths: &[&str],
3085 expected_included_paths: &[&str],
3086) {
3087 for path in expected_excluded_paths {
3088 let entry = tree.entry_for_path(path);
3089 assert!(
3090 entry.is_none(),
3091 "expected path '{path}' to be excluded, but got entry: {entry:?}",
3092 );
3093 }
3094 for path in expected_ignored_paths {
3095 let entry = tree
3096 .entry_for_path(path)
3097 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
3098 assert!(
3099 entry.is_ignored,
3100 "expected path '{path}' to be ignored, but got entry: {entry:?}",
3101 );
3102 }
3103 for path in expected_tracked_paths {
3104 let entry = tree
3105 .entry_for_path(path)
3106 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
3107 assert!(
3108 !entry.is_ignored || entry.is_always_included,
3109 "expected path '{path}' to be tracked, but got entry: {entry:?}",
3110 );
3111 }
3112 for path in expected_included_paths {
3113 let entry = tree
3114 .entry_for_path(path)
3115 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
3116 assert!(
3117 entry.is_always_included,
3118 "expected path '{path}' to always be included, but got entry: {entry:?}",
3119 );
3120 }
3121}
3122
3123fn init_test(cx: &mut gpui::TestAppContext) {
3124 if std::env::var("RUST_LOG").is_ok() {
3125 env_logger::try_init().ok();
3126 }
3127
3128 cx.update(|cx| {
3129 let settings_store = SettingsStore::test(cx);
3130 cx.set_global(settings_store);
3131 WorktreeSettings::register(cx);
3132 });
3133}
3134
3135#[track_caller]
3136fn assert_entry_git_state(
3137 tree: &Worktree,
3138 path: &str,
3139 index_status: Option<StatusCode>,
3140 is_ignored: bool,
3141) {
3142 let entry = tree.entry_for_path(path).expect("entry {path} not found");
3143 let status = tree.status_for_file(Path::new(path));
3144 let expected = index_status.map(|index_status| {
3145 TrackedStatus {
3146 index_status,
3147 worktree_status: StatusCode::Unmodified,
3148 }
3149 .into()
3150 });
3151 assert_eq!(
3152 status, expected,
3153 "expected {path} to have git status: {expected:?}"
3154 );
3155 assert_eq!(
3156 entry.is_ignored, is_ignored,
3157 "expected {path} to have is_ignored: {is_ignored}"
3158 );
3159}