From 4c4779871c5d961ac3aa602d8b4cb98046dead5f Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:43:17 +0000 Subject: [PATCH] Gracefully handle when linked worktree .git path is outside worktree root (#53443) (cherry-pick to preview) (#53565) Cherry-pick of #53443 to preview ---- In `update_git_repositories`, a `.git` path outside the worktree root can occur legitimately when `.git` is a gitfile (as in linked worktrees and submodules) pointing to a directory in the parent repo. Previously this triggered a `debug_panic!`, crashing debug builds. Now we skip the path with a `debug_assert!` that it is indeed a file (not a directory), so a genuine `.git` directory outside the worktree root would still be caught in debug builds. (No release notes because this is extremely hard to encounter until https://github.com/zed-industries/zed/pull/53215 lands) Release Notes: - N/A Co-authored-by: Richard Feldman --- crates/worktree/src/worktree.rs | 15 +++-- crates/worktree/tests/integration/main.rs | 76 +++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 1494ba5fce2e46bdc2d199324ee5021b19f99408..e1f15683e2d120f79fb7aaae0d8a3b5bff51f5f5 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -68,7 +68,7 @@ use std::{ use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ - ResultExt, debug_panic, maybe, + ResultExt, maybe, paths::{PathMatcher, PathStyle, SanitizedPath, home_dir}, rel_path::RelPath, }; @@ -5245,10 +5245,17 @@ impl BackgroundScanner { match existing_repository_entry { None => { let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else { - debug_panic!( - "update_git_repositories called with .git directory outside the worktree root" + // 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:?}", ); - return Vec::new(); + continue; }; affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); state diff --git a/crates/worktree/tests/integration/main.rs b/crates/worktree/tests/integration/main.rs index b8d1994b1dc3f8ddbd482dd0863e3441ab7adc64..633a04ad7ac1b7cb0aea93ddcc60ca38fba5fe98 100644 --- a/crates/worktree/tests/integration/main.rs +++ b/crates/worktree/tests/integration/main.rs @@ -2827,6 +2827,82 @@ async fn test_root_repo_common_dir(executor: BackgroundExecutor, cx: &mut TestAp ); } +#[gpui::test] +async fn test_linked_worktree_git_file_event_does_not_panic( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + // Regression test: in a linked worktree, `.git` is a file (containing + // "gitdir: ..."), not a directory. When the background scanner receives + // a filesystem event for a path inside the main repo's `.git` directory + // (which it watches via the commondir), the ancestor-walking code in + // `process_events` calls `is_git_dir` on each ancestor. If `is_git_dir` + // treats `.git` files the same as `.git` directories, it incorrectly + // identifies the gitfile as a git dir, adds it to `dot_git_abs_paths`, + // and `update_git_repositories` panics because the path is outside the + // worktree root. + 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, + }, + ) + .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(); + + // Trigger a filesystem event inside the main repo's .git directory + // (which the linked worktree scanner watches via the commondir). This + // uses the sentinel-file helper to ensure the event goes through the + // real watcher path, exactly as it would in production. + tree.flush_fs_events_in_root_git_repository(cx).await; + + // The worktree should still be intact. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.snapshot().root_repo_common_dir().map(|p| p.as_ref()), + Some(Path::new(path!("/main_repo/.git"))), + ); + }); +} + fn init_test(cx: &mut gpui::TestAppContext) { zlog::init_test();