@@ -208,24 +208,18 @@ func TestListDirectory(t *testing.T) {
}
})
- t.Run("hidden_directories_included", func(t *testing.T) {
- // Create a temp directory with a hidden directory
+ t.Run("hidden_directories_sorted_last", func(t *testing.T) {
+ // Create a temp directory with hidden and non-hidden directories
tmpDir, err := os.MkdirTemp("", "listdir_hidden_test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
- // Create a visible subdirectory
- visibleDir := tmpDir + "/visible"
- if err := os.Mkdir(visibleDir, 0o755); err != nil {
- t.Fatalf("failed to create visible dir: %v", err)
- }
-
- // Create a hidden subdirectory
- hiddenDir := tmpDir + "/.hidden"
- if err := os.Mkdir(hiddenDir, 0o755); err != nil {
- t.Fatalf("failed to create hidden dir: %v", err)
+ for _, name := range []string{".alpha", "beta", ".gamma", "delta", "alpha"} {
+ if err := os.Mkdir(filepath.Join(tmpDir, name), 0o755); err != nil {
+ t.Fatalf("failed to create dir %s: %v", name, err)
+ }
}
req := httptest.NewRequest("GET", "/api/list-directory?path="+tmpDir, nil)
@@ -241,21 +235,16 @@ func TestListDirectory(t *testing.T) {
t.Fatalf("failed to parse response: %v", err)
}
- // Should include both visible and hidden directories
- if len(resp.Entries) != 2 {
- t.Errorf("expected 2 entries, got: %d", len(resp.Entries))
+ if len(resp.Entries) != 5 {
+ t.Fatalf("expected 5 entries, got: %d", len(resp.Entries))
}
- // Check that both directories are present (sorted alphabetically, hidden first)
- names := make(map[string]bool)
- for _, e := range resp.Entries {
- names[e.Name] = true
- }
- if !names[".hidden"] {
- t.Errorf("expected .hidden to be included")
- }
- if !names["visible"] {
- t.Errorf("expected visible to be included")
+ // Non-hidden sorted first, then hidden sorted
+ want := []string{"alpha", "beta", "delta", ".alpha", ".gamma"}
+ for i, e := range resp.Entries {
+ if e.Name != want[i] {
+ t.Errorf("entry[%d]: expected %q, got %q", i, want[i], e.Name)
+ }
}
})
@@ -360,6 +349,86 @@ func TestListDirectory(t *testing.T) {
}
})
+ t.Run("git_worktree_root", func(t *testing.T) {
+ // Create a main git repo and a worktree, then verify that
+ // listing the worktree returns git_worktree_root pointing to the main repo.
+ tmpDir, err := os.MkdirTemp("", "listdir_wtroot_test")
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ mainRepo := filepath.Join(tmpDir, "main-repo")
+ if err := os.Mkdir(mainRepo, 0o755); err != nil {
+ t.Fatalf("failed to create main repo dir: %v", err)
+ }
+
+ for _, args := range [][]string{
+ {"git", "init"},
+ {"git", "config", "user.email", "test@example.com"},
+ {"git", "config", "user.name", "Test User"},
+ } {
+ cmd := exec.Command(args[0], args[1:]...)
+ cmd.Dir = mainRepo
+ if err := cmd.Run(); err != nil {
+ t.Fatalf("%v failed: %v", args, err)
+ }
+ }
+
+ if err := os.WriteFile(filepath.Join(mainRepo, "README.md"), []byte("# Hi"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command("git", "add", ".")
+ cmd.Dir = mainRepo
+ if err := cmd.Run(); err != nil {
+ t.Fatal(err)
+ }
+ cmd = exec.Command("git", "commit", "-m", "init\n\nPrompt: test")
+ cmd.Dir = mainRepo
+ if err := cmd.Run(); err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a worktree
+ cmd = exec.Command("git", "branch", "wt-branch")
+ cmd.Dir = mainRepo
+ if err := cmd.Run(); err != nil {
+ t.Fatal(err)
+ }
+ worktreePath := filepath.Join(tmpDir, "my-worktree")
+ cmd = exec.Command("git", "worktree", "add", worktreePath, "wt-branch")
+ cmd.Dir = mainRepo
+ if err := cmd.Run(); err != nil {
+ t.Fatal(err)
+ }
+
+ // List the worktree directory itself - should have git_worktree_root
+ req := httptest.NewRequest("GET", "/api/list-directory?path="+worktreePath, nil)
+ w := httptest.NewRecorder()
+ h.server.handleListDirectory(w, req)
+
+ var resp ListDirectoryResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("failed to parse response: %v", err)
+ }
+ if resp.GitWorktreeRoot != mainRepo {
+ t.Errorf("expected git_worktree_root=%q, got %q", mainRepo, resp.GitWorktreeRoot)
+ }
+
+ // List the main repo directory - should NOT have git_worktree_root
+ req = httptest.NewRequest("GET", "/api/list-directory?path="+mainRepo, nil)
+ w = httptest.NewRecorder()
+ h.server.handleListDirectory(w, req)
+
+ var resp2 ListDirectoryResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp2); err != nil {
+ t.Fatalf("failed to parse response: %v", err)
+ }
+ if resp2.GitWorktreeRoot != "" {
+ t.Errorf("main repo should not have git_worktree_root, got %q", resp2.GitWorktreeRoot)
+ }
+ })
+
t.Run("git_worktree_head_subject", func(t *testing.T) {
// Create a temp directory containing a git repo and a worktree
tmpDir, err := os.MkdirTemp("", "listdir_worktree_test")
@@ -11,6 +11,7 @@ import (
"os/exec"
"os/signal"
"path/filepath"
+ "sort"
"strings"
"sync"
"syscall"
@@ -354,10 +355,11 @@ type DirectoryEntry struct {
// ListDirectoryResponse is the response from the list-directory endpoint
type ListDirectoryResponse struct {
- Path string `json:"path"`
- Parent string `json:"parent"`
- Entries []DirectoryEntry `json:"entries"`
- GitHeadSubject string `json:"git_head_subject,omitempty"`
+ Path string `json:"path"`
+ Parent string `json:"parent"`
+ Entries []DirectoryEntry `json:"entries"`
+ GitHeadSubject string `json:"git_head_subject,omitempty"`
+ GitWorktreeRoot string `json:"git_worktree_root,omitempty"`
}
// handleListDirectory lists the contents of a directory for the directory picker
@@ -447,6 +449,16 @@ func (s *Server) handleListDirectory(w http.ResponseWriter, r *http.Request) {
}
}
+ // Sort entries: non-hidden first, then hidden (.*), alphabetically within each group
+ sort.Slice(entries, func(i, j int) bool {
+ iHidden := strings.HasPrefix(entries[i].Name, ".")
+ jHidden := strings.HasPrefix(entries[j].Name, ".")
+ if iHidden != jHidden {
+ return !iHidden // non-hidden comes first
+ }
+ return entries[i].Name < entries[j].Name
+ })
+
// Calculate parent directory
parent := filepath.Dir(path)
if parent == path {
@@ -463,6 +475,9 @@ func (s *Server) handleListDirectory(w http.ResponseWriter, r *http.Request) {
// Check if the current directory itself is a git repo
if isGitRepo(path) {
response.GitHeadSubject = getGitHeadSubject(path)
+ if root := getGitWorktreeRoot(path); root != "" {
+ response.GitWorktreeRoot = root
+ }
}
w.Header().Set("Content-Type", "application/json")
@@ -504,6 +519,42 @@ func getGitHeadSubject(repoPath string) string {
return strings.TrimSpace(string(output))
}
+// getGitWorktreeRoot returns the main repository root if the given path is
+// a git worktree (not the main repo itself). Returns "" otherwise.
+func getGitWorktreeRoot(repoPath string) string {
+ // Get the worktree's git dir and the common (main repo) git dir
+ cmd := exec.Command("git", "rev-parse", "--git-dir", "--git-common-dir")
+ cmd.Dir = repoPath
+ output, err := cmd.Output()
+ if err != nil {
+ return ""
+ }
+ lines := strings.SplitN(strings.TrimSpace(string(output)), "\n", 2)
+ if len(lines) != 2 {
+ return ""
+ }
+ gitDir := lines[0]
+ commonDir := lines[1]
+
+ // Resolve relative paths
+ if !filepath.IsAbs(gitDir) {
+ gitDir = filepath.Join(repoPath, gitDir)
+ }
+ if !filepath.IsAbs(commonDir) {
+ commonDir = filepath.Join(repoPath, commonDir)
+ }
+ gitDir = filepath.Clean(gitDir)
+ commonDir = filepath.Clean(commonDir)
+
+ // If they're the same, this is the main repo, not a worktree
+ if gitDir == commonDir {
+ return ""
+ }
+
+ // The main repo root is the parent of the common .git dir
+ return filepath.Dir(commonDir)
+}
+
// handleCreateDirectory creates a new directory
func (s *Server) handleCreateDirectory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {