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
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 |