azure_migrate/internal/migrator/migrator.go
2026-03-22 12:15:18 +02:00

204 lines
4.9 KiB
Go

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 Migrator struct {
config *config.Config
azureClient *azure.Client
forgejoClient *forgejo.Client
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] + "..."
}