package migrator import ( "fmt" "strings" "sync" "vix.ro/sonix/azure_migrate/internal/azure" "vix.ro/sonix/azure_migrate/internal/config" "vix.ro/sonix/azure_migrate/internal/forgejo" ) type AzureClient interface { GetRepositories() ([]azure.Repository, error) GetAuthenticatedURL(remoteURL, pat string) string } type ForgejoClient interface { GetUser(username string) (*forgejo.User, error) RepositoryExists(owner, name string) (bool, error) CreateMirror(req forgejo.MigrateRequest) (*forgejo.Repository, error) } type Migrator struct { config *config.Config azureClient AzureClient forgejoClient ForgejoClient errors []error mu sync.Mutex } type MigrationResult struct { RepoName string Success bool Error error ForgejoURL string } func New(cfg *config.Config) (*Migrator, error) { azClient := azure.NewClient(cfg.AzureOrg, cfg.AzurePAT) fjClient := forgejo.NewClient(cfg.ForgejoURL, cfg.ForgejoToken) return &Migrator{ config: cfg, azureClient: azClient, forgejoClient: fjClient, errors: make([]error, 0), }, nil } func (m *Migrator) ListRepositories() error { fmt.Println("Fetching repositories from Azure DevOps...") repos, err := m.azureClient.GetRepositories() if err != nil { return fmt.Errorf("failed to list repositories: %w", err) } fmt.Printf("\nFound %d repositories:\n", len(repos)) fmt.Println(strings.Repeat("-", 80)) fmt.Printf("%-40s %-20s %s\n", "NAME", "PROJECT", "URL") fmt.Println(strings.Repeat("-", 80)) for _, repo := range repos { fmt.Printf("%-40s %-20s %s\n", truncate(repo.Name, 40), truncate(repo.Project.Name, 20), repo.WebURL) } return nil } func (m *Migrator) Run() error { fmt.Println("Starting migration process...") if m.config.DryRun { fmt.Println("*** DRY RUN MODE - No changes will be made ***") } // Get Azure repos repos, err := m.azureClient.GetRepositories() if err != nil { return fmt.Errorf("failed to fetch Azure repositories: %w", err) } fmt.Printf("Found %d repositories to process\n\n", len(repos)) // Get Forgejo user/org ID user, err := m.forgejoClient.GetUser(m.config.ForgejoOwner) if err != nil { return fmt.Errorf("failed to get Forgejo user/org '%s': %w", m.config.ForgejoOwner, err) } // Process with concurrency semaphore := make(chan struct{}, m.config.Concurrent) var wg sync.WaitGroup results := make(chan MigrationResult, len(repos)) for _, repo := range repos { wg.Add(1) semaphore <- struct{}{} // Acquire go func(r azure.Repository) { defer wg.Done() defer func() { <-semaphore }() // Release result := m.migrateRepository(r, user.ID) results <- result if result.Error != nil { m.addError(result.Error) } }(repo) } // Close results channel when done go func() { wg.Wait() close(results) }() // Print results var successCount, failCount int for result := range results { if result.Success { successCount++ fmt.Printf("✓ Migrated: %s -> %s\n", result.RepoName, result.ForgejoURL) } else { failCount++ fmt.Printf("✗ Failed: %s - Error: %v\n", result.RepoName, result.Error) } } // Summary fmt.Println(strings.Repeat("=", 80)) fmt.Printf("Migration Complete: %d succeeded, %d failed\n", successCount, failCount) if len(m.errors) > 0 { fmt.Println("\nDetailed errors:") for i, err := range m.errors { fmt.Printf("%d. %v\n", i+1, err) } return fmt.Errorf("migration completed with %d errors", len(m.errors)) } return nil } func (m *Migrator) migrateRepository(repo azure.Repository, uid int64) MigrationResult { result := MigrationResult{ RepoName: repo.Name, } // Check if already exists exists, err := m.forgejoClient.RepositoryExists(m.config.ForgejoOwner, repo.Name) if err != nil { result.Error = fmt.Errorf("failed to check if repository exists: %w", err) return result } if exists { result.Error = fmt.Errorf("repository already exists in Forgejo") return result } // Prepare authenticated URL for Azure cloneURL := m.azureClient.GetAuthenticatedURL(repo.RemoteURL, m.config.AzurePAT) // Prepare migration request req := forgejo.MigrateRequest{ CloneAddr: cloneURL, AuthUsername: "", // Azure uses PAT as password with empty username AuthPassword: m.config.AzurePAT, UID: int(uid), RepoName: repo.Name, RepoOwner: m.config.ForgejoOwner, Mirror: true, LFS: true, // Enable LFS support Interval: m.config.MirrorInterval, Description: fmt.Sprintf("Mirrored from Azure DevOps: %s", repo.WebURL), Private: true, // Default to private, adjust as needed } if m.config.DryRun { result.Success = true result.ForgejoURL = fmt.Sprintf("%s/%s/%s", m.config.ForgejoURL, m.config.ForgejoOwner, repo.Name) return result } // Create mirror createdRepo, err := m.forgejoClient.CreateMirror(req) if err != nil { // Provide detailed error context result.Error = fmt.Errorf("failed to create mirror for '%s' (project: %s): %w", repo.Name, repo.Project.Name, err) return result } result.Success = true result.ForgejoURL = createdRepo.HTMLURL return result } func (m *Migrator) addError(err error) { m.mu.Lock() defer m.mu.Unlock() m.errors = append(m.errors, err) } func truncate(s string, length int) string { if len(s) <= length { return s } return s[:length-3] + "..." }