From 679d9a154de4ac7318b9736f25ea334fa5313e57 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 17:36:50 +0800 Subject: [PATCH 1/2] fix: skip workdir status when bare repo has no worktree Cross-linked bare repositories (separate-git-dir) are not inside a work tree, so Git CLI refuses status. Return an empty workdir/both status list instead of treating gitdir files as unstaged changes (#2867). --- asyncgit/src/sync/status.rs | 133 +++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 8afdf2dd09..e0043f19a2 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -175,13 +175,21 @@ pub fn get_status( ) -> Result> { scope_time!("get_status"); + let git2_repo = crate::sync::repository::repo(repo_path)?; + + // Bare repos without a linked worktree cannot run worktree status (see #2867). + if matches!(status_type, StatusType::WorkingDir | StatusType::Both) + && git2_repo.is_bare() + && !git2_repo.is_worktree() + { + return Ok(Vec::new()); + } + let repo: gix::Repository = gix_repo(repo_path)?; let show_untracked = if let Some(config) = show_untracked { config } else { - let git2_repo = crate::sync::repository::repo(repo_path)?; - // Calling `untracked_files_config_repo` ensures compatibility with `gitui` <= 0.27. // `untracked_files_config_repo` defaults to `All` while both `libgit2` and `gix` default to // `Normal`. According to [show-untracked-files], `normal` is the default value that `git` @@ -368,4 +376,125 @@ mod tests { }] ); } + + /// Regression for https://github.com/gitui-org/gitui/issues/2876 + #[test] + fn test_info_exclude_honored_in_linked_worktree() { + use std::process::Command; + + let (_td, repo) = repo_init().unwrap(); + let main_root = repo.workdir().unwrap().to_path_buf(); + let main_repo_path: RepoPath = + main_root.as_os_str().to_str().unwrap().into(); + + let exclude = main_root.join(".git/info/exclude"); + std::fs::write(&exclude, ".cache/\n").unwrap(); + + let linked_td = TempDir::new().unwrap(); + let linked_root = linked_td.path().to_path_buf(); + let status = Command::new("git") + .args([ + "worktree", + "add", + "-b", + "wt", + linked_root.to_str().unwrap(), + "HEAD", + ]) + .current_dir(&main_root) + .status() + .unwrap(); + assert!(status.success()); + + let cache_dir = linked_root.join(".cache"); + std::fs::create_dir_all(&cache_dir).unwrap(); + std::fs::write(cache_dir.join("foo"), "clangd").unwrap(); + + let main_status = get_status( + &main_repo_path, + StatusType::WorkingDir, + None, + ) + .unwrap(); + assert!( + main_status.is_empty(), + "main worktree should honor info/exclude: {main_status:?}" + ); + + let linked_repo_path: RepoPath = + linked_root.as_os_str().to_str().unwrap().into(); + let linked_status = get_status( + &linked_repo_path, + StatusType::WorkingDir, + None, + ) + .unwrap(); + assert!( + linked_status.is_empty(), + "linked worktree should honor info/exclude: {linked_status:?}" + ); + + assert!( + is_workdir_clean(&linked_repo_path, None).unwrap(), + "libgit2 status should honor info/exclude in linked worktree" + ); + + // Simulate explicit GIT_DIR + GIT_WORK_TREE (bare-style open). + let gitdir = std::fs::read_to_string(linked_root.join(".git")) + .unwrap() + .trim() + .strip_prefix("gitdir: ") + .unwrap() + .to_string(); + let explicit_path = RepoPath::Workdir { + gitdir: gitdir.into(), + workdir: linked_root.clone(), + }; + let explicit_status = get_status( + &explicit_path, + StatusType::WorkingDir, + None, + ) + .unwrap(); + assert!( + explicit_status.is_empty(), + "explicit worktree gitdir should honor info/exclude: {explicit_status:?}" + ); + } + + #[test] + fn test_crosslinked_bare_repo_no_workdir_status() { + use crate::sync::tests::debug_cmd_print; + use std::fs; + + let base = TempDir::new().unwrap(); + let repo0 = base.path().join("repo0"); + let repo1 = base.path().join("repo1"); + fs::create_dir_all(&repo0).unwrap(); + fs::create_dir_all(&repo1).unwrap(); + + let repo0_path: &RepoPath = + &repo0.as_os_str().to_str().unwrap().into(); + + debug_cmd_print(repo0_path, "git init --bare"); + debug_cmd_print(repo0_path, "git init --separate-git-dir=../repo1"); + + let repo1_path: &RepoPath = + &repo1.as_os_str().to_str().unwrap().into(); + + debug_cmd_print(repo1_path, "git init --bare"); + debug_cmd_print(repo1_path, "git init --separate-git-dir=../repo0"); + + let status = get_status( + repo0_path, + StatusType::WorkingDir, + None, + ) + .unwrap(); + + assert!( + status.is_empty(), + "bare cross-linked repo should not report workdir files: {status:?}" + ); + } } From 2d3223004130d415ed7a4aa4d1848d4c745c4ed9 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 17:38:24 +0800 Subject: [PATCH 2/2] chore: drop unrelated worktree exclude test from #2867 branch Keeps PR #2938 scoped to bare cross-linked repo status only (#2876 is covered separately). --- asyncgit/src/sync/status.rs | 85 ------------------------------------- 1 file changed, 85 deletions(-) diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index e0043f19a2..737e25dfca 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -377,91 +377,6 @@ mod tests { ); } - /// Regression for https://github.com/gitui-org/gitui/issues/2876 - #[test] - fn test_info_exclude_honored_in_linked_worktree() { - use std::process::Command; - - let (_td, repo) = repo_init().unwrap(); - let main_root = repo.workdir().unwrap().to_path_buf(); - let main_repo_path: RepoPath = - main_root.as_os_str().to_str().unwrap().into(); - - let exclude = main_root.join(".git/info/exclude"); - std::fs::write(&exclude, ".cache/\n").unwrap(); - - let linked_td = TempDir::new().unwrap(); - let linked_root = linked_td.path().to_path_buf(); - let status = Command::new("git") - .args([ - "worktree", - "add", - "-b", - "wt", - linked_root.to_str().unwrap(), - "HEAD", - ]) - .current_dir(&main_root) - .status() - .unwrap(); - assert!(status.success()); - - let cache_dir = linked_root.join(".cache"); - std::fs::create_dir_all(&cache_dir).unwrap(); - std::fs::write(cache_dir.join("foo"), "clangd").unwrap(); - - let main_status = get_status( - &main_repo_path, - StatusType::WorkingDir, - None, - ) - .unwrap(); - assert!( - main_status.is_empty(), - "main worktree should honor info/exclude: {main_status:?}" - ); - - let linked_repo_path: RepoPath = - linked_root.as_os_str().to_str().unwrap().into(); - let linked_status = get_status( - &linked_repo_path, - StatusType::WorkingDir, - None, - ) - .unwrap(); - assert!( - linked_status.is_empty(), - "linked worktree should honor info/exclude: {linked_status:?}" - ); - - assert!( - is_workdir_clean(&linked_repo_path, None).unwrap(), - "libgit2 status should honor info/exclude in linked worktree" - ); - - // Simulate explicit GIT_DIR + GIT_WORK_TREE (bare-style open). - let gitdir = std::fs::read_to_string(linked_root.join(".git")) - .unwrap() - .trim() - .strip_prefix("gitdir: ") - .unwrap() - .to_string(); - let explicit_path = RepoPath::Workdir { - gitdir: gitdir.into(), - workdir: linked_root.clone(), - }; - let explicit_status = get_status( - &explicit_path, - StatusType::WorkingDir, - None, - ) - .unwrap(); - assert!( - explicit_status.is_empty(), - "explicit worktree gitdir should honor info/exclude: {explicit_status:?}" - ); - } - #[test] fn test_crosslinked_bare_repo_no_workdir_status() { use crate::sync::tests::debug_cmd_print;