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 | |
| parent | 4001fd2ec8ff05a982da18744b8cbde9df3ab530 (diff) | |
fix: support worktree actions from bare repos
refs #497
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/worktrees.rs | 49 | ||||
| -rw-r--r-- | src/workspace/git/discovery.rs | 95 |
2 files changed, 132 insertions, 12 deletions
diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs index 7874cb0b..f7d906ee 100644 --- a/src/app/worktrees.rs +++ b/src/app/worktrees.rs @@ -1015,6 +1015,55 @@ mod tests { } #[test] + fn open_new_worktree_dialog_supports_standalone_bare_repo_source() { + let repo = create_committed_repo("app-worktree-dialog-bare-origin"); + let bare = unique_temp_path("app-worktree-dialog-bare-repo"); + run_git( + &repo, + &["clone", "--quiet", "--bare", ".", bare.to_str().unwrap()], + ); + let worktree_root = unique_temp_path("app-worktree-dialog-bare-root"); + + let mut app = app_for_worktree_tests(); + app.state.worktree_directory = worktree_root.clone(); + app.state.workspaces = vec![crate::workspace::Workspace::test_new("source")]; + app.state.workspaces[0].identity_cwd = bare.clone(); + + app.open_new_linked_worktree_dialog(0); + + assert_eq!(app.state.mode, Mode::NewLinkedWorktree); + assert!(app.state.config_diagnostic.is_none()); + let create = app.state.worktree_create.as_ref().unwrap(); + assert_eq!(create.source_checkout_path, bare); + assert_eq!(create.source_repo_root, create.source_checkout_path); + let source_checkout_path = create.source_checkout_path.clone(); + + let branch = "worktree/from-bare-source"; + let repo_name = create.repo_name.clone(); + let checkout = crate::worktree::default_checkout_path(&worktree_root, &repo_name, branch); + app.state.name_input = branch.into(); + + app.start_worktree_add(); + + let event = wait_for_worktree_event(&mut app); + match event { + AppEvent::WorktreeAddFinished(result) => { + assert_eq!(result.path, checkout); + assert_eq!(result.result, Ok(())); + } + other => panic!("unexpected event: {other:?}"), + } + assert!(checkout.join("README.md").exists()); + + let remove_new = + crate::worktree::build_worktree_remove_command(&source_checkout_path, &checkout, false); + crate::worktree::run_worktree_command(&remove_new).unwrap(); + let _ = std::fs::remove_dir_all(worktree_root); + let _ = std::fs::remove_dir_all(source_checkout_path); + let _ = std::fs::remove_dir_all(repo); + } + + #[test] fn start_worktree_add_uses_source_checkout_head_as_base() { let repo = create_committed_repo("app-worktree-add-source-repo"); let source_checkout = unique_temp_path("app-worktree-add-source-checkout"); 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"); |
