diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 8d66c375a24168e82af11e3eac022366fe288f1c..55d40e1416307622f71c244dc12e79884a76bbac 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -5318,16 +5318,13 @@ impl BackgroundScanner { match existing_repository_entry { None => { let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else { - // This can happen legitimately when `.git` is a - // gitfile (e.g. in a linked worktree or submodule) - // pointing to a directory outside the worktree root. - // Skip it — the repository was already registered - // during the initial scan via `discover_git_paths`. - debug_assert!( - self.fs.is_file(&dot_git_dir).await, - "update_git_repositories: .git path outside worktree root \ - is not a gitfile: {dot_git_dir:?}", - ); + // A `.git` path outside the worktree root is not + // ours to register. This happens legitimately when + // `.git` is a gitfile pointing outside the worktree + // (linked worktrees and submodules), and also when + // a rescan of a linked worktree's commondir arrives + // after the worktree's repository has already been + // unregistered. continue; }; affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); diff --git a/crates/worktree/tests/integration/main.rs b/crates/worktree/tests/integration/main.rs index 76034c7f5fa01cd50f254fec241ed830c5f90b08..8b6a03fd1f02e725dc119283007798745ffdcaed 100644 --- a/crates/worktree/tests/integration/main.rs +++ b/crates/worktree/tests/integration/main.rs @@ -2913,6 +2913,78 @@ async fn test_linked_worktree_git_file_event_does_not_panic( }); } +#[gpui::test] +async fn test_linked_worktree_event_in_unregistered_common_git_dir_does_not_panic( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + // Regression test: a rescan event on a linked worktree's commondir + // must not panic when the worktree's repository has already been + // unregistered from `git_repositories`. + init_test(cx); + + use git::repository::Worktree as GitWorktree; + + let fs = FakeFs::new(executor); + + fs.insert_tree( + path!("/main_repo"), + json!({ + ".git": {}, + "file.txt": "content", + }), + ) + .await; + fs.add_linked_worktree_for_repo( + Path::new(path!("/main_repo/.git")), + false, + GitWorktree { + path: PathBuf::from(path!("/linked_worktree")), + ref_name: Some("refs/heads/feature".into()), + sha: "abc123".into(), + is_main: false, + is_bare: false, + }, + ) + .await; + fs.write( + path!("/linked_worktree/file.txt").as_ref(), + "content".as_bytes(), + ) + .await + .unwrap(); + + let tree = Worktree::local( + path!("/linked_worktree").as_ref(), + true, + fs.clone(), + Arc::default(), + true, + WorktreeId::from_proto(0), + &mut cx.to_async(), + ) + .await + .unwrap(); + tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + cx.run_until_parked(); + + // Unregister the linked worktree's repository by removing its gitfile. + fs.remove_file( + Path::new(path!("/linked_worktree/.git")), + Default::default(), + ) + .await + .unwrap(); + tree.flush_fs_events(cx).await; + + // Deliver the kind of Rescan event `FsWatcher` emits when the kernel + // signals `need_rescan` for the commondir. + fs.emit_fs_event(path!("/main_repo/.git"), Some(fs::PathEventKind::Rescan)); + cx.run_until_parked(); + tree.flush_fs_events(cx).await; +} + fn init_test(cx: &mut gpui::TestAppContext) { zlog::init_test();