Skip to content
Closed
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
4 changes: 2 additions & 2 deletions cmd/bd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
31 changes: 30 additions & 1 deletion cmd/bd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
115 changes: 115 additions & 0 deletions cmd/bd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
}
1 change: 0 additions & 1 deletion internal/storage/dolt/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading