Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions internal/statusline/statusline.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ func (sl *StatusLine) renderDir() string {
}

displayName := filepath.Base(displayBase)
if inWorktree {
if mainRepo := getMainRepoName(displayBase); mainRepo != "" {
displayName = mainRepo + "/" + displayName
}
}
icon := sl.config.Icon
if icon != "" {
icon += " "
Expand Down Expand Up @@ -275,6 +280,52 @@ func (sl *StatusLine) findWorktreeRootCached(dir string) (string, bool) {
return root, isWorktree
}

// parseMainRepoName reads the .git file in a worktree root to determine the
// main repository name. In a git worktree, .git is a file containing:
//
// gitdir: /path/to/main/repo/.git/worktrees/worktree-name
//
// Returns "" on any failure (not a worktree, can't read, can't parse).
func parseMainRepoName(worktreeRoot string) string {
data, err := os.ReadFile(filepath.Join(worktreeRoot, ".git"))
if err != nil {
return ""
}

content := strings.TrimSpace(string(data))
if !strings.HasPrefix(content, "gitdir: ") {
return ""
}

gitdir := strings.TrimPrefix(content, "gitdir: ")
if !filepath.IsAbs(gitdir) {
gitdir = filepath.Clean(filepath.Join(worktreeRoot, gitdir))
}

// Find the ".git" segment in the path to locate the main repo root.
// e.g. /path/to/main-repo/.git/worktrees/name → /path/to/main-repo
sep := string(filepath.Separator)
marker := sep + ".git" + sep
idx := strings.LastIndex(gitdir, marker)
if idx < 0 {
return ""
}

return filepath.Base(gitdir[:idx])
}

// getMainRepoName returns the main repository name for a worktree, with caching.
func getMainRepoName(worktreeRoot string) string {
cacheKey := "main-repo:" + worktreeRoot
if cached, ok := statusCache.Get(cacheKey); ok {
return cached
}

name := parseMainRepoName(worktreeRoot)
statusCache.Set(cacheKey, name, cache.WorktreeTTL)
return name
}

func (sl *StatusLine) renderModel() string {
return colors.Wrap(colors.Magenta, sl.input.Model.DisplayName)
}
Expand Down
85 changes: 79 additions & 6 deletions internal/statusline/statusline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,14 @@ func TestRenderDir_WorktreeIndicator(t *testing.T) {
if !strings.Contains(result, "⎇") {
t.Errorf("renderDir should include ⎇ indicator for worktree, got: %s", result)
}

// Should show mainRepoName/worktreeName
mainRepoName := filepath.Base(tmpDir)
worktreeName := filepath.Base(worktreeDir)
expected := mainRepoName + "/" + worktreeName
if !strings.Contains(result, expected) {
t.Errorf("renderDir should show '%s', got: %s", expected, result)
}
}

// TestRenderDir_NoIndicatorForMainRepo does not show ⎇ for main repo
Expand Down Expand Up @@ -718,10 +726,11 @@ func TestRenderDir_CurrentDirInWorktree(t *testing.T) {
if !strings.Contains(result, worktreeName) {
t.Errorf("should show worktree name '%s', got: %s", worktreeName, result)
}
// Should NOT show the main repo name
// Should show the main repo name as prefix
mainRepoName := filepath.Base(tmpDir)
if strings.Contains(result, mainRepoName) {
t.Errorf("should NOT show main repo name '%s' when in worktree, got: %s", mainRepoName, result)
expected := mainRepoName + "/" + worktreeName
if !strings.Contains(result, expected) {
t.Errorf("should show '%s' (mainRepo/worktree), got: %s", expected, result)
}
}

Expand Down Expand Up @@ -759,13 +768,15 @@ func TestRenderDir_CurrentDirInWorktreeSubdir(t *testing.T) {

result := sl.renderDir()

// Should show worktree name + /src with ⎇ indicator
// Should show mainRepo/worktreeName + /src with ⎇ indicator
worktreeName := filepath.Base(worktreeDir)
mainRepoName := filepath.Base(tmpDir)
if !strings.Contains(result, "⎇") {
t.Errorf("should show ⎇ indicator, got: %s", result)
}
if !strings.Contains(result, worktreeName) {
t.Errorf("should show worktree name '%s', got: %s", worktreeName, result)
expected := mainRepoName + "/" + worktreeName
if !strings.Contains(result, expected) {
t.Errorf("should show '%s', got: %s", expected, result)
}
if !strings.Contains(result, "/src") {
t.Errorf("should show subdir '/src', got: %s", result)
Expand Down Expand Up @@ -1243,6 +1254,68 @@ func TestRenderCost_ShowBurnRateDisabled(t *testing.T) {
}
}

// TestGetMainRepoName_ValidWorktree returns correct main repo name
func TestGetMainRepoName_ValidWorktree(t *testing.T) {
tmpDir := setupTestGitRepo(t)
defer os.RemoveAll(tmpDir)

// Create a worktree
worktreeDir := filepath.Join(os.TempDir(), "prism-test-mainrepo-valid")
defer os.RemoveAll(worktreeDir)

cmd := exec.Command("git", "worktree", "add", worktreeDir, "HEAD")
cmd.Dir = tmpDir
if err := cmd.Run(); err != nil {
t.Fatalf("failed to create worktree: %v", err)
}

name := getMainRepoName(worktreeDir)
expected := filepath.Base(tmpDir)
if name != expected {
t.Errorf("getMainRepoName should return '%s', got '%s'", expected, name)
}
}

// TestGetMainRepoName_MainRepo returns empty for a regular (non-worktree) repo
func TestGetMainRepoName_MainRepo(t *testing.T) {
tmpDir := setupTestGitRepo(t)
defer os.RemoveAll(tmpDir)

name := getMainRepoName(tmpDir)
if name != "" {
t.Errorf("getMainRepoName should return '' for main repo, got '%s'", name)
}
}

// TestParseMainRepoName_Fallbacks verifies graceful failures
func TestParseMainRepoName_Fallbacks(t *testing.T) {
// Nonexistent directory
if name := parseMainRepoName("/nonexistent/path"); name != "" {
t.Errorf("expected '' for nonexistent dir, got '%s'", name)
}

// Directory without .git
tmpDir, err := os.MkdirTemp("", "prism-test-nogit-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)

if name := parseMainRepoName(tmpDir); name != "" {
t.Errorf("expected '' for dir without .git, got '%s'", name)
}

// .git as directory (regular repo, not worktree)
gitDir := filepath.Join(tmpDir, ".git")
if err := os.MkdirAll(gitDir, 0755); err != nil {
t.Fatal(err)
}

if name := parseMainRepoName(tmpDir); name != "" {
t.Errorf("expected '' for .git directory, got '%s'", name)
}
}

// TestRenderCost_NoCacheIndicatorWhenZero verifies no cache indicator when no cache reads
func TestRenderCost_NoCacheIndicatorWhenZero(t *testing.T) {
sl := &StatusLine{
Expand Down
Loading