Files
gitlab-to-gitea/migration/manager.go
2025-04-20 22:16:11 -04:00

273 lines
7.9 KiB
Go

// manager.go
// Package migration handles the migration of data from GitLab to Gitea
package migration
import (
"fmt"
"os"
"github.com/go-i2p/gitlab-to-gitea/utils"
"github.com/go-i2p/gitlab-to-gitea/config"
"github.com/go-i2p/gitlab-to-gitea/gitea"
"github.com/go-i2p/gitlab-to-gitea/gitlab"
gogitlab "github.com/xanzy/go-gitlab"
)
// Manager handles the migration process
type Manager struct {
gitlabClient *gitlab.Client
giteaClient *gitea.Client
config *config.Config
state *State
}
func FileExists(filename string) bool {
_, err := os.Stat(filename)
return !os.IsNotExist(err)
}
// NewManager creates a new migration manager
func NewManager(gitlabClient *gitlab.Client, giteaClient *gitea.Client, cfg *config.Config) *Manager {
// Initialize state
state := NewState(cfg.MigrationStateFile)
if FileExists(cfg.MigrationStateFile) && cfg.ResumeMigration {
utils.PrintInfo("Resuming previous migration...")
if err := state.Load(); err != nil {
utils.PrintWarning(fmt.Sprintf("Could not load migration state: %v. Starting new migration.", err))
}
} else {
utils.PrintInfo("Starting new migration...")
if err := state.Reset(); err != nil {
utils.PrintWarning(fmt.Sprintf("Could not reset migration state: %v", err))
}
utils.PrintInfo("Migration state reset.")
}
utils.PrintInfo("Migration state initialized.")
return &Manager{
gitlabClient: gitlabClient,
giteaClient: giteaClient,
config: cfg,
state: state,
}
}
// ImportUsersGroups imports users and groups from GitLab to Gitea
func (m *Manager) ImportUsersGroups() error {
utils.PrintInfo("Fetching users from GitLab...")
// Get GitLab users
users, err := m.gitlabClient.ListUsers()
if err != nil {
return fmt.Errorf("failed to list GitLab users: %w", err)
}
utils.PrintInfo(fmt.Sprintf("Found %d GitLab users", len(users)))
utils.PrintInfo("Fetching groups from GitLab...")
// Get GitLab groups
groups, err := m.gitlabClient.ListGroups()
if err != nil {
return fmt.Errorf("failed to list GitLab groups: %w", err)
}
utils.PrintInfo(fmt.Sprintf("Found %d GitLab groups", len(groups)))
utils.PrintHeader("Importing users")
// Import users
for _, user := range users {
utils.PrintInfo(fmt.Sprintf("Importing user %s...", user.Username))
if m.config.ResumeMigration && m.state.HasImportedUser(user.Username) {
utils.PrintWarning(fmt.Sprintf("User %s already imported, skipping!", user.Username))
continue
}
if err := m.ImportUser(user, false); err != nil {
utils.PrintError(fmt.Sprintf("Failed to import user %s: %v", user.Username, err))
continue
}
m.state.MarkUserImported(user.Username)
if err := m.state.Save(); err != nil {
utils.PrintWarning(fmt.Sprintf("Failed to save migration state: %v", err))
}
utils.PrintSuccess(fmt.Sprintf("Imported user %s.", user.Username))
}
utils.PrintHeader("Importing groups")
// Import groups
for _, group := range groups {
cleanName := utils.CleanName(group.Name)
utils.PrintInfo(fmt.Sprintf("Importing group: %s...", cleanName))
if m.config.ResumeMigration && m.state.HasImportedGroup(cleanName) {
utils.PrintWarning(fmt.Sprintf("Group %s already imported, skipping!", cleanName))
continue
}
if err := m.ImportGroup(group); err != nil {
utils.PrintError(fmt.Sprintf("Failed to import group %s: %v", group.Name, err))
continue
}
m.state.MarkGroupImported(cleanName)
if err := m.state.Save(); err != nil {
utils.PrintWarning(fmt.Sprintf("Failed to save migration state: %v", err))
}
utils.PrintSuccess(fmt.Sprintf("Imported group: %s.", cleanName))
}
return nil
}
// ImportProjects imports projects from GitLab to Gitea
func (m *Manager) ImportProjects() error {
// Get GitLab projects
projects, err := m.gitlabClient.ListProjects()
if err != nil {
return fmt.Errorf("failed to list GitLab projects: %w", err)
}
utils.PrintInfo(fmt.Sprintf("Found %d GitLab projects", len(projects)))
// Import projects
utils.PrintInfo("Pre-creating all necessary users for project migration...")
// Create a set of all usernames and namespaces that need to exist
requiredUsers := m.collectRequiredUsers(projects)
// Create any missing users
utils.PrintInfo(fmt.Sprintf("Found %d users that need to exist in Gitea", len(requiredUsers)))
for username := range requiredUsers {
exists, err := m.userExists(utils.NormalizeUsername(username))
if err != nil {
utils.PrintWarning(fmt.Sprintf("Error checking if user exists: %v", err))
continue
}
if !exists {
if err := m.ImportPlaceholderUser(username); err != nil {
utils.PrintWarning(fmt.Sprintf("Failed to create placeholder user: %v", err))
}
}
}
utils.PrintInfo("Starting project migration...")
// Import projects
for _, project := range projects {
projectKey := fmt.Sprintf("%s/%s", project.Namespace.Name, utils.CleanName(project.Name))
// Skip if project was already fully imported
if m.config.ResumeMigration && m.state.HasImportedProject(projectKey) {
utils.PrintWarning(fmt.Sprintf("Project %s already imported, skipping!", projectKey))
continue
}
// Import project
if err := m.ImportProject(project); err != nil {
utils.PrintError(fmt.Sprintf("Failed to import project %s: %v", project.Name, err))
continue
}
m.state.MarkProjectImported(projectKey)
if err := m.state.Save(); err != nil {
utils.PrintWarning(fmt.Sprintf("Failed to save migration state: %v", err))
}
}
return nil
}
// collectRequiredUsers builds a set of usernames that need to exist before project migration
func (m *Manager) collectRequiredUsers(projects []*gogitlab.Project) map[string]struct{} {
required := make(map[string]struct{})
utils.PrintHeader("Collecting required users for project migration")
// Helper function to add a user to the required map if not already present
addUser := func(username string) {
if username == "" {
return
}
if _, exists := required[username]; !exists {
required[username] = struct{}{}
utils.PrintInfo(fmt.Sprintf("Adding required user: %s", username))
}
}
// Collect users from projects
for _, project := range projects {
utils.PrintInfo(fmt.Sprintf("Collecting users for project %s...", project.Name))
// Add project namespace/owner if it's a user
if project.Namespace.Kind == "user" {
addUser(project.Namespace.Path)
}
// Collect project members
members, err := m.gitlabClient.GetProjectMembers(project.ID)
if err != nil {
utils.PrintWarning(fmt.Sprintf("Error collecting members for %s: %v", project.Name, err))
continue
}
for _, member := range members {
addUser(member.Username)
}
// Collect issues and related users
issues, err := m.gitlabClient.GetProjectIssues(project.ID)
if err != nil {
utils.PrintWarning(fmt.Sprintf("Error collecting issues for %s: %v", project.Name, err))
continue
}
for _, issue := range issues {
// Add issue author
if issue.Author != nil {
addUser(issue.Author.Username)
}
// Add issue assignees
if issue.Assignee != nil {
addUser(issue.Assignee.Username)
}
for _, assignee := range issue.Assignees {
addUser(assignee.Username)
}
// Process issue notes/comments for authors
notes, err := m.gitlabClient.GetIssueNotes(project.ID, issue.IID)
if err != nil {
utils.PrintWarning(fmt.Sprintf("Error collecting notes for issue #%d: %v", issue.IID, err))
continue
}
for _, note := range notes {
if !note.System && note.Author.ID != 0 {
addUser(note.Author.Username)
}
}
// Extract mentioned users from issue description
/*for _, mention := range utils.ExtractUserMentions(issue.Description) {
addUser(mention)
}
// Extract mentioned users from notes
for _, note := range notes {
if !note.System {
for _, mention := range utils.ExtractUserMentions(note.Body) {
addUser(mention)
}
}
}*/
}
// Milestones don't have authors
}
utils.PrintInfo(fmt.Sprintf("Collected a total of %d unique required users", len(required)))
return required
}