feat: add bd migrate-sync command for sync.branch workflow setup (bd-epn)
Add a new command that encapsulates all the work needed to migrate a clone to use the sync.branch workflow for multi-clone setups like Gas Town: - Validates current state (not on sync branch, not already configured) - Creates sync branch if it doesn't exist (from remote or locally) - Sets up git worktree for the sync branch - Syncs current beads data to worktree - Commits initial state to sync branch - Sets sync.branch configuration - Pushes sync branch to remote Usage: bd migrate-sync beads-sync # Basic migration bd migrate-sync beads-sync --dry-run # Preview changes bd migrate-sync beads-sync --force # Reconfigure even if set
This commit is contained in:
180
cmd/bd/migrate_sync_test.go
Normal file
180
cmd/bd/migrate_sync_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrateSyncValidation(t *testing.T) {
|
||||
// Test invalid branch names
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid simple", "beads-sync", false},
|
||||
{"valid with slash", "beads/sync", false},
|
||||
{"valid with dots", "beads.sync", false},
|
||||
{"invalid empty", "", true},
|
||||
{"invalid HEAD", "HEAD", true},
|
||||
{"invalid dots", "..", true},
|
||||
{"invalid leading slash", "/beads", true},
|
||||
{"invalid trailing slash", "beads/", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We can't easily test the full command without a git repo,
|
||||
// but we can test branch validation indirectly
|
||||
if tt.branch == "" {
|
||||
// Empty branch should fail at args validation level
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateSyncDryRun(t *testing.T) {
|
||||
// Create a temp directory with a git repo
|
||||
tmpDir, err := os.MkdirTemp("", "bd-migrate-sync-test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to init git: %v", err)
|
||||
}
|
||||
|
||||
// Configure git user for commits
|
||||
cmd = exec.Command("git", "config", "user.email", "test@test.com")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to config git email: %v", err)
|
||||
}
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to config git name: %v", err)
|
||||
}
|
||||
|
||||
// Create initial commit
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
cmd = exec.Command("git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to git add: %v", err)
|
||||
}
|
||||
cmd = exec.Command("git", "commit", "-m", "initial")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to git commit: %v", err)
|
||||
}
|
||||
|
||||
// Create .beads directory and initialize
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
||||
t.Fatalf("failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Create minimal issues.jsonl
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if err := os.WriteFile(jsonlPath, []byte(""), 0644); err != nil {
|
||||
t.Fatalf("failed to create issues.jsonl: %v", err)
|
||||
}
|
||||
|
||||
// Test that branchExistsLocal returns false for non-existent branch
|
||||
// Note: We need to run this from tmpDir context since branchExistsLocal uses git in cwd
|
||||
ctx := context.Background()
|
||||
origDir, _ := os.Getwd()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
defer os.Chdir(origDir)
|
||||
|
||||
if branchExistsLocal(ctx, "beads-sync") {
|
||||
t.Error("branchExistsLocal should return false for non-existent branch")
|
||||
}
|
||||
|
||||
// Create the branch
|
||||
cmd = exec.Command("git", "branch", "beads-sync")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to create branch: %v", err)
|
||||
}
|
||||
|
||||
// Now it should exist
|
||||
if !branchExistsLocal(ctx, "beads-sync") {
|
||||
t.Error("branchExistsLocal should return true for existing branch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasChangesInWorktreeDir(t *testing.T) {
|
||||
// Create a temp directory with a git repo
|
||||
tmpDir, err := os.MkdirTemp("", "bd-worktree-changes-test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to init git: %v", err)
|
||||
}
|
||||
|
||||
// Configure git user
|
||||
cmd = exec.Command("git", "config", "user.email", "test@test.com")
|
||||
cmd.Dir = tmpDir
|
||||
_ = cmd.Run()
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = tmpDir
|
||||
_ = cmd.Run()
|
||||
|
||||
// Create and commit initial file
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
cmd = exec.Command("git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
_ = cmd.Run()
|
||||
cmd = exec.Command("git", "commit", "-m", "initial")
|
||||
cmd.Dir = tmpDir
|
||||
_ = cmd.Run()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// No changes initially
|
||||
hasChanges, err := hasChangesInWorktreeDir(ctx, tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktreeDir failed: %v", err)
|
||||
}
|
||||
if hasChanges {
|
||||
t.Error("should have no changes initially")
|
||||
}
|
||||
|
||||
// Add uncommitted file
|
||||
newFile := filepath.Join(tmpDir, "new.txt")
|
||||
if err := os.WriteFile(newFile, []byte("new"), 0644); err != nil {
|
||||
t.Fatalf("failed to create new file: %v", err)
|
||||
}
|
||||
|
||||
hasChanges, err = hasChangesInWorktreeDir(ctx, tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktreeDir failed: %v", err)
|
||||
}
|
||||
if !hasChanges {
|
||||
t.Error("should have changes after adding file")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user