fix(init): error when redirect target already has database
When running `bd init` from a directory with a .beads/redirect file pointing to a canonical .beads/ that already has a database, init now errors instead of silently overwriting the existing database. This prevents accidental data loss when: - A project uses redirect to share a canonical database - Someone runs `bd init` from the redirected location - The canonical database was already initialized The error message clearly explains: - What happened (redirect target already has database) - Where the redirect points to - How to use the existing database (just run bd commands normally) - How to reinitialize if needed (rm the database first) Adds test: TestInitWithRedirectToExistingDatabase Part of GH#bd-0qel Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -805,6 +805,9 @@ func readFirstIssueFromGit(jsonlPath, gitRef string) (*types.Issue, error) {
|
|||||||
//
|
//
|
||||||
// For worktrees, checks the main repository root instead of current directory
|
// For worktrees, checks the main repository root instead of current directory
|
||||||
// since worktrees should share the database with the main repository.
|
// since worktrees should share the database with the main repository.
|
||||||
|
//
|
||||||
|
// 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).
|
||||||
func checkExistingBeadsData(prefix string) error {
|
func checkExistingBeadsData(prefix string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -855,6 +858,34 @@ 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)
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
if _, err := os.Stat(dbPath); err == nil {
|
if _, err := os.Stat(dbPath); err == nil {
|
||||||
return fmt.Errorf(`
|
return fmt.Errorf(`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/beads"
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/config"
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/git"
|
"github.com/steveyegge/beads/internal/git"
|
||||||
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInitCommand(t *testing.T) {
|
func TestInitCommand(t *testing.T) {
|
||||||
@@ -1551,3 +1552,94 @@ func TestInitWithRedirect(t *testing.T) {
|
|||||||
t.Errorf("Expected prefix 'redirect-test', got %q", prefix)
|
t.Errorf("Expected prefix 'redirect-test', got %q", prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestInitWithRedirectToExistingDatabase verifies that bd init errors when the redirect
|
||||||
|
// target already has a database, preventing accidental overwrites. (GH#bd-0qel)
|
||||||
|
func TestInitWithRedirectToExistingDatabase(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Clear BEADS_DIR to ensure we test the tree search path
|
||||||
|
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||||
|
os.Unsetenv("BEADS_DIR")
|
||||||
|
defer func() {
|
||||||
|
if origBeadsDir != "" {
|
||||||
|
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("prefix", "")
|
||||||
|
initCmd.Flags().Set("quiet", "false")
|
||||||
|
initCmd.Flags().Set("force", "false")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create canonical .beads directory with EXISTING database
|
||||||
|
canonicalDir := filepath.Join(tmpDir, "canonical")
|
||||||
|
canonicalBeadsDir := filepath.Join(canonicalDir, ".beads")
|
||||||
|
if err := os.MkdirAll(canonicalBeadsDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an existing database in canonical location
|
||||||
|
canonicalDBPath := filepath.Join(canonicalBeadsDir, "beads.db")
|
||||||
|
store, err := sqlite.New(context.Background(), canonicalDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create canonical database: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.SetConfig(context.Background(), "issue_prefix", "existing"); err != nil {
|
||||||
|
t.Fatalf("Failed to set prefix in canonical database: %v", err)
|
||||||
|
}
|
||||||
|
store.Close()
|
||||||
|
|
||||||
|
// Create project directory with redirect to canonical
|
||||||
|
projectDir := filepath.Join(tmpDir, "project")
|
||||||
|
projectBeadsDir := filepath.Join(projectDir, ".beads")
|
||||||
|
if err := os.MkdirAll(projectBeadsDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write redirect file pointing to canonical
|
||||||
|
redirectPath := filepath.Join(projectBeadsDir, beads.RedirectFileName)
|
||||||
|
if err := os.WriteFile(redirectPath, []byte("../canonical/.beads\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test checkExistingBeadsData directly since init uses os.Exit(1) which terminates tests
|
||||||
|
// Change to project directory first
|
||||||
|
origWd, _ := os.Getwd()
|
||||||
|
if err := os.Chdir(projectDir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(origWd)
|
||||||
|
|
||||||
|
// Call checkExistingBeadsData directly - should return error
|
||||||
|
err = checkExistingBeadsData("new-prefix")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected checkExistingBeadsData to return error when redirect target already has database")
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := err.Error()
|
||||||
|
if !strings.Contains(errorMsg, "redirect target already has database") {
|
||||||
|
t.Errorf("Expected error about redirect target having database, got: %s", errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify canonical database was NOT modified
|
||||||
|
store, err = openExistingTestDB(t, canonicalDBPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to reopen canonical 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 canonical database: %v", err)
|
||||||
|
}
|
||||||
|
if prefix != "existing" {
|
||||||
|
t.Errorf("Canonical database prefix should still be 'existing', got %q (was overwritten!)", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user