fix(init): respect BEADS_DIR environment variable (#1273)
* fix(sync): read sync.mode from yaml first, then database bd config set sync.mode writes to config.yaml (because sync.* is a yaml-only prefix), but GetSyncMode() only read from the database. This caused dolt-native mode to be ignored - JSONL export still happened because the database had no sync.mode value. Now GetSyncMode() checks config.yaml first (via config.GetSyncMode()), falling back to database for backward compatibility. Fixes: oss-5ca279 * fix(init): respect BEADS_DIR environment variable Problem: - `bd init` ignored BEADS_DIR when checking for existing data - `bd init` created database at CWD/.beads instead of BEADS_DIR - Contributor wizard used ~/.beads-planning as default, ignoring BEADS_DIR Solution: - Add BEADS_DIR check in checkExistingBeadsData() (matches FindBeadsDir pattern) - Compute beadsDirForInit early, before initDBPath determination - Use BEADS_DIR as default in contributor wizard when set - Preserve precedence: --db > BEADS_DB > BEADS_DIR > default Impact: - Users with BEADS_DIR set now get consistent behavior across all bd commands - ACF-style fork tracking (external .beads directory) now works correctly Fixes: steveyegge/beads#??? * fix(doctor): respect BEADS_DIR environment variable Also updates documentation to reflect BEADS_DIR support in init and doctor. Changes: - doctor.go: Check BEADS_DIR before falling back to CWD - doctor_test.go: Add tests for BEADS_DIR path resolution - WORKTREES.md: Document simplified BEADS_DIR+init workflow - CONTRIBUTOR_NAMESPACE_ISOLATION.md: Note init/doctor BEADS_DIR support * test(init): add BEADS_DB > BEADS_DIR precedence test Verifies that BEADS_DB env var takes precedence over BEADS_DIR when both are set, ensuring the documented precedence order: --db > BEADS_DB > BEADS_DIR > default * chore: fill in GH#1277 placeholder in sync_mode comment
This commit is contained in:
committed by
GitHub
parent
810192157c
commit
b7d650bd8e
@@ -144,9 +144,15 @@ Examples:
|
|||||||
// Use global jsonOutput set by PersistentPreRun
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Determine path to check
|
// Determine path to check
|
||||||
checkPath := "."
|
// Precedence: explicit arg > BEADS_DIR (parent) > CWD
|
||||||
|
var checkPath string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
checkPath = args[0]
|
checkPath = args[0]
|
||||||
|
} else if beadsDir := os.Getenv("BEADS_DIR"); beadsDir != "" {
|
||||||
|
// BEADS_DIR points to .beads directory, doctor needs parent
|
||||||
|
checkPath = filepath.Dir(beadsDir)
|
||||||
|
} else {
|
||||||
|
checkPath = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to absolute path
|
// Convert to absolute path
|
||||||
|
|||||||
@@ -1398,3 +1398,120 @@ func TestCheckSyncBranchHookQuick(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDoctor_WithBEADS_DIR tests that doctor respects BEADS_DIR environment variable
|
||||||
|
func TestDoctor_WithBEADS_DIR(t *testing.T) {
|
||||||
|
// Reset Cobra flags to avoid interference
|
||||||
|
defer func() {
|
||||||
|
doctorFix = false
|
||||||
|
doctorYes = false
|
||||||
|
doctorInteractive = false
|
||||||
|
doctorDryRun = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create target directory (where BEADS_DIR points)
|
||||||
|
targetDir := t.TempDir()
|
||||||
|
targetBeadsDir := filepath.Join(targetDir, ".beads")
|
||||||
|
if err := os.Mkdir(targetBeadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CWD directory (where we run from - should be ignored)
|
||||||
|
cwdDir := t.TempDir()
|
||||||
|
// Explicitly do NOT create .beads here - doctor should not check CWD
|
||||||
|
|
||||||
|
// Save original working directory
|
||||||
|
origDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to CWD (no .beads)
|
||||||
|
if err := os.Chdir(cwdDir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = os.Chdir(origDir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set BEADS_DIR to point to target/.beads
|
||||||
|
t.Setenv("BEADS_DIR", targetBeadsDir)
|
||||||
|
|
||||||
|
// The doctor command's Run function determines path from BEADS_DIR
|
||||||
|
// We test that by verifying runDiagnostics receives the parent of BEADS_DIR
|
||||||
|
// Direct call: runDiagnostics uses the path it's given
|
||||||
|
// Through cobra: Run function computes path from BEADS_DIR
|
||||||
|
|
||||||
|
// Test the path resolution logic directly
|
||||||
|
beadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
if beadsDir == "" {
|
||||||
|
t.Fatal("BEADS_DIR should be set")
|
||||||
|
}
|
||||||
|
checkPath := filepath.Dir(beadsDir) // Parent of .beads
|
||||||
|
|
||||||
|
// Verify we get the target directory, not CWD
|
||||||
|
if checkPath != targetDir {
|
||||||
|
t.Errorf("Expected checkPath to be %s, got %s", targetDir, checkPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run diagnostics on the computed path (simulates what cobra Run does)
|
||||||
|
result := runDiagnostics(checkPath)
|
||||||
|
|
||||||
|
// Should find .beads at target location
|
||||||
|
if len(result.Checks) == 0 {
|
||||||
|
t.Fatal("Expected at least one check")
|
||||||
|
}
|
||||||
|
installCheck := result.Checks[0]
|
||||||
|
if installCheck.Status != "ok" {
|
||||||
|
t.Errorf("Expected Installation to pass (found .beads at BEADS_DIR parent), got status=%s, message=%s",
|
||||||
|
installCheck.Status, installCheck.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDoctor_ExplicitPathOverridesBEADS_DIR tests that explicit path arg takes precedence
|
||||||
|
func TestDoctor_ExplicitPathOverridesBEADS_DIR(t *testing.T) {
|
||||||
|
// Create two directories: one for explicit path, one for BEADS_DIR
|
||||||
|
explicitDir := t.TempDir()
|
||||||
|
explicitBeadsDir := filepath.Join(explicitDir, ".beads")
|
||||||
|
if err := os.Mkdir(explicitBeadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Mark explicit with a file so we can verify it was used
|
||||||
|
if err := os.WriteFile(filepath.Join(explicitBeadsDir, "explicit-marker"), []byte("explicit"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beadsDirTarget := t.TempDir()
|
||||||
|
beadsDirBeads := filepath.Join(beadsDirTarget, ".beads")
|
||||||
|
if err := os.Mkdir(beadsDirBeads, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set BEADS_DIR
|
||||||
|
t.Setenv("BEADS_DIR", beadsDirBeads)
|
||||||
|
|
||||||
|
// Test precedence: explicit arg > BEADS_DIR > CWD
|
||||||
|
// When explicit path is given, BEADS_DIR should be ignored
|
||||||
|
|
||||||
|
// Simulate the logic from doctor.go's Run function:
|
||||||
|
args := []string{explicitDir}
|
||||||
|
var checkPath string
|
||||||
|
if len(args) > 0 {
|
||||||
|
checkPath = args[0]
|
||||||
|
} else if beadsDir := os.Getenv("BEADS_DIR"); beadsDir != "" {
|
||||||
|
checkPath = filepath.Dir(beadsDir)
|
||||||
|
} else {
|
||||||
|
checkPath = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should use explicit path, not BEADS_DIR
|
||||||
|
if checkPath != explicitDir {
|
||||||
|
t.Errorf("Expected explicit path %s to take precedence, got %s", explicitDir, checkPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the marker file exists at the chosen path
|
||||||
|
markerPath := filepath.Join(checkPath, ".beads", "explicit-marker")
|
||||||
|
if _, err := os.Stat(markerPath); os.IsNotExist(err) {
|
||||||
|
t.Error("Expected to find explicit-marker in chosen path - wrong directory was selected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
191
cmd/bd/init.go
191
cmd/bd/init.go
@@ -148,23 +148,34 @@ and --server-user. Password should be set via BEADS_DOLT_PASSWORD environment va
|
|||||||
// The hyphen is added automatically during ID generation
|
// The hyphen is added automatically during ID generation
|
||||||
prefix = strings.TrimRight(prefix, "-")
|
prefix = strings.TrimRight(prefix, "-")
|
||||||
|
|
||||||
|
// Determine beadsDir first (used for all storage path calculations).
|
||||||
|
// BEADS_DIR takes precedence, otherwise use CWD/.beads (with redirect support).
|
||||||
|
// This must be computed BEFORE initDBPath to ensure consistent path resolution
|
||||||
|
// (avoiding macOS /var -> /private/var symlink issues when directory creation
|
||||||
|
// happens between path computations).
|
||||||
|
var beadsDirForInit string
|
||||||
|
if envBeadsDir := os.Getenv("BEADS_DIR"); envBeadsDir != "" {
|
||||||
|
beadsDirForInit = utils.CanonicalizePath(envBeadsDir)
|
||||||
|
} else {
|
||||||
|
localBeadsDir := filepath.Join(".", ".beads")
|
||||||
|
beadsDirForInit = beads.FollowRedirect(localBeadsDir)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine storage path.
|
// Determine storage path.
|
||||||
//
|
//
|
||||||
// IMPORTANT: In Dolt mode, we must NOT create a SQLite database file.
|
// IMPORTANT: In Dolt mode, we must NOT create a SQLite database file.
|
||||||
// `initDBPath` is used for SQLite-specific tasks (migration, import helpers, etc),
|
// `initDBPath` is used for SQLite-specific tasks (migration, import helpers, etc),
|
||||||
// so in Dolt mode it should point to the Dolt directory instead.
|
// so in Dolt mode it should point to the Dolt directory instead.
|
||||||
//
|
//
|
||||||
// Use global dbPath if set via --db flag or BEADS_DB env var (SQLite-only),
|
// Precedence: --db > BEADS_DB > BEADS_DIR > default (.beads/beads.db)
|
||||||
// otherwise default to `.beads/beads.db` for SQLite.
|
|
||||||
// If there's a redirect file, use the redirect target (GH#bd-0qel)
|
// If there's a redirect file, use the redirect target (GH#bd-0qel)
|
||||||
initDBPath := dbPath
|
initDBPath := dbPath
|
||||||
if backend == configfile.BackendDolt {
|
if backend == configfile.BackendDolt {
|
||||||
initDBPath = filepath.Join(".beads", "dolt")
|
// Dolt backend: use computed beadsDirForInit
|
||||||
|
initDBPath = filepath.Join(beadsDirForInit, "dolt")
|
||||||
} else if initDBPath == "" {
|
} else if initDBPath == "" {
|
||||||
// Check for redirect in local .beads
|
// SQLite backend: use computed beadsDirForInit
|
||||||
localBeadsDir := filepath.Join(".", ".beads")
|
initDBPath = filepath.Join(beadsDirForInit, beads.CanonicalDatabaseName)
|
||||||
targetBeadsDir := beads.FollowRedirect(localBeadsDir)
|
|
||||||
initDBPath = filepath.Join(targetBeadsDir, beads.CanonicalDatabaseName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate old SQLite database files if they exist (SQLite backend only).
|
// Migrate old SQLite database files if they exist (SQLite backend only).
|
||||||
@@ -211,11 +222,9 @@ and --server-user. Password should be set via BEADS_DOLT_PASSWORD environment va
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var beadsDir string
|
// Use the beadsDir computed earlier (before any directory creation)
|
||||||
// For regular repos, use current directory
|
// to ensure consistent path representation.
|
||||||
// But first check if there's a redirect file - if so, use the redirect target (GH#bd-0qel)
|
beadsDir := beadsDirForInit
|
||||||
localBeadsDir := filepath.Join(cwd, ".beads")
|
|
||||||
beadsDir = beads.FollowRedirect(localBeadsDir)
|
|
||||||
|
|
||||||
// Prevent nested .beads directories
|
// Prevent nested .beads directories
|
||||||
// Check if current working directory is inside a .beads directory
|
// Check if current working directory is inside a .beads directory
|
||||||
@@ -850,6 +859,78 @@ func readFirstIssueFromGit(jsonlPath, gitRef string) (*types.Issue, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkExistingBeadsDataAt checks for existing database at a specific beadsDir path.
|
||||||
|
// This is extracted to support both BEADS_DIR and CWD-based resolution.
|
||||||
|
func checkExistingBeadsDataAt(beadsDir string, prefix string) error {
|
||||||
|
// Check if .beads directory exists
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
return nil // No .beads directory, safe to init
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing database (SQLite or Dolt)
|
||||||
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.GetBackend() == configfile.BackendDolt {
|
||||||
|
doltPath := filepath.Join(beadsDir, "dolt")
|
||||||
|
if info, err := os.Stat(doltPath); err == nil && info.IsDir() {
|
||||||
|
return fmt.Errorf(`
|
||||||
|
%s Found existing Dolt database: %s
|
||||||
|
|
||||||
|
This workspace is already initialized.
|
||||||
|
|
||||||
|
To use the existing database:
|
||||||
|
Just run bd commands normally (e.g., %s)
|
||||||
|
|
||||||
|
To completely reinitialize (data loss warning):
|
||||||
|
rm -rf %s && bd init --backend dolt --prefix %s
|
||||||
|
|
||||||
|
Aborting.`, ui.RenderWarn("⚠"), doltPath, ui.RenderAccent("bd list"), beadsDir, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for redirect file - if present, check the redirect target
|
||||||
|
redirectTarget := beads.FollowRedirect(beadsDir)
|
||||||
|
if redirectTarget != beadsDir {
|
||||||
|
targetDBPath := filepath.Join(redirectTarget, beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(targetDBPath); err == nil {
|
||||||
|
return fmt.Errorf(`
|
||||||
|
%s Cannot init: redirect target already has database
|
||||||
|
|
||||||
|
Local .beads redirects to: %s
|
||||||
|
That location already has: %s
|
||||||
|
|
||||||
|
The redirect target is already initialized. Running init here would overwrite it.
|
||||||
|
|
||||||
|
To use the existing database:
|
||||||
|
Just run bd commands normally (e.g., %s)
|
||||||
|
The redirect will route to the canonical database.
|
||||||
|
|
||||||
|
To reinitialize the canonical location (data loss warning):
|
||||||
|
rm %s && bd init --prefix %s
|
||||||
|
|
||||||
|
Aborting.`, ui.RenderWarn("⚠"), redirectTarget, targetDBPath, ui.RenderAccent("bd list"), targetDBPath, prefix)
|
||||||
|
}
|
||||||
|
return nil // Redirect target has no database - safe to init
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing database file (no redirect case)
|
||||||
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(dbPath); err == nil {
|
||||||
|
return fmt.Errorf(`
|
||||||
|
%s Found existing database: %s
|
||||||
|
|
||||||
|
This workspace is already initialized.
|
||||||
|
|
||||||
|
To use the existing database:
|
||||||
|
Just run bd commands normally (e.g., %s)
|
||||||
|
|
||||||
|
To completely reinitialize (data loss warning):
|
||||||
|
rm -rf %s && bd init --prefix %s
|
||||||
|
|
||||||
|
Aborting.`, ui.RenderWarn("⚠"), dbPath, ui.RenderAccent("bd list"), beadsDir, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil // No database found, safe to init
|
||||||
|
}
|
||||||
|
|
||||||
// checkExistingBeadsData checks for existing database files
|
// checkExistingBeadsData checks for existing database files
|
||||||
// and returns an error if found (safety guard for bd-emg)
|
// and returns an error if found (safety guard for bd-emg)
|
||||||
//
|
//
|
||||||
@@ -863,6 +944,13 @@ func readFirstIssueFromGit(jsonlPath, gitRef string) (*types.Issue, error) {
|
|||||||
// For redirects, checks the redirect target and errors if it already has a database.
|
// For redirects, checks the redirect target and errors if it already has a database.
|
||||||
// This prevents accidentally overwriting an existing canonical database (GH#bd-0qel).
|
// This prevents accidentally overwriting an existing canonical database (GH#bd-0qel).
|
||||||
func checkExistingBeadsData(prefix string) error {
|
func checkExistingBeadsData(prefix string) error {
|
||||||
|
// Check BEADS_DIR environment variable first (matches FindBeadsDir pattern)
|
||||||
|
// When BEADS_DIR is set, it takes precedence over CWD and worktree checks
|
||||||
|
if envBeadsDir := os.Getenv("BEADS_DIR"); envBeadsDir != "" {
|
||||||
|
absBeadsDir := utils.CanonicalizePath(envBeadsDir)
|
||||||
|
return checkExistingBeadsDataAt(absBeadsDir, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil // Can't determine CWD, allow init to proceed
|
return nil // Can't determine CWD, allow init to proceed
|
||||||
@@ -884,82 +972,5 @@ func checkExistingBeadsData(prefix string) error {
|
|||||||
beadsDir = filepath.Join(cwd, ".beads")
|
beadsDir = filepath.Join(cwd, ".beads")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if .beads directory exists
|
return checkExistingBeadsDataAt(beadsDir, prefix)
|
||||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
|
||||||
return nil // No .beads directory, safe to init
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing database (SQLite or Dolt)
|
|
||||||
//
|
|
||||||
// NOTE: For Dolt backend, the "database" is a directory at `.beads/dolt/`.
|
|
||||||
// We prefer metadata.json as the single source of truth, but we also keep a
|
|
||||||
// conservative fallback for legacy SQLite setups.
|
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.GetBackend() == configfile.BackendDolt {
|
|
||||||
doltPath := filepath.Join(beadsDir, "dolt")
|
|
||||||
if info, err := os.Stat(doltPath); err == nil && info.IsDir() {
|
|
||||||
return fmt.Errorf(`
|
|
||||||
%s Found existing Dolt database: %s
|
|
||||||
|
|
||||||
This workspace is already initialized.
|
|
||||||
|
|
||||||
To use the existing database:
|
|
||||||
Just run bd commands normally (e.g., %s)
|
|
||||||
|
|
||||||
To completely reinitialize (data loss warning):
|
|
||||||
rm -rf .beads && bd init --backend dolt --prefix %s
|
|
||||||
|
|
||||||
Aborting.`, ui.RenderWarn("⚠"), doltPath, ui.RenderAccent("bd list"), prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for redirect file - if present, we need to check the redirect target (GH#bd-0qel)
|
|
||||||
redirectTarget := beads.FollowRedirect(beadsDir)
|
|
||||||
if redirectTarget != beadsDir {
|
|
||||||
// There's a redirect - check if the target already has a database
|
|
||||||
targetDBPath := filepath.Join(redirectTarget, beads.CanonicalDatabaseName)
|
|
||||||
if _, err := os.Stat(targetDBPath); err == nil {
|
|
||||||
return fmt.Errorf(`
|
|
||||||
%s Cannot init: redirect target already has database
|
|
||||||
|
|
||||||
Local .beads redirects to: %s
|
|
||||||
That location already has: %s
|
|
||||||
|
|
||||||
The redirect target is already initialized. Running init here would overwrite it.
|
|
||||||
|
|
||||||
To use the existing database:
|
|
||||||
Just run bd commands normally (e.g., %s)
|
|
||||||
The redirect will route to the canonical database.
|
|
||||||
|
|
||||||
To reinitialize the canonical location (data loss warning):
|
|
||||||
rm %s && bd init --prefix %s
|
|
||||||
|
|
||||||
Aborting.`, ui.RenderWarn("⚠"), redirectTarget, targetDBPath, ui.RenderAccent("bd list"), targetDBPath, prefix)
|
|
||||||
}
|
|
||||||
// Redirect target has no database - safe to init there
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing database file (no redirect case)
|
|
||||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
|
||||||
if _, err := os.Stat(dbPath); err == nil {
|
|
||||||
return fmt.Errorf(`
|
|
||||||
%s Found existing database: %s
|
|
||||||
|
|
||||||
This workspace is already initialized.
|
|
||||||
|
|
||||||
To use the existing database:
|
|
||||||
Just run bd commands normally (e.g., %s)
|
|
||||||
|
|
||||||
To completely reinitialize (data loss warning):
|
|
||||||
rm -rf .beads && bd init --prefix %s
|
|
||||||
|
|
||||||
Aborting.`, ui.RenderWarn("⚠"), dbPath, ui.RenderAccent("bd list"), prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fresh clones (JSONL exists but no database) are allowed - init will
|
|
||||||
// create the database and import from JSONL automatically.
|
|
||||||
// This fixes the circular dependency where init told users to run
|
|
||||||
// "bd doctor --fix" but doctor couldn't create a database (bd-4h9).
|
|
||||||
|
|
||||||
return nil // No database found, safe to init
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
|||||||
return fmt.Errorf("failed to get home directory: %w", err)
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use BEADS_DIR as default if set (user explicitly set it and continued past warning)
|
||||||
|
// Otherwise fall back to ~/.beads-planning
|
||||||
defaultPlanningRepo := filepath.Join(homeDir, ".beads-planning")
|
defaultPlanningRepo := filepath.Join(homeDir, ".beads-planning")
|
||||||
|
if envBeadsDir := os.Getenv("BEADS_DIR"); envBeadsDir != "" {
|
||||||
|
defaultPlanningRepo = envBeadsDir
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("\nWhere should contributor planning issues be stored?\n")
|
fmt.Printf("\nWhere should contributor planning issues be stored?\n")
|
||||||
fmt.Printf("Default: %s\n", ui.RenderAccent(defaultPlanningRepo))
|
fmt.Printf("Default: %s\n", ui.RenderAccent(defaultPlanningRepo))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -579,6 +580,9 @@ func TestInitNoDbMode(t *testing.T) {
|
|||||||
dbPath = ""
|
dbPath = ""
|
||||||
noDb = false
|
noDb = false
|
||||||
|
|
||||||
|
// Reset Cobra flags - critical for --no-db to work correctly
|
||||||
|
rootCmd.PersistentFlags().Set("no-db", "false")
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
t.Chdir(tmpDir)
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
@@ -602,13 +606,36 @@ func TestInitNoDbMode(t *testing.T) {
|
|||||||
// Initialize with --no-db flag
|
// Initialize with --no-db flag
|
||||||
rootCmd.SetArgs([]string{"init", "--no-db", "--no-daemon", "--prefix", "test", "--quiet"})
|
rootCmd.SetArgs([]string{"init", "--no-db", "--no-daemon", "--prefix", "test", "--quiet"})
|
||||||
|
|
||||||
|
t.Logf("DEBUG: noDb before Execute=%v", noDb)
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
t.Fatalf("Init with --no-db failed: %v", err)
|
t.Fatalf("Init with --no-db failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Logf("DEBUG: noDb after Execute=%v", noDb)
|
||||||
|
|
||||||
|
// Debug: Check where files were created
|
||||||
|
beadsDirEnv := os.Getenv("BEADS_DIR")
|
||||||
|
t.Logf("DEBUG: tmpDir=%s", tmpDir)
|
||||||
|
t.Logf("DEBUG: BEADS_DIR=%s", beadsDirEnv)
|
||||||
|
t.Logf("DEBUG: CWD=%s", func() string { cwd, _ := os.Getwd(); return cwd }())
|
||||||
|
|
||||||
|
// Check what files exist in tmpDir
|
||||||
|
entries, _ := os.ReadDir(tmpDir)
|
||||||
|
t.Logf("DEBUG: entries in tmpDir: %v", entries)
|
||||||
|
if beadsDirEnv != "" {
|
||||||
|
beadsEntries, err := os.ReadDir(beadsDirEnv)
|
||||||
|
t.Logf("DEBUG: entries in BEADS_DIR: %v (err: %v)", beadsEntries, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify issues.jsonl was created
|
// Verify issues.jsonl was created
|
||||||
jsonlPath := filepath.Join(tmpDir, ".beads", "issues.jsonl")
|
jsonlPath := filepath.Join(tmpDir, ".beads", "issues.jsonl")
|
||||||
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||||
|
// Also check at BEADS_DIR directly
|
||||||
|
beadsDirJsonlPath := filepath.Join(beadsDirEnv, "issues.jsonl")
|
||||||
|
if _, err2 := os.Stat(beadsDirJsonlPath); err2 == nil {
|
||||||
|
t.Logf("DEBUG: issues.jsonl found at BEADS_DIR path: %s", beadsDirJsonlPath)
|
||||||
|
}
|
||||||
t.Error("issues.jsonl was not created in --no-db mode")
|
t.Error("issues.jsonl was not created in --no-db mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,3 +1670,384 @@ func TestInitWithRedirectToExistingDatabase(t *testing.T) {
|
|||||||
t.Errorf("Canonical database prefix should still be 'existing', got %q (was overwritten!)", prefix)
|
t.Errorf("Canonical database prefix should still be 'existing', got %q (was overwritten!)", prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BEADS_DIR Tests
|
||||||
|
// =============================================================================
|
||||||
|
// These tests verify that bd init respects the BEADS_DIR environment variable
|
||||||
|
// for both safety checks and database creation.
|
||||||
|
|
||||||
|
// TestCheckExistingBeadsData_WithBEADS_DIR verifies that checkExistingBeadsData
|
||||||
|
// uses BEADS_DIR instead of CWD when the environment variable is set.
|
||||||
|
// This tests requirements FR-001, FR-004.
|
||||||
|
func TestCheckExistingBeadsData_WithBEADS_DIR(t *testing.T) {
|
||||||
|
// Save and restore BEADS_DIR
|
||||||
|
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
defer func() {
|
||||||
|
if origBeadsDir != "" {
|
||||||
|
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("BEADS_DIR")
|
||||||
|
}
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("TC-002: BEADS_DIR set, no existing DB", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create BEADS_DIR location (no database)
|
||||||
|
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||||
|
os.MkdirAll(beadsDirPath, 0755)
|
||||||
|
|
||||||
|
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||||
|
beads.ResetCaches()
|
||||||
|
|
||||||
|
// Should succeed because BEADS_DIR has no database
|
||||||
|
err := checkExistingBeadsData("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error when BEADS_DIR has no database, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TC-003: BEADS_DIR set, CWD has .beads, should ignore CWD", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create CWD with existing database (should be ignored)
|
||||||
|
cwdBeadsDir := filepath.Join(tmpDir, "cwd", ".beads")
|
||||||
|
os.MkdirAll(cwdBeadsDir, 0755)
|
||||||
|
cwdDBPath := filepath.Join(cwdBeadsDir, beads.CanonicalDatabaseName)
|
||||||
|
store, err := sqlite.New(context.Background(), cwdDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
store.Close()
|
||||||
|
|
||||||
|
// Create BEADS_DIR location (no database)
|
||||||
|
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||||
|
os.MkdirAll(beadsDirPath, 0755)
|
||||||
|
|
||||||
|
// Set BEADS_DIR - should check external, not CWD
|
||||||
|
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||||
|
beads.ResetCaches()
|
||||||
|
|
||||||
|
// Change to CWD with database
|
||||||
|
origWd, _ := os.Getwd()
|
||||||
|
os.Chdir(filepath.Join(tmpDir, "cwd"))
|
||||||
|
defer os.Chdir(origWd)
|
||||||
|
|
||||||
|
// Should succeed because BEADS_DIR has no database (CWD ignored)
|
||||||
|
err = checkExistingBeadsData("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error when BEADS_DIR has no database (CWD should be ignored), got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TC-004: BEADS_DIR set, target exists with DB, should error", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create BEADS_DIR with existing database
|
||||||
|
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||||
|
os.MkdirAll(beadsDirPath, 0755)
|
||||||
|
dbPath := filepath.Join(beadsDirPath, beads.CanonicalDatabaseName)
|
||||||
|
store, err := sqlite.New(context.Background(), dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
store.Close()
|
||||||
|
|
||||||
|
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||||
|
beads.ResetCaches()
|
||||||
|
|
||||||
|
// Should error because BEADS_DIR already has database
|
||||||
|
err = checkExistingBeadsData("test")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when BEADS_DIR already has database")
|
||||||
|
}
|
||||||
|
// FR-005: Error message should reference the BEADS_DIR path
|
||||||
|
if !strings.Contains(err.Error(), beadsDirPath) {
|
||||||
|
t.Errorf("Expected error to mention BEADS_DIR path %s, got: %v", beadsDirPath, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInit_WithBEADS_DIR verifies that bd init creates the database at BEADS_DIR
|
||||||
|
// when the environment variable is set.
|
||||||
|
// This tests requirements FR-002.
|
||||||
|
func TestInit_WithBEADS_DIR(t *testing.T) {
|
||||||
|
// Skip on Windows - init has platform-specific behaviors
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Skipping BEADS_DIR test on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Save and restore BEADS_DIR
|
||||||
|
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
defer func() {
|
||||||
|
if origBeadsDir != "" {
|
||||||
|
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("BEADS_DIR")
|
||||||
|
}
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("prefix", "")
|
||||||
|
initCmd.Flags().Set("quiet", "false")
|
||||||
|
initCmd.Flags().Set("backend", "")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create external BEADS_DIR location
|
||||||
|
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||||
|
os.MkdirAll(filepath.Dir(beadsDirPath), 0755) // Create parent, not .beads itself
|
||||||
|
|
||||||
|
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
|
||||||
|
// Change to a different working directory
|
||||||
|
cwdPath := filepath.Join(tmpDir, "workdir")
|
||||||
|
os.MkdirAll(cwdPath, 0755)
|
||||||
|
t.Chdir(cwdPath)
|
||||||
|
|
||||||
|
// Run bd init with quiet flag
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "beadsdir-test", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init with BEADS_DIR failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was created at BEADS_DIR, not CWD
|
||||||
|
expectedDBPath := filepath.Join(beadsDirPath, beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(expectedDBPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Database was not created at BEADS_DIR path: %s", expectedDBPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was NOT created at CWD
|
||||||
|
cwdDBPath := filepath.Join(cwdPath, ".beads", beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(cwdDBPath); err == nil {
|
||||||
|
t.Errorf("Database should NOT have been created at CWD: %s", cwdDBPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database has correct prefix
|
||||||
|
store, err := openExistingTestDB(t, expectedDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open database at BEADS_DIR: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||||
|
}
|
||||||
|
if prefix != "beadsdir-test" {
|
||||||
|
t.Errorf("Expected prefix 'beadsdir-test', got %q", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInit_WithBEADS_DIR_DoltBackend verifies that bd init with Dolt backend
|
||||||
|
// creates the database at BEADS_DIR when the environment variable is set.
|
||||||
|
// This tests requirements FR-002 for Dolt backend.
|
||||||
|
func TestInit_WithBEADS_DIR_DoltBackend(t *testing.T) {
|
||||||
|
// Skip on Windows
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Skipping BEADS_DIR Dolt test on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dolt is available
|
||||||
|
if _, err := exec.LookPath("dolt"); err != nil {
|
||||||
|
t.Skip("Dolt not installed, skipping Dolt backend test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Save and restore BEADS_DIR
|
||||||
|
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
defer func() {
|
||||||
|
if origBeadsDir != "" {
|
||||||
|
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("BEADS_DIR")
|
||||||
|
}
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("prefix", "")
|
||||||
|
initCmd.Flags().Set("quiet", "false")
|
||||||
|
initCmd.Flags().Set("backend", "")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create external BEADS_DIR location
|
||||||
|
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||||
|
os.MkdirAll(filepath.Dir(beadsDirPath), 0755)
|
||||||
|
|
||||||
|
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
|
||||||
|
// Change to a different working directory
|
||||||
|
cwdPath := filepath.Join(tmpDir, "workdir")
|
||||||
|
os.MkdirAll(cwdPath, 0755)
|
||||||
|
t.Chdir(cwdPath)
|
||||||
|
|
||||||
|
// Run bd init with Dolt backend
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "dolt-test", "--backend", "dolt", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init with BEADS_DIR and Dolt backend failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Dolt database was created at BEADS_DIR
|
||||||
|
expectedDoltPath := filepath.Join(beadsDirPath, "dolt")
|
||||||
|
if info, err := os.Stat(expectedDoltPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Dolt database was not created at BEADS_DIR path: %s", expectedDoltPath)
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
t.Errorf("Expected Dolt path to be a directory: %s", expectedDoltPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was NOT created at CWD
|
||||||
|
cwdDoltPath := filepath.Join(cwdPath, ".beads", "dolt")
|
||||||
|
if _, err := os.Stat(cwdDoltPath); err == nil {
|
||||||
|
t.Errorf("Dolt database should NOT have been created at CWD: %s", cwdDoltPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInit_WithoutBEADS_DIR_NoBehaviorChange verifies that existing behavior
|
||||||
|
// is unchanged when BEADS_DIR is not set.
|
||||||
|
// This tests requirement NFR-001.
|
||||||
|
func TestInit_WithoutBEADS_DIR_NoBehaviorChange(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Ensure BEADS_DIR is not set
|
||||||
|
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
os.Unsetenv("BEADS_DIR")
|
||||||
|
defer func() {
|
||||||
|
if origBeadsDir != "" {
|
||||||
|
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||||
|
}
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("prefix", "")
|
||||||
|
initCmd.Flags().Set("quiet", "false")
|
||||||
|
initCmd.Flags().Set("backend", "")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Run bd init
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "no-beadsdir", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init without BEADS_DIR failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was created at CWD/.beads (default behavior)
|
||||||
|
expectedDBPath := filepath.Join(tmpDir, ".beads", beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(expectedDBPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Database was not created at default CWD/.beads path: %s", expectedDBPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database has correct prefix
|
||||||
|
store, err := openExistingTestDB(t, expectedDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||||
|
}
|
||||||
|
if prefix != "no-beadsdir" {
|
||||||
|
t.Errorf("Expected prefix 'no-beadsdir', got %q", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInit_BEADS_DB_OverridesBEADS_DIR verifies precedence: BEADS_DB > BEADS_DIR
|
||||||
|
// This ensures that explicit database path env var takes precedence over directory env var.
|
||||||
|
func TestInit_BEADS_DB_OverridesBEADS_DIR(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
beads.ResetCaches()
|
||||||
|
git.ResetCaches()
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("prefix", "")
|
||||||
|
initCmd.Flags().Set("quiet", "false")
|
||||||
|
initCmd.Flags().Set("backend", "")
|
||||||
|
|
||||||
|
// Create two target locations
|
||||||
|
beadsDirTarget := t.TempDir() // Where BEADS_DIR points (should be ignored)
|
||||||
|
beadsDBTarget := t.TempDir() // Where BEADS_DB points (should be used)
|
||||||
|
|
||||||
|
beadsDirBeads := filepath.Join(beadsDirTarget, ".beads")
|
||||||
|
if err := os.MkdirAll(beadsDirBeads, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beadsDBPath := filepath.Join(beadsDBTarget, "override.db")
|
||||||
|
|
||||||
|
// Set both env vars - BEADS_DB should take precedence
|
||||||
|
t.Setenv("BEADS_DIR", beadsDirBeads)
|
||||||
|
t.Setenv("BEADS_DB", beadsDBPath)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Run bd init
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "precedence", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init with BEADS_DB + BEADS_DIR failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was created at BEADS_DB location (not BEADS_DIR)
|
||||||
|
if _, err := os.Stat(beadsDBPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Database was NOT created at BEADS_DB path: %s", beadsDBPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify database was NOT created at BEADS_DIR location
|
||||||
|
beadsDirDBPath := filepath.Join(beadsDirBeads, beads.CanonicalDatabaseName)
|
||||||
|
if _, err := os.Stat(beadsDirDBPath); err == nil {
|
||||||
|
t.Errorf("Database was incorrectly created at BEADS_DIR path: %s (BEADS_DB should override)", beadsDirDBPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the database has correct prefix
|
||||||
|
store, err := openExistingTestDB(t, beadsDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||||
|
}
|
||||||
|
if prefix != "precedence" {
|
||||||
|
t.Errorf("Expected prefix 'precedence', got %q", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,10 +48,17 @@ const (
|
|||||||
TriggerChange = "change"
|
TriggerChange = "change"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSyncMode returns the configured sync mode from the database, defaulting to git-portable.
|
// GetSyncMode returns the configured sync mode, checking config.yaml first (where bd config set writes),
|
||||||
// This reads from storage.Storage (database), not config.yaml.
|
// then falling back to database. This fixes GH#1277 where yaml and database were inconsistent.
|
||||||
// For config.yaml access, use config.GetSyncMode() instead.
|
|
||||||
func GetSyncMode(ctx context.Context, s storage.Storage) string {
|
func GetSyncMode(ctx context.Context, s storage.Storage) string {
|
||||||
|
// First check config.yaml (where bd config set writes for sync.* keys)
|
||||||
|
yamlMode := config.GetSyncMode()
|
||||||
|
if yamlMode != "" && yamlMode != config.SyncModeGitPortable {
|
||||||
|
// Non-default value in yaml takes precedence
|
||||||
|
return string(yamlMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to database (legacy path)
|
||||||
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
|
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
|
||||||
if err != nil || mode == "" {
|
if err != nil || mode == "" {
|
||||||
return SyncModeGitPortable
|
return SyncModeGitPortable
|
||||||
|
|||||||
@@ -315,6 +315,18 @@ export BEADS_DIR=~/.beads-planning
|
|||||||
bd create "My task" -p 1
|
bd create "My task" -p 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: `bd init` and `bd doctor` also respect `BEADS_DIR`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize directly at BEADS_DIR location (no need to cd)
|
||||||
|
mkdir -p ~/.beads-planning/.beads
|
||||||
|
export BEADS_DIR=~/.beads-planning/.beads
|
||||||
|
bd init --prefix planning # Creates database at $BEADS_DIR
|
||||||
|
|
||||||
|
# Doctor checks BEADS_DIR location (not CWD)
|
||||||
|
bd doctor # Diagnoses database at $BEADS_DIR
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Routing Not Working
|
### Routing Not Working
|
||||||
|
|||||||
@@ -448,6 +448,20 @@ For users who want complete separation between code history and issue tracking,
|
|||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
|
**Option A: Initialize with BEADS_DIR (simplest)**
|
||||||
|
```bash
|
||||||
|
# 1. Create the directory structure
|
||||||
|
mkdir -p ~/my-project-beads/.beads
|
||||||
|
|
||||||
|
# 2. Set BEADS_DIR and initialize from anywhere
|
||||||
|
export BEADS_DIR=~/my-project-beads/.beads
|
||||||
|
bd init --prefix myproj # Creates database at $BEADS_DIR
|
||||||
|
|
||||||
|
# 3. Initialize git in the beads repo (optional, for sync)
|
||||||
|
cd ~/my-project-beads && git init
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Traditional approach**
|
||||||
```bash
|
```bash
|
||||||
# 1. Create a dedicated beads repository (one-time)
|
# 1. Create a dedicated beads repository (one-time)
|
||||||
mkdir ~/my-project-beads
|
mkdir ~/my-project-beads
|
||||||
|
|||||||
Reference in New Issue
Block a user