feat: integrate migration detection into bd doctor (bd-7l27)
Add a consolidated "Pending Migrations" check to bd doctor that: - Detects sequential ID usage (suggests bd migrate hash-ids) - Detects legacy deletions.jsonl (suggests bd migrate tombstones) - Detects missing sync-branch config (suggests bd migrate sync) - Detects database version mismatches (suggests bd migrate) Also updates existing checks to use correct modern commands: - CheckIDFormat: bd migrate hash-ids (was bd migrate --to-hash-ids) - CheckDeletionsManifest: bd migrate tombstones (was bd migrate-tombstones) - CheckSyncBranchConfig: bd migrate sync beads-sync (was config.yaml edit) Removes TODO(bd-7l27) comments from migrate_*.go files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,11 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, sizeCheck)
|
result.Checks = append(result.Checks, sizeCheck)
|
||||||
// Don't fail overall check for size warning, just inform
|
// Don't fail overall check for size warning, just inform
|
||||||
|
|
||||||
|
// Check 30: Pending migrations (summarizes all available migrations)
|
||||||
|
migrationsCheck := convertDoctorCheck(doctor.CheckPendingMigrations(path))
|
||||||
|
result.Checks = append(result.Checks, migrationsCheck)
|
||||||
|
// Status is determined by the check itself based on migration priorities
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -476,8 +476,8 @@ func CheckSyncBranchConfig(path string) DoctorCheck {
|
|||||||
Name: "Sync Branch Config",
|
Name: "Sync Branch Config",
|
||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: "sync-branch not configured",
|
Message: "sync-branch not configured",
|
||||||
Detail: "Multi-clone setups should configure sync-branch in config.yaml",
|
Detail: "Multi-clone setups should configure sync-branch for safe data synchronization",
|
||||||
Fix: "Add 'sync-branch: beads-sync' to .beads/config.yaml",
|
Fix: "Run 'bd migrate sync beads-sync' to set up sync branch workflow",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func CheckIDFormat(path string) DoctorCheck {
|
|||||||
Name: "Issue IDs",
|
Name: "Issue IDs",
|
||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: "sequential (e.g., bd-1, bd-2, ...)",
|
Message: "sequential (e.g., bd-1, bd-2, ...)",
|
||||||
Fix: "Run 'bd migrate --to-hash-ids' to upgrade (prevents ID collisions in multi-worker scenarios)",
|
Fix: "Run 'bd migrate hash-ids' to upgrade (prevents ID collisions in multi-worker scenarios)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ func CheckDeletionsManifest(path string) DoctorCheck {
|
|||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: fmt.Sprintf("Legacy format (%d entries)", count),
|
Message: fmt.Sprintf("Legacy format (%d entries)", count),
|
||||||
Detail: "deletions.jsonl is deprecated in favor of inline tombstones",
|
Detail: "deletions.jsonl is deprecated in favor of inline tombstones",
|
||||||
Fix: "Run 'bd migrate-tombstones' to convert to inline tombstones",
|
Fix: "Run 'bd migrate tombstones' to convert to inline tombstones",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DoctorCheck{
|
return DoctorCheck{
|
||||||
|
|||||||
275
cmd/bd/doctor/migration.go
Normal file
275
cmd/bd/doctor/migration.go
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
|
"github.com/steveyegge/beads/internal/configfile"
|
||||||
|
"github.com/steveyegge/beads/internal/git"
|
||||||
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PendingMigration represents a single pending migration
|
||||||
|
type PendingMigration struct {
|
||||||
|
Name string // e.g., "hash-ids", "tombstones", "sync"
|
||||||
|
Description string // e.g., "Convert sequential IDs to hash-based IDs"
|
||||||
|
Command string // e.g., "bd migrate hash-ids"
|
||||||
|
Priority int // 1 = critical, 2 = recommended, 3 = optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectPendingMigrations detects all pending migrations for a beads directory
|
||||||
|
func DetectPendingMigrations(path string) []PendingMigration {
|
||||||
|
var pending []PendingMigration
|
||||||
|
|
||||||
|
// Follow redirect to resolve actual beads directory
|
||||||
|
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||||
|
|
||||||
|
// Skip if .beads doesn't exist
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for sequential IDs (hash-ids migration)
|
||||||
|
if needsHashIDsMigration(beadsDir) {
|
||||||
|
pending = append(pending, PendingMigration{
|
||||||
|
Name: "hash-ids",
|
||||||
|
Description: "Convert sequential IDs to hash-based IDs",
|
||||||
|
Command: "bd migrate hash-ids",
|
||||||
|
Priority: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for legacy deletions.jsonl (tombstones migration)
|
||||||
|
if needsTombstonesMigration(beadsDir) {
|
||||||
|
pending = append(pending, PendingMigration{
|
||||||
|
Name: "tombstones",
|
||||||
|
Description: "Convert deletions.jsonl to inline tombstones",
|
||||||
|
Command: "bd migrate tombstones",
|
||||||
|
Priority: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing sync-branch config (sync migration)
|
||||||
|
if needsSyncMigration(beadsDir, path) {
|
||||||
|
pending = append(pending, PendingMigration{
|
||||||
|
Name: "sync",
|
||||||
|
Description: "Configure sync branch for multi-clone setup",
|
||||||
|
Command: "bd migrate sync beads-sync",
|
||||||
|
Priority: 3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for database version mismatch (main migrate command)
|
||||||
|
if versionMismatch := checkDatabaseVersionMismatch(beadsDir); versionMismatch != "" {
|
||||||
|
pending = append(pending, PendingMigration{
|
||||||
|
Name: "database",
|
||||||
|
Description: versionMismatch,
|
||||||
|
Command: "bd migrate",
|
||||||
|
Priority: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPendingMigrations returns a doctor check summarizing all pending migrations
|
||||||
|
func CheckPendingMigrations(path string) DoctorCheck {
|
||||||
|
pending := DetectPendingMigrations(path)
|
||||||
|
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Pending Migrations",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "None required",
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build message with count
|
||||||
|
message := fmt.Sprintf("%d available", len(pending))
|
||||||
|
|
||||||
|
// Build detail with list of migrations
|
||||||
|
var details []string
|
||||||
|
var fixes []string
|
||||||
|
for _, m := range pending {
|
||||||
|
priority := ""
|
||||||
|
switch m.Priority {
|
||||||
|
case 1:
|
||||||
|
priority = " [critical]"
|
||||||
|
case 2:
|
||||||
|
priority = " [recommended]"
|
||||||
|
case 3:
|
||||||
|
priority = " [optional]"
|
||||||
|
}
|
||||||
|
details = append(details, fmt.Sprintf("• %s: %s%s", m.Name, m.Description, priority))
|
||||||
|
fixes = append(fixes, m.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status based on highest priority migration
|
||||||
|
status := StatusOK
|
||||||
|
for _, m := range pending {
|
||||||
|
if m.Priority == 1 {
|
||||||
|
status = StatusError
|
||||||
|
break
|
||||||
|
} else if m.Priority == 2 && status != StatusError {
|
||||||
|
status = StatusWarning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Pending Migrations",
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
Detail: strings.Join(details, "\n"),
|
||||||
|
Fix: strings.Join(fixes, "\n"),
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsHashIDsMigration checks if the database uses sequential IDs
|
||||||
|
func needsHashIDsMigration(beadsDir string) bool {
|
||||||
|
var dbPath string
|
||||||
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
|
dbPath = cfg.DatabasePath(beadsDir)
|
||||||
|
} else {
|
||||||
|
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no database
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Get sample of issues
|
||||||
|
rows, err := db.Query("SELECT id FROM issues ORDER BY created_at LIMIT 10")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var issueIDs []string
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
if err := rows.Scan(&id); err == nil {
|
||||||
|
issueIDs = append(issueIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issueIDs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if NOT using hash-based IDs (i.e., using sequential)
|
||||||
|
return !DetectHashBasedIDs(db, issueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsTombstonesMigration checks if deletions.jsonl exists with entries
|
||||||
|
func needsTombstonesMigration(beadsDir string) bool {
|
||||||
|
deletionsPath := filepath.Join(beadsDir, "deletions.jsonl")
|
||||||
|
|
||||||
|
info, err := os.Stat(deletionsPath)
|
||||||
|
if err != nil {
|
||||||
|
return false // File doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Size() == 0 {
|
||||||
|
return false // Empty file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count non-empty lines
|
||||||
|
file, err := os.Open(deletionsPath) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if len(scanner.Bytes()) > 0 {
|
||||||
|
return true // Has at least one entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsSyncMigration checks if sync-branch should be configured
|
||||||
|
func needsSyncMigration(beadsDir, repoPath string) bool {
|
||||||
|
// Check if already configured
|
||||||
|
if syncbranch.GetFromYAML() != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in a git repository
|
||||||
|
_, err := git.GetGitDir()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if has remote (multi-clone indicator)
|
||||||
|
return hasGitRemote(repoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasGitRemote checks if the repository has a git remote
|
||||||
|
func hasGitRemote(repoPath string) bool {
|
||||||
|
cmd := exec.Command("git", "remote")
|
||||||
|
cmd.Dir = repoPath
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(strings.TrimSpace(string(output))) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDatabaseVersionMismatch returns a description if database version is old
|
||||||
|
func checkDatabaseVersionMismatch(beadsDir string) string {
|
||||||
|
var dbPath string
|
||||||
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
|
dbPath = cfg.DatabasePath(beadsDir)
|
||||||
|
} else {
|
||||||
|
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no database
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Get stored version
|
||||||
|
var storedVersion string
|
||||||
|
err = db.QueryRow("SELECT value FROM metadata WHERE key = 'bd_version'").Scan(&storedVersion)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no such table") {
|
||||||
|
return "Database schema needs update (pre-metadata table)"
|
||||||
|
}
|
||||||
|
// No version stored
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We can't compare to current version here since we don't have access
|
||||||
|
// to the Version variable from main package. The individual check does this.
|
||||||
|
// This function is just for detecting obviously old databases.
|
||||||
|
if storedVersion == "" || storedVersion == "unknown" {
|
||||||
|
return "Database version unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
188
cmd/bd/doctor/migration_test.go
Normal file
188
cmd/bd/doctor/migration_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckPendingMigrations(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func(t *testing.T, dir string)
|
||||||
|
wantStatus string
|
||||||
|
wantMessage string
|
||||||
|
wantMigrations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no beads directory",
|
||||||
|
setup: func(t *testing.T, dir string) {},
|
||||||
|
wantStatus: StatusOK,
|
||||||
|
wantMessage: "None required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty beads directory",
|
||||||
|
setup: func(t *testing.T, dir string) {
|
||||||
|
if err := os.MkdirAll(filepath.Join(dir, ".beads"), 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .beads: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantStatus: StatusOK,
|
||||||
|
wantMessage: "None required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletions.jsonl exists with entries",
|
||||||
|
setup: func(t *testing.T, dir string) {
|
||||||
|
beadsDir := filepath.Join(dir, ".beads")
|
||||||
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .beads: %v", err)
|
||||||
|
}
|
||||||
|
// Create deletions.jsonl with an entry
|
||||||
|
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||||
|
if err := os.WriteFile(filepath.Join(beadsDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantStatus: StatusWarning,
|
||||||
|
wantMigrations: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
tt.setup(t, tmpDir)
|
||||||
|
|
||||||
|
check := CheckPendingMigrations(tmpDir)
|
||||||
|
|
||||||
|
if check.Status != tt.wantStatus {
|
||||||
|
t.Errorf("status = %q, want %q", check.Status, tt.wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantMessage != "" && check.Message != tt.wantMessage {
|
||||||
|
t.Errorf("message = %q, want %q", check.Message, tt.wantMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if check.Category != CategoryMaintenance {
|
||||||
|
t.Errorf("category = %q, want %q", check.Category, CategoryMaintenance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectPendingMigrations(t *testing.T) {
|
||||||
|
t.Run("no beads directory returns empty", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
migrations := DetectPendingMigrations(tmpDir)
|
||||||
|
if len(migrations) != 0 {
|
||||||
|
t.Errorf("expected 0 migrations, got %d", len(migrations))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty beads directory returns empty", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(tmpDir, ".beads"), 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .beads: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations := DetectPendingMigrations(tmpDir)
|
||||||
|
if len(migrations) != 0 {
|
||||||
|
t.Errorf("expected 0 migrations, got %d", len(migrations))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deletions.jsonl triggers tombstones migration", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .beads: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create deletions.jsonl with an entry
|
||||||
|
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||||
|
if err := os.WriteFile(filepath.Join(beadsDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations := DetectPendingMigrations(tmpDir)
|
||||||
|
if len(migrations) != 1 {
|
||||||
|
t.Errorf("expected 1 migration, got %d", len(migrations))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrations[0].Name != "tombstones" {
|
||||||
|
t.Errorf("migration name = %q, want %q", migrations[0].Name, "tombstones")
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrations[0].Command != "bd migrate tombstones" {
|
||||||
|
t.Errorf("migration command = %q, want %q", migrations[0].Command, "bd migrate tombstones")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNeedsTombstonesMigration(t *testing.T) {
|
||||||
|
t.Run("no deletions.jsonl returns false", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if needsTombstonesMigration(tmpDir) {
|
||||||
|
t.Error("expected false for non-existent deletions.jsonl")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty deletions.jsonl returns false", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "deletions.jsonl"), []byte(""), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsTombstonesMigration(tmpDir) {
|
||||||
|
t.Error("expected false for empty deletions.jsonl")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deletions.jsonl with entries returns true", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needsTombstonesMigration(tmpDir) {
|
||||||
|
t.Error("expected true for deletions.jsonl with entries")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
_ "github.com/ncruces/go-sqlite3/embed"
|
_ "github.com/ncruces/go-sqlite3/embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection
|
|
||||||
var migrateCmd = &cobra.Command{
|
var migrateCmd = &cobra.Command{
|
||||||
Use: "migrate",
|
Use: "migrate",
|
||||||
GroupID: "maint",
|
GroupID: "maint",
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/ui"
|
"github.com/steveyegge/beads/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection
|
|
||||||
var migrateHashIDsCmd = &cobra.Command{
|
var migrateHashIDsCmd = &cobra.Command{
|
||||||
Use: "hash-ids",
|
Use: "hash-ids",
|
||||||
Short: "Migrate sequential IDs to hash-based IDs (legacy)",
|
Short: "Migrate sequential IDs to hash-based IDs (legacy)",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection
|
|
||||||
var migrateIssuesCmd = &cobra.Command{
|
var migrateIssuesCmd = &cobra.Command{
|
||||||
Use: "issues",
|
Use: "issues",
|
||||||
Short: "Move issues between repositories",
|
Short: "Move issues between repositories",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/syncbranch"
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection
|
|
||||||
var migrateSyncCmd = &cobra.Command{
|
var migrateSyncCmd = &cobra.Command{
|
||||||
Use: "sync <branch-name>",
|
Use: "sync <branch-name>",
|
||||||
Short: "Migrate to sync.branch workflow for multi-clone setups",
|
Short: "Migrate to sync.branch workflow for multi-clone setups",
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ func loadLegacyDeletionsCmd(path string) (map[string]legacyDeletionRecordCmd, []
|
|||||||
return records, warnings, nil
|
return records, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection
|
|
||||||
var migrateTombstonesCmd = &cobra.Command{
|
var migrateTombstonesCmd = &cobra.Command{
|
||||||
Use: "tombstones",
|
Use: "tombstones",
|
||||||
Short: "Convert deletions.jsonl entries to inline tombstones",
|
Short: "Convert deletions.jsonl entries to inline tombstones",
|
||||||
|
|||||||
Reference in New Issue
Block a user