diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 01:16:42 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 01:16:42 +0300 |
| commit | 5d7e56737ef30d9de5802b0684a9428c40131dda (patch) | |
| tree | a5c3c5870fc4686fd9bb24c819465e8f8bcc33f6 /src/workspace | |
| parent | 4001fd2ec8ff05a982da18744b8cbde9df3ab530 (diff) | |
fix: support worktree actions from bare repos
refs #497
Diffstat (limited to 'src/workspace')
| -rw-r--r-- | src/workspace/git/discovery.rs | 95 |
1 files changed, 83 insertions, 12 deletions
diff --git a/src/workspace/git/discovery.rs b/src/workspace/git/discovery.rs index 7e973bb9..9aa351b2 100644 --- a/src/workspace/git/discovery.rs +++ b/src/workspace/git/discovery.rs @@ -44,12 +44,13 @@ pub fn git_worktree_info(cwd: &Path) -> Option<GitWorktreeInfo> { let git_dir = canonicalize_best_effort_path(&git_dir_for_repo_root(&repo_root)?); let git_common_dir = canonicalize_best_effort_path(&git_common_dir_for_git_dir(&git_dir)); let is_linked_worktree = git_dir != git_common_dir; + let is_bare = git_dir_is_bare(&git_dir); Some(GitWorktreeInfo { repo_root, git_dir, git_common_dir, - is_bare: false, + is_bare, is_linked_worktree, }) } @@ -58,9 +59,6 @@ pub fn git_space_metadata(cwd: &Path) -> Option<GitSpaceMetadata> { git_repo_root(cwd)?; let info = git_worktree_info(cwd)?; - if info.is_bare { - return None; - } let key = canonicalize_best_effort_path(&info.git_common_dir) .display() .to_string(); @@ -126,14 +124,26 @@ pub(super) fn git_dir_for_repo_root(repo_root: &Path) -> Option<PathBuf> { return Some(git_path); } - let gitdir = std::fs::read_to_string(&git_path).ok()?; - let relative = gitdir.trim().strip_prefix("gitdir:")?.trim(); - let resolved = Path::new(relative); - Some(if resolved.is_absolute() { - resolved.to_path_buf() - } else { - repo_root.join(resolved) - }) + if let Ok(gitdir) = std::fs::read_to_string(&git_path) { + if let Some(relative) = gitdir.trim().strip_prefix("gitdir:").map(str::trim) { + let resolved = Path::new(relative); + return Some(if resolved.is_absolute() { + resolved.to_path_buf() + } else { + repo_root.join(resolved) + }); + } + } + + if path_is_git_dir_layout(repo_root) && git_dir_is_bare(repo_root) { + return Some(repo_root.to_path_buf()); + } + + None +} + +fn path_is_git_dir_layout(path: &Path) -> bool { + path.join("HEAD").is_file() && path.join("objects").is_dir() && path.join("refs").is_dir() } pub(super) fn git_symbolic_head_full(repo_root: &Path) -> Option<String> { @@ -153,6 +163,11 @@ pub(super) fn git_ref_storage_is_reftable(git_common_dir: &Path) -> bool { .is_some_and(|value| value.eq_ignore_ascii_case("reftable")) } +fn git_dir_is_bare(git_dir: &Path) -> bool { + read_git_config_value(&git_dir.join("config"), "core", "bare") + .is_some_and(|value| value.eq_ignore_ascii_case("true")) +} + fn parse_git_head_branch(head: &str) -> Option<String> { let branch = head.trim().strip_prefix("ref: refs/heads/")?; (!branch.is_empty()).then(|| branch.to_string()) @@ -350,6 +365,62 @@ mod tests { } #[test] + fn git_repo_root_ignores_standalone_non_bare_git_dir_layout() { + let root = temp_test_dir("standalone-non-bare-git-dir"); + std::fs::write(root.join("HEAD"), "ref: refs/heads/main\n").unwrap(); + std::fs::create_dir_all(root.join("objects")).unwrap(); + std::fs::create_dir_all(root.join("refs")).unwrap(); + std::fs::write(root.join("config"), "[core]\n\tbare = false\n").unwrap(); + + assert_eq!(git_repo_root(&root.join("refs")), None); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn git_space_metadata_supports_standalone_bare_repo() { + let bare = temp_test_dir("bare-space"); + run_git(&bare, &["init", "--bare", "."]); + let nested = bare.join("refs"); + + let info = git_worktree_info(&nested).expect("bare repo should be discovered"); + assert!(info.is_bare); + assert!(!info.is_linked_worktree); + assert_eq!(info.git_dir, canonicalize_best_effort_path(&bare)); + + let metadata = git_space_metadata(&nested).expect("bare repo should map to a git space"); + assert_eq!( + canonicalize_best_effort_path(&metadata.repo_root), + canonicalize_best_effort_path(&bare) + ); + assert!(!metadata.is_linked_worktree); + + std::fs::remove_dir_all(bare).unwrap(); + } + + #[test] + fn git_space_metadata_marks_bare_dot_git_repo() { + let root = temp_test_dir("bare-dot-git"); + run_git(&root, &["init", "--bare", ".git"]); + + let info = git_worktree_info(&root).expect("bare .git repo should be discovered"); + assert!(info.is_bare); + assert!(!info.is_linked_worktree); + assert_eq!( + info.git_dir, + canonicalize_best_effort_path(&root.join(".git")) + ); + + let metadata = git_space_metadata(&root).expect("bare .git repo should map to a git space"); + assert_eq!( + canonicalize_best_effort_path(&metadata.repo_root), + canonicalize_best_effort_path(&root) + ); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] fn derive_label_prefers_repo_root_name() { let root = temp_test_dir("label-repo"); let nested = root.join("nested"); |
