aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 01:16:42 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 01:16:42 +0300
commit5d7e56737ef30d9de5802b0684a9428c40131dda (patch)
treea5c3c5870fc4686fd9bb24c819465e8f8bcc33f6 /src
parent4001fd2ec8ff05a982da18744b8cbde9df3ab530 (diff)
fix: support worktree actions from bare repos
refs #497
Diffstat (limited to 'src')
-rw-r--r--src/app/worktrees.rs49
-rw-r--r--src/workspace/git/discovery.rs95
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");