Skip to content

Commit 0827ccd

Browse files
steveyeggeclaude
andcommitted
fix: migrate server-root remotes to database dir on store open (GH#2118)
Users commonly run `dolt remote add` in .beads/dolt/ (server root) instead of .beads/dolt/<database>/ (where CLI push/pull runs). syncCLIRemotesToSQL now checks the server root for remotes missing from the database directory and propagates them to both CLI and SQL surfaces. Also improves the "remote not found" hint to explain the directory mismatch and recommend `bd dolt remote add` over raw `dolt remote add`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c51923d commit 0827ccd

File tree

3 files changed

+155
-4
lines changed

3 files changed

+155
-4
lines changed

cmd/bd/dolt.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,19 @@ uncommitted changes in its working set).`,
157157
if err := st.ForcePush(ctx); err != nil {
158158
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
159159
if isRemoteNotFoundErr(err) {
160-
fmt.Fprintf(os.Stderr, "Hint: run 'bd dolt remote add <name> <url>' to register the remote.\n")
160+
fmt.Fprintf(os.Stderr, "Hint: use 'bd dolt remote add <name> <url>' (not 'dolt remote add').\n")
161+
fmt.Fprintf(os.Stderr, " Running 'dolt remote add' directly may add the remote to the wrong directory.\n")
162+
fmt.Fprintf(os.Stderr, " Use 'bd dolt remote list' to check for discrepancies.\n")
161163
}
162164
os.Exit(1)
163165
}
164166
} else {
165167
if err := st.Push(ctx); err != nil {
166168
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
167169
if isRemoteNotFoundErr(err) {
168-
fmt.Fprintf(os.Stderr, "Hint: run 'bd dolt remote add <name> <url>' to register the remote.\n")
170+
fmt.Fprintf(os.Stderr, "Hint: use 'bd dolt remote add <name> <url>' (not 'dolt remote add').\n")
171+
fmt.Fprintf(os.Stderr, " Running 'dolt remote add' directly may add the remote to the wrong directory.\n")
172+
fmt.Fprintf(os.Stderr, " Use 'bd dolt remote list' to check for discrepancies.\n")
169173
}
170174
os.Exit(1)
171175
}
@@ -193,7 +197,9 @@ variables for authentication.`,
193197
if err := st.Pull(ctx); err != nil {
194198
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
195199
if isRemoteNotFoundErr(err) {
196-
fmt.Fprintf(os.Stderr, "Hint: run 'bd dolt remote add <name> <url>' to register the remote.\n")
200+
fmt.Fprintf(os.Stderr, "Hint: use 'bd dolt remote add <name> <url>' (not 'dolt remote add').\n")
201+
fmt.Fprintf(os.Stderr, " Running 'dolt remote add' directly may add the remote to the wrong directory.\n")
202+
fmt.Fprintf(os.Stderr, " Use 'bd dolt remote list' to check for discrepancies.\n")
197203
}
198204
os.Exit(1)
199205
}

internal/storage/dolt/federation.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package dolt
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"os/exec"
8+
"path/filepath"
79
"strings"
810
"time"
911

