refactor: consolidate duplicate path-finding utilities (bd-74w1, bd-4nqq)
- Add git.GetRepoRoot() with Windows path normalization - Update beads.findGitRoot() to delegate to git.GetRepoRoot() - Replace findBeadsDir() with beads.FindBeadsDir() across 8 files - Remove duplicate findBeadsDir() and findGitRoot() function definitions - Remove dead test code (TestInfoCommand, TestInfoWithNoDaemon) - Update tests to work with consolidated APIs Part of Code Health Review Dec 2025 epic (bd-tggf). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/syncbranch"
|
||||
@@ -69,13 +69,13 @@ func checkAndAutoImport(ctx context.Context, store storage.Storage) bool {
|
||||
// Returns (issue_count, relative_jsonl_path, git_ref)
|
||||
func checkGitForIssues() (int, string, string) {
|
||||
// Try to find .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return 0, "", ""
|
||||
}
|
||||
|
||||
// Construct relative path from git root
|
||||
gitRoot := findGitRoot()
|
||||
gitRoot := git.GetRepoRoot()
|
||||
if gitRoot == "" {
|
||||
return 0, "", ""
|
||||
}
|
||||
@@ -193,80 +193,7 @@ func getLocalSyncBranch(beadsDir string) string {
|
||||
return cfg.SyncBranch
|
||||
}
|
||||
|
||||
// findBeadsDir finds the .beads directory in current or parent directories
|
||||
func findBeadsDir() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if we're in a git worktree with sparse checkout
|
||||
// In worktrees, .beads might not exist locally, so we need to resolve to the main repo
|
||||
if git.IsWorktree() {
|
||||
mainRepoRoot, err := git.GetMainRepoRoot()
|
||||
if err == nil && mainRepoRoot != "" {
|
||||
mainBeadsDir := filepath.Join(mainRepoRoot, ".beads")
|
||||
if info, err := os.Stat(mainBeadsDir); err == nil && info.IsDir() {
|
||||
resolved, err := filepath.EvalSymlinks(mainBeadsDir)
|
||||
if err != nil {
|
||||
return mainBeadsDir
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
beadsDir := filepath.Join(dir, ".beads")
|
||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||
// Resolve symlinks to get canonical path (fixes macOS /var -> /private/var)
|
||||
resolved, err := filepath.EvalSymlinks(beadsDir)
|
||||
if err != nil {
|
||||
return beadsDir // Fall back to unresolved if EvalSymlinks fails
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// Reached root
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// findGitRoot finds the git repository root
|
||||
func findGitRoot() string {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
root := string(bytes.TrimSpace(output))
|
||||
|
||||
// Normalize path for the current OS
|
||||
// Git on Windows may return paths with forward slashes (C:/Users/...)
|
||||
// or Unix-style paths (/c/Users/...), convert to native format
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(root) > 0 && root[0] == '/' && len(root) >= 3 && root[2] == '/' {
|
||||
// Convert /c/Users/... to C:\Users\...
|
||||
root = strings.ToUpper(string(root[1])) + ":" + filepath.FromSlash(root[2:])
|
||||
} else {
|
||||
// Convert C:/Users/... to C:\Users\...
|
||||
root = filepath.FromSlash(root)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve symlinks to get canonical path (fixes macOS /var -> /private/var)
|
||||
resolved, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
return root // Fall back to unresolved if EvalSymlinks fails
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// importFromGit imports issues from git at the specified ref (bd-0is: supports sync-branch)
|
||||
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath, gitRef string) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
@@ -181,17 +182,21 @@ func TestCheckAndAutoImport_EmptyDatabaseNoGit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindBeadsDir(t *testing.T) {
|
||||
// Create temp directory with .beads
|
||||
// Create temp directory with .beads and a valid project file
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
// Create a config.yaml so beads.FindBeadsDir() recognizes this as a valid project
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "config.yaml"), []byte("prefix: test\n"), 0600); err != nil {
|
||||
t.Fatalf("Failed to create config.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Change to tmpDir
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
found := findBeadsDir()
|
||||
found := beads.FindBeadsDir()
|
||||
if found == "" {
|
||||
t.Error("Expected to find .beads directory")
|
||||
}
|
||||
@@ -209,7 +214,7 @@ func TestFindBeadsDir_NotFound(t *testing.T) {
|
||||
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
found := findBeadsDir()
|
||||
found := beads.FindBeadsDir()
|
||||
// findBeadsDir walks up to root, so it might find .beads in parent dirs
|
||||
// (e.g., user's home directory). Just verify it's not in tmpDir itself.
|
||||
if found != "" && filepath.Dir(found) == tmpDir {
|
||||
@@ -224,6 +229,10 @@ func TestFindBeadsDir_ParentDirectory(t *testing.T) {
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
// Create a config.yaml so beads.FindBeadsDir() recognizes this as a valid project
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "config.yaml"), []byte("prefix: test\n"), 0600); err != nil {
|
||||
t.Fatalf("Failed to create config.yaml: %v", err)
|
||||
}
|
||||
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||
@@ -233,7 +242,7 @@ func TestFindBeadsDir_ParentDirectory(t *testing.T) {
|
||||
// Change to subdir
|
||||
t.Chdir(subDir)
|
||||
|
||||
found := findBeadsDir()
|
||||
found := beads.FindBeadsDir()
|
||||
if found == "" {
|
||||
t.Error("Expected to find .beads directory in parent")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
)
|
||||
|
||||
// ensureForkProtection prevents contributors from accidentally committing
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
// .beads/issues.jsonl to .git/info/exclude so it won't be staged.
|
||||
// This is a per-clone setting that doesn't modify tracked files.
|
||||
func ensureForkProtection() {
|
||||
// Find git root (reuses existing findGitRoot from autoimport.go)
|
||||
gitRoot := findGitRoot()
|
||||
// Find git root
|
||||
gitRoot := git.GetRepoRoot()
|
||||
if gitRoot == "" {
|
||||
return // Not in a git repo
|
||||
}
|
||||
|
||||
@@ -5,14 +5,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInfoCommand(t *testing.T) {
|
||||
t.Skip("Manual test - bd info command is working, see manual testing")
|
||||
}
|
||||
|
||||
func TestInfoWithNoDaemon(t *testing.T) {
|
||||
t.Skip("Manual test - bd info --no-daemon command is working, see manual testing")
|
||||
}
|
||||
|
||||
func TestVersionChangesStructure(t *testing.T) {
|
||||
// Verify versionChanges is properly structured
|
||||
if len(versionChanges) == 0 {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
@@ -568,7 +569,7 @@ func findJiraScript(name string) (string, error) {
|
||||
}
|
||||
|
||||
// Check BEADS_DIR or current .beads location
|
||||
if beadsDir := findBeadsDir(); beadsDir != "" {
|
||||
if beadsDir := beads.FindBeadsDir(); beadsDir != "" {
|
||||
repoRoot := filepath.Dir(beadsDir)
|
||||
locations = append(locations, filepath.Join(repoRoot, "examples", "jira-import", name))
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ This command:
|
||||
}
|
||||
|
||||
// Find .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
@@ -729,7 +729,7 @@ func cleanupWALFiles(dbPath string) {
|
||||
// handleInspect shows migration plan and database state for AI agent analysis
|
||||
func handleInspect() {
|
||||
// Find .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
@@ -927,7 +927,7 @@ func handleToSeparateBranch(branch string, dryRun bool) {
|
||||
}
|
||||
|
||||
// Find .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
@@ -102,7 +103,7 @@ Examples:
|
||||
}
|
||||
|
||||
// Find .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
@@ -863,7 +864,7 @@ func getRepoRootForWorktree(_ context.Context) string {
|
||||
// gitHasBeadsChanges checks if any tracked files in .beads/ have uncommitted changes
|
||||
func gitHasBeadsChanges(ctx context.Context) (bool, error) {
|
||||
// Get the absolute path to .beads directory
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return false, fmt.Errorf("no .beads directory found")
|
||||
}
|
||||
@@ -961,7 +962,7 @@ func gitCommit(ctx context.Context, filePath string, message string) error {
|
||||
// to avoid staging gitignored snapshot files that may be tracked. (bd-guc fix)
|
||||
// Worktree-aware: handles cases where .beads is in the main repo but we're running from a worktree.
|
||||
func gitCommitBeadsDir(ctx context.Context, message string) error {
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return fmt.Errorf("no .beads directory found")
|
||||
}
|
||||
@@ -1185,7 +1186,7 @@ func gitPush(ctx context.Context) error {
|
||||
// This is used after sync when sync.branch is configured to keep the working directory clean.
|
||||
// The actual beads data lives on the sync branch; the main branch's .beads/ is just a snapshot.
|
||||
func restoreBeadsDirFromBranch(ctx context.Context) error {
|
||||
beadsDir := findBeadsDir()
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return fmt.Errorf("no .beads directory found")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -442,15 +441,10 @@ type DatabaseInfo struct {
|
||||
// or empty string if not in a git repository. Used to limit directory
|
||||
// tree walking to within the current git repo (bd-c8x).
|
||||
//
|
||||
// This function is worktree-aware and will correctly identify the repository
|
||||
// root in both regular repositories and git worktrees.
|
||||
// This function delegates to git.GetRepoRoot() which is worktree-aware
|
||||
// and handles Windows path normalization.
|
||||
func findGitRoot() string {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
return git.GetRepoRoot()
|
||||
}
|
||||
|
||||
// findDatabaseInTree walks up the directory tree looking for .beads/*.db
|
||||
|
||||
@@ -103,6 +103,50 @@ func GetMainRepoRoot() (string, error) {
|
||||
return mainRepoRoot, nil
|
||||
}
|
||||
|
||||
// GetRepoRoot returns the root directory of the current git repository.
|
||||
// Returns empty string if not in a git repository.
|
||||
//
|
||||
// This function is worktree-aware and handles Windows path normalization
|
||||
// (Git on Windows may return paths like /c/Users/... or C:/Users/...).
|
||||
// It also resolves symlinks to get the canonical path.
|
||||
func GetRepoRoot() string {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
root := strings.TrimSpace(string(output))
|
||||
|
||||
// Normalize Windows paths from Git
|
||||
// Git on Windows may return /c/Users/... or C:/Users/...
|
||||
root = NormalizePath(root)
|
||||
|
||||
// Resolve symlinks to get canonical path (fixes macOS /var -> /private/var)
|
||||
if resolved, err := filepath.EvalSymlinks(root); err == nil {
|
||||
return resolved
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// NormalizePath converts Git's Windows path formats to native format.
|
||||
// Git on Windows may return paths like /c/Users/... or C:/Users/...
|
||||
// This function converts them to native Windows format (C:\Users\...).
|
||||
// On non-Windows systems, this is a no-op.
|
||||
func NormalizePath(path string) string {
|
||||
// Only apply Windows normalization on Windows
|
||||
if filepath.Separator != '\\' {
|
||||
return path
|
||||
}
|
||||
|
||||
// Convert /c/Users/... to C:\Users\...
|
||||
if len(path) >= 3 && path[0] == '/' && path[2] == '/' {
|
||||
return strings.ToUpper(string(path[1])) + ":" + filepath.FromSlash(path[2:])
|
||||
}
|
||||
|
||||
// Convert C:/Users/... to C:\Users\...
|
||||
return filepath.FromSlash(path)
|
||||
}
|
||||
|
||||
// getGitDirNoError is a helper that returns empty string on error
|
||||
// to avoid cluttering code with error handling for simple checks.
|
||||
func getGitDirNoError(flag string) string {
|
||||
|
||||
Reference in New Issue
Block a user