diff --git a/cmd/bd/import.go b/cmd/bd/import.go index 0c0c298e32..630a9d0856 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -21,12 +21,12 @@ created and existing issues are updated (upsert semantics). This command makes the git-tracked JSONL portable again — after 'git pull' brings new issues, 'bd import' loads them into the local Dolt database. -EXAMPLES: + EXAMPLES: bd import # Import from .beads/issues.jsonl bd import backup.jsonl # Import from a specific file bd import --dry-run # Show what would be imported`, GroupID: "sync", - RunE: runImport, + RunE: runImport, } var ( diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 7588a14841..a4e41de553 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -116,6 +116,35 @@ func isReadOnlyCommand(cmdName string) bool { return readOnlyCommands[cmdName] } +// resolveCommandBeadsDir maps a discovered Dolt data path back to the owning +// .beads directory. filepath.Dir(dbPath) only works when the Dolt data lives +// under .beads/dolt; custom dolt_data_dir values can place it elsewhere. +func resolveCommandBeadsDir(dbPath string) string { + if dbPath == "" { + return "" + } + + guessedBeadsDir := filepath.Dir(dbPath) + + // BEADS_DB is an explicit database override, so preserve its legacy + // filepath.Dir behavior instead of trying to rediscover a repo-local .beads. + if os.Getenv("BEADS_DB") != "" { + return guessedBeadsDir + } + + if cfg, err := configfile.Load(guessedBeadsDir); err == nil && cfg != nil { + if utils.PathsEqual(cfg.DatabasePath(guessedBeadsDir), dbPath) { + return guessedBeadsDir + } + } + + if discoveredBeadsDir := beads.FindBeadsDir(); discoveredBeadsDir != "" { + return discoveredBeadsDir + } + + return guessedBeadsDir +} + // getActorWithGit returns the actor for audit trails with git config fallback. // Priority: --actor flag > BD_ACTOR env > BEADS_ACTOR env > git config user.name > $USER > "unknown" // This provides a sensible default for developers: their git identity is used unless @@ -473,7 +502,7 @@ var rootCmd = &cobra.Command{ // opens its own store connection, writes the version metadata, commits it, // and closes BEFORE the main store is opened. This ensures bd doctor and // read-only commands see the correct version after a CLI upgrade. - beadsDir := filepath.Dir(dbPath) + beadsDir := resolveCommandBeadsDir(dbPath) autoMigrateOnVersionBump(beadsDir) diff --git a/cmd/bd/main_test.go b/cmd/bd/main_test.go index 6d3f35e5b5..a170bb90e1 100644 --- a/cmd/bd/main_test.go +++ b/cmd/bd/main_test.go @@ -5,11 +5,15 @@ package main import ( "context" "os" + "os/exec" "path/filepath" + "strconv" "strings" "testing" "time" + "github.com/steveyegge/beads/internal/configfile" + "github.com/steveyegge/beads/internal/storage/dolt" "github.com/steveyegge/beads/internal/types" ) @@ -153,3 +157,114 @@ func TestBlockedEnvVars(t *testing.T) { } }) } + +func TestListUsesRepoBeadsDirWhenDoltDataDirEscapesDotBeads(t *testing.T) { + if testDoltServerPort == 0 { + t.Skip("Dolt test server not available, skipping") + } + + initConfigForTest(t) + ensureCleanGlobalState(t) + + tmpDir := t.TempDir() + repoDir := filepath.Join(tmpDir, "repo") + beadsDir := filepath.Join(repoDir, ".beads") + if err := os.MkdirAll(beadsDir, 0o755); err != nil { + t.Fatalf("mkdir beads dir: %v", err) + } + + relativeDoltDir := "../external-dolt" + externalDoltDir := filepath.Join(beadsDir, relativeDoltDir) + if err := os.MkdirAll(filepath.Dir(externalDoltDir), 0o755); err != nil { + t.Fatalf("mkdir external dolt parent: %v", err) + } + + database := uniqueTestDBName(t) + cfg := &configfile.Config{ + Backend: configfile.BackendDolt, + DoltMode: configfile.DoltModeServer, + DoltServerHost: "127.0.0.1", + DoltServerPort: testDoltServerPort, + DoltDatabase: database, + DoltDataDir: relativeDoltDir, + } + if err := cfg.Save(beadsDir); err != nil { + t.Fatalf("save metadata: %v", err) + } + if err := os.WriteFile(filepath.Join(beadsDir, "dolt-server.port"), []byte(strconv.Itoa(testDoltServerPort)), 0o600); err != nil { + t.Fatalf("write port file: %v", err) + } + + ctx := context.Background() + testStore, err := dolt.New(ctx, &dolt.Config{ + Path: externalDoltDir, + BeadsDir: beadsDir, + ServerHost: "127.0.0.1", + ServerPort: testDoltServerPort, + Database: database, + CreateIfMissing: true, + }) + if err != nil { + t.Fatalf("create test store: %v", err) + } + defer func() { + _ = testStore.Close() + dropTestDatabase(database, testDoltServerPort) + }() + + if err := testStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("set issue_prefix: %v", err) + } + if err := testStore.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil { + t.Fatalf("set types.custom: %v", err) + } + + now := time.Now() + issue := &types.Issue{ + ID: "test-port-proof-1", + Title: "Port-proof issue", + Description: "Verifies bd list uses the repo's .beads config even with external dolt data", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeBug, + CreatedAt: now, + UpdatedAt: now, + } + if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil { + t.Fatalf("create issue: %v", err) + } + + t.Setenv("BEADS_DIR", beadsDir) + t.Setenv("BEADS_DB", "") + t.Setenv("BEADS_DOLT_SERVER_PORT", "") + t.Setenv("BEADS_DOLT_PORT", "") + + binPath := filepath.Join(t.TempDir(), "bd-under-test") + packageDir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + buildCmd := exec.Command("go", "build", "-o", binPath, ".") + buildCmd.Dir = packageDir + buildOut, err := buildCmd.CombinedOutput() + if err != nil { + t.Fatalf("go build failed: %v\n%s", err, buildOut) + } + + listCmd := exec.Command(binPath, "list", "--json") + listCmd.Dir = repoDir + listCmd.Env = append(os.Environ(), + "BEADS_TEST_MODE=1", + "BEADS_DIR="+beadsDir, + "BEADS_DB=", + "BEADS_DOLT_SERVER_PORT=", + "BEADS_DOLT_PORT=", + ) + output, err := listCmd.CombinedOutput() + if err != nil { + t.Fatalf("bd list failed: %v\n%s", err, output) + } + if !strings.Contains(string(output), "Port-proof issue") { + t.Fatalf("expected list output to include created issue\n%s", output) + } +} diff --git a/internal/storage/dolt/store.go b/internal/storage/dolt/store.go index 02417fe8b4..3e9f22d4af 100644 --- a/internal/storage/dolt/store.go +++ b/internal/storage/dolt/store.go @@ -1844,7 +1844,6 @@ func (s *DoltStore) tryAutoResolveMetadataConflicts(ctx context.Context, tx *sql return true, nil } - // Branch creates a new branch func (s *DoltStore) Branch(ctx context.Context, name string) (retErr error) { ctx, span := doltTracer.Start(ctx, "dolt.branch",