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
18 changes: 18 additions & 0 deletions internal/agent/loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,24 @@ func TestGitBranchForPromptResolvesRelativeWorktreeGitdir(t *testing.T) {
}
}

func TestGitBranchForPromptInSubdirectory(t *testing.T) {
root := t.TempDir()
gitdir := filepath.Join(root, ".git")
if err := os.MkdirAll(gitdir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(gitdir, "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil {
t.Fatal(err)
}
subdir := filepath.Join(root, "sub", "dir")
if err := os.MkdirAll(subdir, 0o755); err != nil {
t.Fatal(err)
}
if got := gitBranchForPrompt(subdir); got != "main" {
t.Fatalf("gitBranchForPrompt in subdirectory = %q, want main", got)
}
}

func TestBuildSystemPromptInjectsWorkspaceContext(t *testing.T) {
cwd := t.TempDir()
if err := os.WriteFile(filepath.Join(cwd, "AGENTS.md"), []byte("Always run `make lint` before committing."), 0o644); err != nil {
Expand Down
20 changes: 12 additions & 8 deletions internal/agent/system_prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func workspaceContext(cwd string) string {
}
b.WriteString("</environment>")

b.WriteString(projectGuidelines(cwd, findProjectGitRoot(cwd)))
b.WriteString(projectGuidelines(cwd, FindProjectGitRoot(cwd)))
if repoMap := repoMapContext(cwd); repoMap != "" {
b.WriteString("\n\n## Repo map\n\n" + repoMap)
}
Expand Down Expand Up @@ -542,17 +542,17 @@ func resolveDirCaseInsensitive(target, anchor string) (string, bool) {
return cur, true
}

// findProjectGitRoot returns the nearest ancestor of cwd that contains a
// FindProjectGitRoot returns the nearest ancestor of cwd that contains a
// .git entry (file or directory). Returns "" when no git root is found, so
// the caller can fall back to cwd-only lookup.
func findProjectGitRoot(cwd string) string {
func FindProjectGitRoot(cwd string) string {
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return ""
}
cur := cwd
for {
if hasGitMetadata(cur) {
if HasGitMetadata(cur) {
return cur
}
parent := filepath.Dir(cur)
Expand All @@ -563,7 +563,7 @@ func findProjectGitRoot(cwd string) string {
}
}

func hasGitMetadata(dir string) bool {
func HasGitMetadata(dir string) bool {
gitPath := filepath.Join(dir, ".git")
info, err := os.Stat(gitPath)
if err != nil {
Expand Down Expand Up @@ -619,7 +619,11 @@ func repoMapContext(cwd string) string {
// cwd, handling both a regular checkout (.git dir) and a worktree (.git file).
// Returns "" on any problem — the prompt simply omits the branch segment.
func gitBranchForPrompt(cwd string) string {
gitPath := filepath.Join(cwd, ".git")
gitRoot := FindProjectGitRoot(cwd)
if gitRoot == "" {
return ""
}
gitPath := filepath.Join(gitRoot, ".git")
info, err := os.Stat(gitPath)
if err != nil {
return ""
Expand All @@ -635,10 +639,10 @@ func gitBranchForPrompt(cwd string) string {
return ""
}
// In worktree mode the gitdir is often RELATIVE (e.g.
// "gitdir: ../.git/worktrees/<name>") — resolve it against cwd, not the
// "gitdir: ../.git/worktrees/<name>") — resolve it against the worktree root (gitRoot), not the
// process working directory, or HEAD lookup fails and we drop the branch.
if !filepath.IsAbs(dir) {
dir = filepath.Join(cwd, dir)
dir = filepath.Join(gitRoot, dir)
}
headPath = filepath.Join(dir, "HEAD")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/agent/system_prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ func TestFindProjectGitRootIgnoresEmptyGitDirectory(t *testing.T) {
if err := os.MkdirAll(leaf, 0o755); err != nil {
t.Fatal(err)
}
if got := findProjectGitRoot(leaf); got != "" {
t.Fatalf("findProjectGitRoot = %q, want empty for invalid .git directory", got)
if got := FindProjectGitRoot(leaf); got != "" {
t.Fatalf("FindProjectGitRoot = %q, want empty for invalid .git directory", got)
}
}
18 changes: 18 additions & 0 deletions internal/tui/tui_fixes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,21 @@ func suggestionNames2(s []commandSuggestion) []string {
}
return out
}

func TestGitBranchInSubdirectory(t *testing.T) {
root := t.TempDir()
gitdir := filepath.Join(root, ".git")
if err := os.MkdirAll(gitdir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(gitdir, "HEAD"), []byte("ref: refs/heads/feature-tui-branch\n"), 0o644); err != nil {
t.Fatal(err)
}
subdir := filepath.Join(root, "nested", "sub", "dir")
if err := os.MkdirAll(subdir, 0o755); err != nil {
t.Fatal(err)
}
if got := gitBranch(subdir); got != "feature-tui-branch" {
t.Fatalf("gitBranch in subdirectory = %q, want feature-tui-branch", got)
}
}
15 changes: 13 additions & 2 deletions internal/tui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,15 @@ func shortenPath(path string) string {
// both regular checkouts (.git dir) and worktrees (.git file). Returns "" on any
// problem — the header simply omits the segment.
func gitBranch(cwd string) string {
if strings.TrimSpace(cwd) == "" {
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return ""
}
gitPath := filepath.Join(cwd, ".git")
gitRoot := agent.FindProjectGitRoot(cwd)
if gitRoot == "" {
return ""
}
gitPath := filepath.Join(gitRoot, ".git")
info, err := os.Stat(gitPath)
if err != nil {
return ""
Expand All @@ -481,6 +486,12 @@ func gitBranch(cwd string) string {
if dir == "" {
return ""
}
// In worktree mode the gitdir is often RELATIVE (e.g.
// "gitdir: ../.git/worktrees/<name>") — resolve it against the worktree root (gitRoot), not the
// process working directory, or HEAD lookup fails and we drop the branch.
if !filepath.IsAbs(dir) {
dir = filepath.Join(gitRoot, dir)
}
headPath = filepath.Join(dir, "HEAD")
}

Expand Down
Loading