@@ -121,14 +123,32 @@ func (s *DoltStore) ListRemotes(ctx context.Context) ([]storage.RemoteInfo, erro
121123
// After a server restart, dolt_remotes (in-memory) is empty while CLI remotes
122124
// (persisted in .dolt/config) survive. This is best-effort: errors are silently
123125
// ignored because a missing remote will surface a clear error at push/pull time.
126+
//
127+
// GH#2118: Also checks the server root directory (dbPath) for remotes that were
128+
// added there instead of the database subdirectory (CLIDir). Users commonly run
129+
// `dolt remote add` in .beads/dolt/ (server root) rather than .beads/dolt/<db>/
130+
// (database dir). When found, these remotes are propagated to the database CLI
131+
// directory so that CLI push/pull can find them.
132+
//
124133
// See GH#2315.
125134
func (s *DoltStore) syncCLIRemotesToSQL(ctx context.Context) {
126135
dir := s.CLIDir()
127136
if dir == "" {
128137
return
129138
}
130139
cliRemotes, err := doltutil.ListCLIRemotes(dir)
131-
if err != nil || len(cliRemotes) == 0 {
140+
if err != nil {
141+
cliRemotes = nil
142+
}
143+
144+
// GH#2118: If the database directory has no remotes (or doesn't exist),
145+
// check the server root directory. Users often run `dolt remote add` in
146+
// .beads/dolt/ (server root) instead of .beads/dolt/<database>/.
147+
if len(cliRemotes) == 0 {
148+
cliRemotes = s.migrateServerRootRemotes(dir)
149+
}
150+
151+
if len(cliRemotes) == 0 {
132152
return
133153
}
134154
sqlRemotes, err := s.ListRemotes(ctx)
@@ -143,6 +163,39 @@ func (s *DoltStore) syncCLIRemotesToSQL(ctx context.Context) {
143163
}
144164
}
145165

166+
// migrateServerRootRemotes checks the dolt server root directory (dbPath) for
167+
// remotes that should be propagated to the database CLI directory (CLIDir).
168+
// This handles GH#2118: users run `dolt remote add` in .beads/dolt/ (server root)
169+
// instead of .beads/dolt/<database>/ (where CLI push/pull actually runs).
170+
//
171+
// Returns the combined CLI remotes after migration, or nil if nothing to migrate.
172+
func (s *DoltStore) migrateServerRootRemotes(cliDir string) []storage.RemoteInfo {
173+
rootDir := s.dbPath
174+
if rootDir == "" || rootDir == cliDir {
175+
return nil
176+
}
177+
// Server root must have .dolt/ (from dolt init)
178+
if _, err := os.Stat(filepath.Join(rootDir, ".dolt")); err != nil {
179+
return nil
180+
}
181+
rootRemotes, err := doltutil.ListCLIRemotes(rootDir)
182+
if err != nil || len(rootRemotes) == 0 {
183+
return nil
184+
}
185+
// Database dir must have .dolt/ to accept CLI remotes
186+
if _, err := os.Stat(filepath.Join(cliDir, ".dolt")); err != nil {
187+
return nil
188+
}
189+
// Propagate server-root remotes to the database directory
190+
for _, r := range rootRemotes {
191+
if existing := doltutil.FindCLIRemote(cliDir, r.Name); existing == "" {
192+
_ = doltutil.AddCLIRemote(cliDir, r.Name, r.URL)
193+
}
194+
}
195+
combined, _ := doltutil.ListCLIRemotes(cliDir)
196+
return combined
197+
}
198+
146199
// RemoveRemote removes a configured remote.
147200
func (s *DoltStore) RemoveRemote(ctx context.Context, name string) error {
148201
_, err := s.execContext(ctx, "CALL DOLT_REMOTE('remove', ?)", name)

internal/storage/dolt/federation_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"strings"
1011
"testing"
1112
"time"
1213

@@ -518,6 +519,97 @@ func TestSyncCLIRemotesToSQL(t *testing.T) {
518519
_ = store.RemoveRemote(ctx, remoteName)
519520
}
520521

522+
// TestMigrateServerRootRemotes verifies GH#2118: remotes added in the dolt
523+
// server root directory (.beads/dolt/) are propagated to the database
524+
// subdirectory (.beads/dolt/<database>/) during syncCLIRemotesToSQL.
525+
// This handles the common case where users run `dolt remote add` in the
526+
// visible server root instead of the database subdirectory.
527+
func TestMigrateServerRootRemotes(t *testing.T) {
528+
skipIfNoDolt(t)
529+
530+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
531+
defer cancel()
532+
533+
store, cleanup := setupTestStore(t)
534+
defer cleanup()
535+
536+
remoteName := "test-root-remote"
537+
remoteURL := "file:///tmp/test-root-remote"
538+
539+
// Set up CLIDir with dolt init (database directory)
540+
cliDir := store.CLIDir()
541+
if cliDir == "" {
542+
t.Skip("no CLI dir available")
543+
}
544+
if err := os.MkdirAll(cliDir, 0755); err != nil {
545+
t.Fatalf("failed to create CLI dir: %v", err)
546+
}
547+
initCmd := exec.Command("dolt", "init", "--name", "test", "--email", "test@test.com")
548+
initCmd.Dir = cliDir
549+
if out, err := initCmd.CombinedOutput(); err != nil {
550+
// Might already be initialized
551+
if !strings.Contains(string(out), "already") {
552+
t.Fatalf("dolt init in CLIDir failed: %s: %v", out, err)
553+
}
554+
}
555+
556+
// Set up server root (dbPath) with dolt init — separate from CLIDir
557+
rootDir := store.Path()
558+
if rootDir == "" || rootDir == cliDir {
559+
t.Skip("dbPath same as CLIDir — migration not applicable")
560+
}
561+
if _, err := os.Stat(filepath.Join(rootDir, ".dolt")); err != nil {
562+
// Initialize root dir if needed
563+
if err := os.MkdirAll(rootDir, 0755); err != nil {
564+
t.Fatalf("failed to create root dir: %v", err)
565+
}
566+
initRootCmd := exec.Command("dolt", "init", "--name", "test", "--email", "test@test.com")
567+
initRootCmd.Dir = rootDir
568+
if out, err := initRootCmd.CombinedOutput(); err != nil {
569+
if !strings.Contains(string(out), "already") {
570+
t.Fatalf("dolt init in root failed: %s: %v", out, err)
571+
}
572+
}
573+
}
574+
575+
// Add remote to server root (the wrong place — simulates the user's mistake)
576+
if err := doltutil.AddCLIRemote(rootDir, remoteName, remoteURL); err != nil {
577+
t.Fatalf("failed to add remote to server root: %v", err)
578+
}
579+
defer func() { _ = doltutil.RemoveCLIRemote(rootDir, remoteName) }()
580+
581+
// Verify remote is NOT in CLIDir before migration
582+
if url := doltutil.FindCLIRemote(cliDir, remoteName); url != "" {
583+
t.Fatalf("remote should not be in CLIDir before migration, found: %s", url)
584+
}
585+
586+
// Remove from SQL if present (simulate clean state)
587+
_ = store.RemoveRemote(ctx, remoteName)
588+
589+
// Run sync — should discover remote in server root and migrate to CLIDir + SQL
590+
store.syncCLIRemotesToSQL(ctx)
591+
592+
// Verify remote was migrated to CLIDir
593+
if url := doltutil.FindCLIRemote(cliDir, remoteName); url == "" {
594+
t.Error("expected remote to be migrated to CLIDir")
595+
} else if url != remoteURL {
596+
t.Errorf("CLIDir remote URL = %q, want %q", url, remoteURL)
597+
}
598+
599+
// Verify remote was registered in SQL
600+
has, err := store.HasRemote(ctx, remoteName)
601+
if err != nil {
602+
t.Fatalf("HasRemote failed: %v", err)
603+
}
604+
if !has {
605+
t.Error("expected remote to be registered in SQL after migration")
606+
}
607+
608+
// Clean up
609+
_ = doltutil.RemoveCLIRemote(cliDir, remoteName)
610+
_ = store.RemoveRemote(ctx, remoteName)
611+
}
612+
521613
// setupFederationStore creates a Dolt store for federation testing
522614
func setupFederationStore(t *testing.T, ctx context.Context, path, prefix string) (*DoltStore, func()) {
523615
t.Helper()

0 commit comments

Comments
 (0)