Files
beads/cmd/bd/routed.go
beads/crew/emma e82f5136c1 fix(dolt): proper server mode support for routing and storage
- FindDatabasePath now handles Dolt server mode (no local dir required)
- main.go uses NewFromConfigWithOptions for Dolt to read server settings
- Routing uses factory via callback to respect backend configuration
- Handle Dolt "database exists" error (error 1007) gracefully

Previously, Dolt server mode failed because:
1. FindDatabasePath required a local directory to exist
2. main.go bypassed server mode config when creating Dolt storage
3. Routing always opened SQLite regardless of backend config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 00:26:58 -08:00

186 lines
5.6 KiB
Go

package main
import (
"context"
"path/filepath"
"github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/factory"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/utils"
)
// RoutedResult contains the result of a routed issue lookup
type RoutedResult struct {
Issue *types.Issue
Store storage.Storage // The store that contains this issue (may be routed)
Routed bool // true if the issue was found via routing
ResolvedID string // The resolved (full) issue ID
closeFn func() // Function to close routed storage (if any)
}
// Close closes any routed storage. Safe to call if Routed is false.
func (r *RoutedResult) Close() {
if r.closeFn != nil {
r.closeFn()
}
}
// resolveAndGetIssueWithRouting resolves a partial ID and gets the issue,
// using routes.jsonl for prefix-based routing if needed.
// This enables cross-repo issue lookups (e.g., `bd show gt-xyz` from ~/gt).
//
// The resolution happens in the correct store based on the ID prefix.
// Returns a RoutedResult containing the issue, resolved ID, and the store to use.
// The caller MUST call result.Close() when done to release any routed storage.
func resolveAndGetIssueWithRouting(ctx context.Context, localStore storage.Storage, id string) (*RoutedResult, error) {
// Step 1: Check if routing is needed based on ID prefix
if dbPath == "" {
// No routing without a database path - use local store
return resolveAndGetFromStore(ctx, localStore, id, false)
}
beadsDir := filepath.Dir(dbPath)
// Use factory.NewFromConfig as the storage opener to respect backend configuration
routedStorage, err := routing.GetRoutedStorageWithOpener(ctx, id, beadsDir, factory.NewFromConfig)
if err != nil {
return nil, err
}
if routedStorage != nil {
// Step 2: Resolve and get from routed store
result, err := resolveAndGetFromStore(ctx, routedStorage.Storage, id, true)
if err != nil {
_ = routedStorage.Close()
return nil, err
}
if result != nil {
result.closeFn = func() { _ = routedStorage.Close() }
return result, nil
}
_ = routedStorage.Close()
}
// Step 3: Fall back to local store
return resolveAndGetFromStore(ctx, localStore, id, false)
}
// resolveAndGetFromStore resolves a partial ID and gets the issue from a specific store.
func resolveAndGetFromStore(ctx context.Context, s storage.Storage, id string, routed bool) (*RoutedResult, error) {
// First, resolve the partial ID
resolvedID, err := utils.ResolvePartialID(ctx, s, id)
if err != nil {
return nil, err
}
// Then get the issue
issue, err := s.GetIssue(ctx, resolvedID)
if err != nil {
return nil, err
}
if issue == nil {
return nil, nil
}
return &RoutedResult{
Issue: issue,
Store: s,
Routed: routed,
ResolvedID: resolvedID,
}, nil
}
// getIssueWithRouting tries to get an issue from the local store first,
// then falls back to checking routes.jsonl for prefix-based routing.
// This enables cross-repo issue lookups (e.g., `bd show gt-xyz` from ~/gt).
//
// Returns a RoutedResult containing the issue and the store to use for related queries.
// The caller MUST call result.Close() when done to release any routed storage.
func getIssueWithRouting(ctx context.Context, localStore storage.Storage, id string) (*RoutedResult, error) {
// Step 1: Try local store first (current behavior)
issue, err := localStore.GetIssue(ctx, id)
if err == nil && issue != nil {
return &RoutedResult{
Issue: issue,
Store: localStore,
Routed: false,
ResolvedID: id,
}, nil
}
// Step 2: Check routes.jsonl for prefix-based routing
if dbPath == "" {
// No routing without a database path - return original result
return &RoutedResult{
Issue: issue,
Store: localStore,
Routed: false,
ResolvedID: id,
}, err
}
beadsDir := filepath.Dir(dbPath)
routedStorage, routeErr := routing.GetRoutedStorageForID(ctx, id, beadsDir)
if routeErr != nil || routedStorage == nil {
// No routing found or error - return original result
return &RoutedResult{
Issue: issue,
Store: localStore,
Routed: false,
ResolvedID: id,
}, err
}
// Step 3: Try the routed storage
routedIssue, routedErr := routedStorage.Storage.GetIssue(ctx, id)
if routedErr != nil || routedIssue == nil {
_ = routedStorage.Close()
// Return the original error if routing also failed
if err != nil {
return nil, err
}
return nil, routedErr
}
// Return the issue with the routed store
return &RoutedResult{
Issue: routedIssue,
Store: routedStorage.Storage,
Routed: true,
ResolvedID: id,
closeFn: func() {
_ = routedStorage.Close()
},
}, nil
}
// getRoutedStoreForID returns a storage connection for an issue ID if routing is needed.
// Returns nil if no routing is needed (issue should be in local store).
// The caller is responsible for closing the returned storage.
func getRoutedStoreForID(ctx context.Context, id string) (*routing.RoutedStorage, error) {
if dbPath == "" {
return nil, nil
}
beadsDir := filepath.Dir(dbPath)
return routing.GetRoutedStorageForID(ctx, id, beadsDir)
}
// needsRouting checks if an ID would be routed to a different beads directory.
// This is used to decide whether to bypass the daemon for cross-repo lookups.
func needsRouting(id string) bool {
if dbPath == "" {
return false
}
beadsDir := filepath.Dir(dbPath)
targetDir, routed, err := routing.ResolveBeadsDirForID(context.Background(), id, beadsDir)
if err != nil || !routed {
return false
}
// Check if the routed directory is different from the current one
return targetDir != beadsDir
}