From 358d88d02f0176217281e80744807b89e2ad674c Mon Sep 17 00:00:00 2001 From: Om Chillure Date: Tue, 5 May 2026 21:04:56 +0530 Subject: [PATCH] Fix git worktree popup popup no worktree when opened in a project (#55053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the `git: worktree` popup showing no worktrees when a project is opened at the parent of a `.bare` directory (the common bare-clone-with-sibling-worktrees layout). ## What's fixed - `crates/git/src/repository.rs` - New `git_binary_for_worktree_list` helper that uses `repository.path()` as the working directory when `workdir()` is `None`. - `worktrees()` switched to the new helper. - `parse_worktrees_from_str` accepts bare entries without a `HEAD` line. - Tests - Unit test: parser handles a bare entry with no `HEAD` followed by a normal worktree entry. - Integration test: full `.git`-file → `.bare` + sibling worktrees layout (`main`, `feature-a`, `feature-b`) is listed correctly via the real `git` binary. UI rendering already gates on empty sha (`worktree_picker.rs` uses `.when(!sha.is_empty(), ...)`), so the bare entry's empty sha renders without artifacts. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments — N/A, no `unsafe` - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable — same single `git worktree list --porcelain` invocation, no extra work #### Closes #54824 Video [Screencast from 2026-04-28 09-43-45.webm](https://github.com/user-attachments/assets/e414d546-eb61-4cb2-857e-3c392f416f96) Release Notes: - Fixed the `git: worktree` popup listing no worktrees when a project was opened at the parent of a `.bare` directory (bare-clone-with-sibling-worktrees layout). --------- Co-authored-by: Max Brunsfeld --- crates/git/src/repository.rs | 142 +++++++++++++++++------------------ 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index d98e917d69c..90ac06d959a 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -27,7 +27,6 @@ use std::process::ExitStatus; use std::str::FromStr; use std::{ cmp::Ordering, - future, path::{Path, PathBuf}, sync::Arc, }; @@ -1089,7 +1088,7 @@ impl RealGitRepository { .map(Path::to_path_buf) } - fn git_binary(&self) -> Result { + fn git_binary_in_worktree(&self) -> Result { Ok(GitBinary::new( self.any_git_binary_path.clone(), self.working_directory() @@ -1100,12 +1099,27 @@ impl RealGitRepository { )) } + fn git_binary(&self) -> GitBinary { + let repository = self.repository.lock(); + let working_directory = repository + .workdir() + .unwrap_or_else(|| repository.path()) + .to_path_buf(); + GitBinary::new( + self.any_git_binary_path.clone(), + working_directory, + repository.path().to_path_buf(), + self.executor.clone(), + self.is_trusted(), + ) + } + fn edit_ref(&self, edit: RefEdit) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { let args = edit.into_args(); - git_binary?.run(&args).await?; + git.run(&args).await?; Ok(()) }) .boxed() @@ -1115,10 +1129,10 @@ impl RealGitRepository { if let Some(output) = self.any_git_binary_help_output.lock().clone() { return output; } - let git_binary = self.git_binary(); + let git = self.git_binary(); let output: SharedString = self .executor - .spawn(async move { git_binary?.run(&["help", "-a"]).await }) + .spawn(async move { git.run(&["help", "-a"]).await }) .await .unwrap_or_default() .into(); @@ -1202,10 +1216,9 @@ impl GitRepository for RealGitRepository { } fn show(&self, commit: String) -> BoxFuture<'_, Result> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let output = git .build_command(&[ "show", @@ -1237,12 +1250,8 @@ impl GitRepository for RealGitRepository { } fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result> { - if self.repository.lock().workdir().is_none() { - return future::ready(Err(anyhow!("no working directory"))).boxed(); - } - let git_binary = self.git_binary(); + let git = self.git_binary(); cx.background_spawn(async move { - let git = git_binary?; let show_output = git .build_command(&[ "show", @@ -1372,7 +1381,7 @@ impl GitRepository for RealGitRepository { mode: ResetMode, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); async move { let mode_flag = match mode { ResetMode::Mixed => "--mixed", @@ -1401,7 +1410,7 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); async move { if paths.is_empty() { return Ok(()); @@ -1557,10 +1566,9 @@ impl GitRepository for RealGitRepository { env: Arc>, is_executable: bool, ) -> BoxFuture<'_, anyhow::Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let mode = if is_executable { "100755" } else { "100644" }; if let Some(content) = content { @@ -1624,10 +1632,9 @@ impl GitRepository for RealGitRepository { } fn revparse_batch(&self, revs: Vec) -> BoxFuture<'_, Result>>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let mut process = git .build_command(&["cat-file", "--batch-check=%(objectname)"]) .stdin(Stdio::piped()) @@ -1678,7 +1685,7 @@ impl GitRepository for RealGitRepository { } fn status(&self, path_prefixes: &[RepoPath]) -> Task> { - let git = match self.git_binary() { + let git = match self.git_binary_in_worktree() { Ok(git) => git, Err(e) => return Task::ready(Err(e)), }; @@ -1697,7 +1704,7 @@ impl GitRepository for RealGitRepository { } fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result> { - let git = match self.git_binary() { + let git = match self.git_binary_in_worktree() { Ok(git) => git, Err(e) => return Task::ready(Err(e)).boxed(), }; @@ -1735,7 +1742,7 @@ impl GitRepository for RealGitRepository { } fn stash_entries(&self) -> BoxFuture<'_, Result> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -1755,7 +1762,7 @@ impl GitRepository for RealGitRepository { } fn branches(&self) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { let fields = [ @@ -1777,7 +1784,6 @@ impl GitRepository for RealGitRepository { "--format", &fields, ]; - let git = git_binary?; let output = git.build_command(&args).output().await?; anyhow::ensure!( @@ -1814,7 +1820,7 @@ impl GitRepository for RealGitRepository { } fn worktrees(&self) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); let main_worktree_path = { let repo = self.repository.lock(); let common_dir = repo.commondir().to_path_buf(); @@ -1822,7 +1828,6 @@ impl GitRepository for RealGitRepository { }; self.executor .spawn(async move { - let git = git_binary?; let output = git .build_command(&["worktree", "list", "--porcelain"]) .output() @@ -1846,7 +1851,7 @@ impl GitRepository for RealGitRepository { target: CreateWorktreeTarget, path: PathBuf, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); let mut args = vec![OsString::from("worktree"), OsString::from("add")]; match &target { @@ -1878,7 +1883,6 @@ impl GitRepository for RealGitRepository { self.executor .spawn(async move { std::fs::create_dir_all(path.parent().unwrap_or(&path))?; - let git = git_binary?; let output = git.build_command(&args).output().await?; if output.status.success() { Ok(()) @@ -1891,7 +1895,7 @@ impl GitRepository for RealGitRepository { } fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { @@ -1901,14 +1905,14 @@ impl GitRepository for RealGitRepository { } args.push("--".into()); args.push(path.as_os_str().into()); - git_binary?.run(&args).await?; + git.run(&args).await?; anyhow::Ok(()) }) .boxed() } fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { @@ -1919,7 +1923,7 @@ impl GitRepository for RealGitRepository { old_path.as_os_str().into(), new_path.as_os_str().into(), ]; - git_binary?.run(&args).await?; + git.run(&args).await?; anyhow::Ok(()) }) .boxed() @@ -1953,7 +1957,7 @@ impl GitRepository for RealGitRepository { fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { let repo = self.repository.clone(); - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); let branch = self.executor.spawn(async move { let repo = repo.lock(); let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) { @@ -1999,7 +2003,7 @@ impl GitRepository for RealGitRepository { name: String, base_branch: Option, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2017,7 +2021,7 @@ impl GitRepository for RealGitRepository { } fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2030,7 +2034,7 @@ impl GitRepository for RealGitRepository { } fn delete_branch(&self, is_remote: bool, name: String) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2048,7 +2052,7 @@ impl GitRepository for RealGitRepository { content: Rope, line_ending: LineEnding, ) -> BoxFuture<'_, Result> { - let git = self.git_binary(); + let git = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2058,7 +2062,7 @@ impl GitRepository for RealGitRepository { } fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2089,7 +2093,7 @@ impl GitRepository for RealGitRepository { path_prefixes: &[RepoPath], ) -> BoxFuture<'_, Result> { let path_prefixes = path_prefixes.to_vec(); - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2119,7 +2123,7 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { if !paths.is_empty() { @@ -2146,7 +2150,7 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { @@ -2175,7 +2179,7 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2201,7 +2205,7 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2226,7 +2230,7 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2251,7 +2255,7 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2279,7 +2283,7 @@ impl GitRepository for RealGitRepository { ask_pass: AskPassDelegate, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); let executor = self.executor.clone(); // Note: Do not spawn this command on the background thread, it might pop open the credential helper // which we want to block on. @@ -2325,11 +2329,11 @@ impl GitRepository for RealGitRepository { } fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { let args: Vec = vec!["worktree".into(), "repair".into()]; - git_binary?.run(&args).await?; + git.run(&args).await?; Ok(()) }) .boxed() @@ -2431,7 +2435,7 @@ impl GitRepository for RealGitRepository { env: Arc>, cx: AsyncApp, ) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); + let working_directory = self.working_directory().unwrap_or(self.path()); let git_directory = self.path(); let remote_name = format!("{}", fetch_options); let git_binary_path = self.system_git_binary_path.clone(); @@ -2441,7 +2445,6 @@ impl GitRepository for RealGitRepository { // which we want to block on. async move { let git_binary_path = git_binary_path.context("git not found on $PATH, can't fetch")?; - let working_directory = working_directory?; let git = GitBinary::new( git_binary_path, working_directory, @@ -2461,10 +2464,9 @@ impl GitRepository for RealGitRepository { } fn get_push_remote(&self, branch: String) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let output = git .build_command(&["rev-parse", "--abbrev-ref"]) .arg(format!("{branch}@{{push}}")) @@ -2486,10 +2488,9 @@ impl GitRepository for RealGitRepository { } fn get_branch_remote(&self, branch: String) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let output = git .build_command(&["config", "--get"]) .arg(format!("branch.{branch}.remote")) @@ -2508,10 +2509,9 @@ impl GitRepository for RealGitRepository { } fn get_all_remotes(&self) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; let output = git.build_command(&["remote", "-v"]).output().await?; anyhow::ensure!( @@ -2561,7 +2561,7 @@ impl GitRepository for RealGitRepository { } fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2615,7 +2615,7 @@ impl GitRepository for RealGitRepository { } fn checkpoint(&self) -> BoxFuture<'static, Result> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let mut git = git_binary?.envs(checkpoint_author_envs()); @@ -2644,7 +2644,7 @@ impl GitRepository for RealGitRepository { } fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2674,7 +2674,7 @@ impl GitRepository for RealGitRepository { } fn create_archive_checkpoint(&self) -> BoxFuture<'_, Result<(String, String)>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let mut git = git_binary?.envs(checkpoint_author_envs()); @@ -2732,7 +2732,7 @@ impl GitRepository for RealGitRepository { staged_sha: String, unstaged_sha: String, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2762,7 +2762,7 @@ impl GitRepository for RealGitRepository { left: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2796,7 +2796,7 @@ impl GitRepository for RealGitRepository { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { let git = git_binary?; @@ -2816,11 +2816,9 @@ impl GitRepository for RealGitRepository { &self, include_remote_name: bool, ) -> BoxFuture<'_, Result>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); self.executor .spawn(async move { - let git = git_binary?; - let strip_prefix = if include_remote_name { "refs/remotes/" } else { @@ -2869,7 +2867,7 @@ impl GitRepository for RealGitRepository { hook: RunHook, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); let repository = self.repository.clone(); let help_output = self.any_git_binary_help_output(); @@ -2922,11 +2920,9 @@ impl GitRepository for RealGitRepository { log_order: LogOrder, request_tx: Sender>>, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); async move { - let git = git_binary?; - let mut git_log_command = vec![ "log", GRAPH_COMMIT_FORMAT, @@ -3004,11 +3000,9 @@ impl GitRepository for RealGitRepository { search_args: SearchCommitArgs, request_tx: Sender, ) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git = self.git_binary(); async move { - let git = git_binary?; - let mut args = vec!["log", SEARCH_COMMIT_FORMAT, log_source.get_arg()?]; args.push("--fixed-strings"); @@ -3058,7 +3052,7 @@ impl GitRepository for RealGitRepository { } fn commit_data_reader(&self) -> Result { - let git_binary = self.git_binary()?; + let git_binary = self.git_binary(); let (request_tx, request_rx) = async_channel::bounded::(64);