Skip to content

Commit eae64ca

Browse files
steveyeggeclaude
andcommitted
fix: detect backup files on database-not-found and after bd init (GH#2327)
When switching git branches, the Dolt database does not travel with the branch, causing database not found errors. Previously the error message only mentioned server misconfiguration. Now it: 1. Checks for .beads/backup/*.jsonl files and suggests bd backup restore 2. Lists branch switching as a common cause 3. After bd init creates a fresh DB, warns if backup files exist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c96d89 commit eae64ca

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

cmd/bd/init.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,16 @@ environment variable.`,
882882
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
883883
fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd quickstart"))
884884

885+
// Detect backup files from a previous session (GH#2327).
886+
// This catches the branch-switch scenario: user ran bd init on a new
887+
// branch and the database was created fresh, but backup JSONL files
888+
// exist from a prior backup on this or another branch.
889+
if !bootstrappedFromRemote && dolt.HasBackupFiles(beadsDir) {
890+
fmt.Printf(" %s Backup files detected in .beads/backup/\n", ui.RenderWarn("!"))
891+
fmt.Printf(" To restore issues from a previous backup, run:\n")
892+
fmt.Printf(" %s\n\n", ui.RenderAccent("bd backup restore"))
893+
}
894+
885895
// Run limited diagnostics to verify init succeeded.
886896
// Uses runInitDiagnostics (not runDiagnostics) to only check things
887897
// that should be true immediately after init — skips git-dependent,

internal/storage/dolt/errors.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"database/sql"
55
"errors"
66
"fmt"
7+
"os"
8+
"path/filepath"
79
"strings"
810

911
mysql "github.com/go-sql-driver/mysql"
@@ -91,12 +93,25 @@ func wrapExecError(op string, err error) error {
9193
}
9294

9395
// databaseNotFoundError builds the "database not found" error with a config-aware
94-
// hint about sync.git-remote. Extracted from openServerConnection for testability.
96+
// hint about sync.git-remote and backup recovery. Extracted from openServerConnection
97+
// for testability.
9598
func databaseNotFoundError(cfg *Config) error {
9699
var b strings.Builder
97100
fmt.Fprintf(&b, "database %q not found on Dolt server at %s:%d\n\n", cfg.Database, cfg.ServerHost, cfg.ServerPort)
98-
b.WriteString("This usually means a server configuration problem, NOT a missing database.\n")
101+
102+
// Check if backup files exist — strong signal this is a branch-switch or
103+
// fresh-clone scenario rather than a server misconfiguration (GH#2327).
104+
if HasBackupFiles(cfg.BeadsDir) {
105+
b.WriteString("Backup files found in .beads/backup/ — this may be a branch-switch\n")
106+
b.WriteString("or fresh-clone scenario where the Dolt database doesn't exist yet.\n\n")
107+
b.WriteString("To restore your issues:\n")
108+
b.WriteString(" bd init --prefix <prefix> # Initialize the database\n")
109+
b.WriteString(" bd backup restore # Restore issues from backup\n\n")
110+
b.WriteString("If this is NOT a branch switch, see common causes below.\n\n")
111+
}
112+
99113
b.WriteString("Common causes:\n")
114+
b.WriteString(" - Switched git branches (the Dolt database is runtime state, not in git)\n")
100115
b.WriteString(" - The server is serving a different data directory than expected\n")
101116
b.WriteString(" - The server was restarted and is using a different port\n")
102117
b.WriteString(" - Another project's Dolt server is running on this port\n\n")
@@ -112,3 +127,22 @@ func databaseNotFoundError(cfg *Config) error {
112127

113128
return errors.New(b.String())
114129
}
130+
131+
// HasBackupFiles checks whether .beads/backup/ contains any JSONL files,
132+
// indicating a prior backup that could be restored (GH#2327).
133+
func HasBackupFiles(beadsDir string) bool {
134+
if beadsDir == "" {
135+
return false
136+
}
137+
backupDir := filepath.Join(beadsDir, "backup")
138+
entries, err := os.ReadDir(backupDir)
139+
if err != nil {
140+
return false
141+
}
142+
for _, e := range entries {
143+
if !e.IsDir() && strings.HasSuffix(e.Name(), ".jsonl") {
144+
return true
145+
}
146+
}
147+
return false
148+
}

internal/storage/dolt/errors_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"database/sql"
55
"errors"
66
"fmt"
7+
"os"
8+
"path/filepath"
79
"strings"
810
"testing"
911

@@ -147,4 +149,97 @@ func TestDatabaseNotFoundHint(t *testing.T) {
147149
t.Errorf("expected bd init suggestion, got:\n%s", msg)
148150
}
149151
})
152+
153+
t.Run("hint detects backup files in beads dir (GH#2327)", func(t *testing.T) {
154+
// Create a temp .beads/backup/ with a JSONL file
155+
tmpDir := t.TempDir()
156+
backupDir := filepath.Join(tmpDir, "backup")
157+
if err := os.MkdirAll(backupDir, 0700); err != nil {
158+
t.Fatal(err)
159+
}
160+
if err := os.WriteFile(filepath.Join(backupDir, "issues.jsonl"), []byte(`{"id":"x"}`), 0600); err != nil {
161+
t.Fatal(err)
162+
}
163+
164+
cfg := baseCfg
165+
cfg.BeadsDir = tmpDir
166+
err := databaseNotFoundError(&cfg)
167+
msg := err.Error()
168+
169+
if !strings.Contains(msg, "Backup files found") {
170+
t.Errorf("expected backup detection hint, got:\n%s", msg)
171+
}
172+
if !strings.Contains(msg, "bd backup restore") {
173+
t.Errorf("expected bd backup restore suggestion, got:\n%s", msg)
174+
}
175+
// Should still mention branch switching as a common cause
176+
if !strings.Contains(msg, "branch") {
177+
t.Errorf("expected branch-switch mention, got:\n%s", msg)
178+
}
179+
})
180+
181+
t.Run("no backup hint when no backup files exist", func(t *testing.T) {
182+
tmpDir := t.TempDir()
183+
184+
cfg := baseCfg
185+
cfg.BeadsDir = tmpDir
186+
err := databaseNotFoundError(&cfg)
187+
msg := err.Error()
188+
189+
if strings.Contains(msg, "Backup files found") {
190+
t.Errorf("should not mention backups when none exist, got:\n%s", msg)
191+
}
192+
})
193+
}
194+
195+
func TestHasBackupFiles(t *testing.T) {
196+
t.Run("returns false for empty beadsDir", func(t *testing.T) {
197+
if HasBackupFiles("") {
198+
t.Error("expected false for empty beadsDir")
199+
}
200+
})
201+
202+
t.Run("returns false when backup dir does not exist", func(t *testing.T) {
203+
if HasBackupFiles(t.TempDir()) {
204+
t.Error("expected false when backup dir missing")
205+
}
206+
})
207+
208+
t.Run("returns false when backup dir is empty", func(t *testing.T) {
209+
tmpDir := t.TempDir()
210+
if err := os.MkdirAll(filepath.Join(tmpDir, "backup"), 0700); err != nil {
211+
t.Fatal(err)
212+
}
213+
if HasBackupFiles(tmpDir) {
214+
t.Error("expected false when backup dir is empty")
215+
}
216+
})
217+
218+
t.Run("returns false when backup dir has non-jsonl files", func(t *testing.T) {
219+
tmpDir := t.TempDir()
220+
backupDir := filepath.Join(tmpDir, "backup")
221+
if err := os.MkdirAll(backupDir, 0700); err != nil {
222+
t.Fatal(err)
223+
}
224+
if err := os.WriteFile(filepath.Join(backupDir, "state.json"), []byte("{}"), 0600); err != nil {
225+
t.Fatal(err)
226+
}
227+
if HasBackupFiles(tmpDir) {
228+
t.Error("expected false when only non-jsonl files present")
229+
}
230+
})
231+
232+
t.Run("returns true when backup dir has jsonl files", func(t *testing.T) {
233+
tmpDir := t.TempDir()
234+
backupDir := filepath.Join(tmpDir, "backup")
235+
if err := os.MkdirAll(backupDir, 0700); err != nil {
236+
t.Fatal(err)
237+
}
238+
if err := os.WriteFile(filepath.Join(backupDir, "issues.jsonl"), []byte(`{"id":"x"}`), 0600); err != nil {
239+
t.Fatal(err)
240+
}
241+
if !HasBackupFiles(tmpDir) {
242+
t.Error("expected true when jsonl files present")
243+
}
244+
})
150245
}

0 commit comments

Comments
 (0)