diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index bc60e8a2fd..7e78f513e3 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1333,13 +1333,23 @@ impl FakeFs { let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else { anyhow::bail!("pointed-to git dir {path:?} not found") }; - let FakeFsEntry::Dir { git_repo_state, .. } = &mut *git_dir_entry.lock() else { + let FakeFsEntry::Dir { + git_repo_state, + entries, + .. + } = &mut *git_dir_entry.lock() + else { anyhow::bail!("gitfile points to a non-directory") }; - let common_dir = canonical_path - .ancestors() - .find(|ancestor| ancestor.ends_with(".git")) - .ok_or_else(|| anyhow!("repository dir not contained in any .git"))?; + let common_dir = if let Some(child) = entries.get("commondir") { + Path::new( + std::str::from_utf8(child.lock().file_content("commondir".as_ref())?) + .context("commondir content")?, + ) + .to_owned() + } else { + canonical_path.clone() + }; let repo_state = git_repo_state.get_or_insert_with(|| { Arc::new(Mutex::new(FakeGitRepositoryState::new( state.git_event_tx.clone(), @@ -1347,7 +1357,7 @@ impl FakeFs { }); let mut repo_state = repo_state.lock(); - let result = f(&mut repo_state, &canonical_path, common_dir); + let result = f(&mut repo_state, &canonical_path, &common_dir); if emit_git_event { state.emit_event([(canonical_path, None)]); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a8870c4e45..d54eaa6e55 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -8273,17 +8273,34 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) { json!({ ".git": { "worktrees": { - "some-worktree": {} + "some-worktree": { + "commondir": "../..\n" + } }, + "modules": { + "subdir": { + "some-submodule": { + // For is_git_dir + "HEAD": "", + "config": "", + } + } + } }, "src": { "a.txt": "A", }, "some-worktree": { - ".git": "gitdir: ../.git/worktrees/some-worktree", + ".git": "gitdir: ../.git/worktrees/some-worktree\n", "src": { "b.txt": "B", } + }, + "subdir": { + "some-submodule": { + ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n", + "c.txt": "C", + } } }), ) @@ -8315,9 +8332,11 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) { [ Path::new(path!("/project")).into(), Path::new(path!("/project/some-worktree")).into(), + Path::new(path!("/project/subdir/some-submodule")).into(), ] ); + // Generate a git-related event for the worktree and check that it's refreshed. fs.with_git_state( path!("/project/some-worktree/.git").as_ref(), true, @@ -8359,6 +8378,45 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) { StatusCode::Modified.worktree(), ); }); + + // The same for the submodule. + fs.with_git_state( + path!("/project/subdir/some-submodule/.git").as_ref(), + true, + |state| { + state.head_contents.insert("c.txt".into(), "c".to_owned()); + state.index_contents.insert("c.txt".into(), "c".to_owned()); + }, + ) + .unwrap(); + cx.run_until_parked(); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx) + }) + .await + .unwrap(); + let (submodule_repo, barrier) = project.update(cx, |project, cx| { + let (repo, _) = project + .git_store() + .read(cx) + .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) + .unwrap(); + pretty_assertions::assert_eq!( + repo.read(cx).work_directory_abs_path, + Path::new(path!("/project/subdir/some-submodule")).into(), + ); + let barrier = repo.update(cx, |repo, _| repo.barrier()); + (repo.clone(), barrier) + }); + barrier.await.unwrap(); + submodule_repo.update(cx, |repo, _| { + pretty_assertions::assert_eq!( + repo.status_for_path(&"c.txt".into()).unwrap().status, + StatusCode::Modified.worktree(), + ); + }); } #[gpui::test] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index fa4bf202d8..c85fc4e930 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3120,32 +3120,12 @@ impl BackgroundScannerState { .as_path() .into(); - let mut common_dir_abs_path = dot_git_abs_path.clone(); - let mut repository_dir_abs_path = dot_git_abs_path.clone(); - // Parse .git if it's a "gitfile" pointing to a repository directory elsewhere. - if let Some(dot_git_contents) = smol::block_on(fs.load(&dot_git_abs_path)).ok() { - if let Some(path) = dot_git_contents.strip_prefix("gitdir:") { - let path = path.trim(); - let path = dot_git_abs_path - .parent() - .unwrap_or(Path::new("")) - .join(path); - if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() { - repository_dir_abs_path = Path::new(&path).into(); - common_dir_abs_path = repository_dir_abs_path.clone(); - if let Some(ancestor_dot_git) = path - .ancestors() - .skip(1) - .find(|ancestor| smol::block_on(is_git_dir(ancestor, fs))) - { - common_dir_abs_path = ancestor_dot_git.into(); - } - } - } else { - log::error!("failed to parse contents of .git file: {dot_git_contents:?}"); - } - }; + let (repository_dir_abs_path, common_dir_abs_path) = + discover_git_paths(&dot_git_abs_path, fs); watcher.add(&common_dir_abs_path).log_err(); + if !repository_dir_abs_path.starts_with(&common_dir_abs_path) { + watcher.add(&repository_dir_abs_path).log_err(); + } let work_directory_id = work_dir_entry.id; @@ -5508,3 +5488,40 @@ impl CreatedEntry { } } } + +fn parse_gitfile(content: &str) -> anyhow::Result<&Path> { + let path = content + .strip_prefix("gitdir:") + .ok_or_else(|| anyhow!("failed to parse gitfile content {content:?}"))?; + Ok(Path::new(path.trim())) +} + +fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, Arc) { + let mut repository_dir_abs_path = dot_git_abs_path.clone(); + let mut common_dir_abs_path = dot_git_abs_path.clone(); + + if let Some(path) = smol::block_on(fs.load(&dot_git_abs_path)) + .ok() + .as_ref() + .and_then(|contents| parse_gitfile(contents).log_err()) + { + let path = dot_git_abs_path + .parent() + .unwrap_or(Path::new("")) + .join(path); + if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() { + repository_dir_abs_path = Path::new(&path).into(); + common_dir_abs_path = repository_dir_abs_path.clone(); + if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok() + { + if let Some(commondir_path) = + smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err() + { + common_dir_abs_path = commondir_path.as_path().into(); + } + } + } + }; + + (repository_dir_abs_path, common_dir_abs_path) +}