You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 lines
7.1 KiB

"""
Main migration engine
"""
import os
import subprocess
import tempfile
import shutil
import logging
from typing import List, Dict
from pathlib import Path
from providers.base import SourceProvider, DestinationProvider, Repository, MigrationError
from .config import MigrationConfig
logger = logging.getLogger(__name__)
class MigrationEngine:
"""Main migration engine that orchestrates repository migrations"""
def __init__(self, source_provider: SourceProvider, destination_provider: DestinationProvider):
self.source_provider = source_provider
self.destination_provider = destination_provider
def migrate_repositories(self, repositories: List[Repository]) -> Dict[str, bool]:
"""Migrate a list of repositories"""
if not repositories:
logger.info("No repositories selected for migration")
return {}
results = {}
for repository in repositories:
target_name = repository.github_name or repository.name
if repository.github_name and repository.github_name != repository.name:
logger.info(f"Migrating repository: {repository.owner}/{repository.name}{target_name}")
else:
logger.info(f"Migrating repository: {repository.owner}/{repository.name}")
try:
success = self._migrate_single_repository(repository, target_name)
display_name = f"{repository.owner}/{repository.name}"
if target_name != repository.name:
display_name += f"{target_name}"
results[display_name] = success
except Exception as e:
logger.error(f"Unexpected error migrating {repository.name}: {e}")
display_name = f"{repository.owner}/{repository.name}"
if target_name != repository.name:
display_name += f"{target_name}"
results[display_name] = False
return results
def _migrate_single_repository(self, repository: Repository, target_name: str) -> bool:
"""Migrate a single repository"""
try:
# Create destination repository
logger.info(f"Creating destination repository: {target_name}")
success = self.destination_provider.create_repository(repository, target_name)
if not success:
logger.error(f"Failed to create destination repository: {target_name}")
return False
# Clone and push repository
logger.info(f"Starting clone and push for repository: {repository.name}")
return self._clone_and_push_repository(repository, target_name)
except MigrationError as e:
logger.error(f"Migration error for repository {repository.name}: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error migrating repository {repository.name}: {e}")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Exception args: {e.args}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return False
def _clone_and_push_repository(self, repository: Repository, target_name: str) -> bool:
"""Clone repository from source and push to destination"""
temp_dir = None
original_cwd = os.getcwd()
try:
# Create temporary directory
temp_dir = tempfile.mkdtemp(prefix=f"migration_{repository.name}_")
repo_path = Path(temp_dir) / repository.name
# Clone from source
source_url = self.source_provider.get_authenticated_clone_url(repository)
clone_cmd = ['git', 'clone', '--mirror', source_url, str(repo_path)]
logger.info(f"Cloning repository from {self.source_provider.__class__.__name__}: {repository.owner}/{repository.name}")
result = subprocess.run(clone_cmd, capture_output=True, text=True, cwd=temp_dir)
if result.returncode != 0:
logger.error(f"Failed to clone repository: {result.stderr}")
return False
# Verify repository was cloned successfully
if not repo_path.exists():
logger.error(f"Repository directory not found after cloning: {repo_path}")
return False
# Add destination remote
dest_url = self.destination_provider.get_authenticated_push_url(target_name)
add_remote_cmd = ['git', 'remote', 'add', 'destination', dest_url]
result = subprocess.run(add_remote_cmd, capture_output=True, text=True, cwd=str(repo_path))
if result.returncode != 0:
logger.error(f"Failed to add destination remote: {result.stderr}")
return False
# Push to destination
return self._push_to_destination(repository, target_name, repo_path)
except Exception as e:
logger.error(f"Error during repository migration: {e}")
return False
finally:
# Restore original working directory
try:
os.chdir(original_cwd)
except Exception as e:
logger.warning(f"Failed to restore original working directory: {e}")
# Clean up temporary directory
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
logger.warning(f"Failed to clean up temporary directory {temp_dir}: {e}")
def _push_to_destination(self, repository: Repository, target_name: str, repo_path: Path) -> bool:
"""Push repository to destination provider"""
if target_name != repository.name:
logger.info(f"Pushing repository to destination: {repository.name}{target_name}")
else:
logger.info(f"Pushing repository to destination: {repository.name}")
# Push branches
push_branches_cmd = ['git', 'push', '--all', 'destination']
result = subprocess.run(push_branches_cmd, capture_output=True, text=True, cwd=str(repo_path))
if result.returncode != 0:
logger.error(f"Failed to push branches to destination: {result.stderr}")
return False
# Push tags (non-blocking)
push_tags_cmd = ['git', 'push', '--tags', 'destination']
result = subprocess.run(push_tags_cmd, capture_output=True, text=True, cwd=str(repo_path))
if result.returncode != 0:
logger.warning(f"Failed to push tags to destination (this is often normal): {result.stderr}")
if target_name != repository.name:
logger.info(f"Successfully migrated repository: {repository.name}{target_name}")
else:
logger.info(f"Successfully migrated repository: {repository.name}")
return True