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.

264 lines
9.7 KiB

#!/usr/bin/env python3
"""
Repository Migration Tool
A flexible tool for migrating repositories between different Git hosting providers.
Currently supports:
- Source providers: Gitea, GitLab
- Destination providers: GitHub, GitLab
Future providers can be easily added through the extensible provider system.
"""
import argparse
import logging
import sys
from pathlib import Path
from colorama import init, Fore, Style
from core.config import MigrationConfig
from core.migration_engine import MigrationEngine
from providers.factory import ProviderFactory
from providers.base import ConfigurationError, ProviderError, MigrationError
from ui.interactive_selector import select_repositories_interactive
from ui.provider_selector import select_providers
# Initialize colorama for cross-platform colored output
init()
def setup_logging(verbose: bool = False):
"""Setup logging configuration"""
level = logging.DEBUG if verbose else logging.INFO
format_str = '%(asctime)s - %(levelname)s - %(message)s'
logging.basicConfig(
level=level,
format=format_str,
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('migration.log')
]
)
def print_banner():
"""Print application banner"""
print(f"{Fore.MAGENTA}{'='*60}")
print(f"{'🚀 GIT MIGRATION TOOL':^60}")
print(f"{'Multi-Provider Repository Migration':^60}")
print(f"{'='*60}{Style.RESET_ALL}")
def print_success_summary(results: dict):
"""Print migration results summary"""
total = len(results)
successful = sum(1 for success in results.values() if success)
print(f"\n{Fore.GREEN}{'='*60}")
print(f" MIGRATION SUMMARY")
print(f"{'='*60}{Style.RESET_ALL}")
print(f"{Fore.GREEN}✅ Successful migrations: {successful}/{total}{Style.RESET_ALL}")
if successful < total:
print(f"{Fore.RED}❌ Failed migrations: {total - successful}/{total}{Style.RESET_ALL}")
print(f"\n{Fore.CYAN}Detailed results:{Style.RESET_ALL}")
for repo, success in results.items():
status = f"{Fore.GREEN}✅ SUCCESS" if success else f"{Fore.RED}❌ FAILED"
print(f" {repo}: {status}{Style.RESET_ALL}")
def create_env_template():
"""Create a .env template file if it doesn't exist"""
env_file = Path('.env')
if not env_file.exists():
template = """# Gitea Source Configuration
GITEA_SOURCE_URL=https://codefirst.iut.uca.fr/git
GITEA_SOURCE_TOKEN=your_gitea_source_personal_access_token
GITEA_SOURCE_USERNAME=your_gitea_source_username
# Gitea Destination Configuration
GITEA_DEST_URL=https://codefirst.iut.uca.fr/git
GITEA_DEST_TOKEN=your_gitea_dest_personal_access_token
GITEA_DEST_USERNAME=your_gitea_dest_username
# GitLab Source Configuration
GITLAB_SOURCE_URL=https://gitlab.com
GITLAB_SOURCE_TOKEN=your_gitlab_source_token
GITLAB_SOURCE_USERNAME=your_gitlab_source_username
# GitLab Destination Configuration
GITLAB_DEST_URL=https://gitlab.com
GITLAB_DEST_TOKEN=your_gitlab_dest_token
GITLAB_DEST_USERNAME=your_gitlab_dest_username
# GitHub Configuration (same for source and destination - only one instance)
GITHUB_TOKEN=your_github_personal_access_token
GITHUB_USERNAME=your_github_username
# Instructions:
# 1. Fill in the credentials for the providers you want to use as source or destination
# 2. You can use the same credentials for source and dest if it's the same instance
# 3. For migrations between different instances of the same provider, use different credentials
# 4. GitHub only has one instance (github.com), so GitHub→GitHub migrations are not supported
# 5. The tool will ask you which provider to use as source and destination
"""
env_file.write_text(template)
print(f"{Fore.YELLOW}📝 Created .env template file. Please fill it with your credentials.{Style.RESET_ALL}")
return False
return True
def main():
"""Main application entry point"""
parser = argparse.ArgumentParser(
description="Migrate repositories between Git hosting providers",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s # Interactive mode (default): select repositories
%(prog)s --no-interactive # Migrate all your repositories automatically
%(prog)s --repos repo1 repo2 # Migrate specific repositories
%(prog)s --repos owner/repo1 # Migrate repositories from other owners
%(prog)s --list # List available repositories
%(prog)s --verbose # Enable verbose logging
Supported providers:
Source: """ + ", ".join(ProviderFactory.get_available_source_providers()) + """
Destination: """ + ", ".join(ProviderFactory.get_available_destination_providers()) + """
"""
)
parser.add_argument(
'--repos',
nargs='+',
help='Specific repositories to migrate (format: repo_name or owner/repo_name)'
)
parser.add_argument(
'--list',
action='store_true',
help='List all available repositories and exit'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose logging'
)
parser.add_argument(
'--setup',
action='store_true',
help='Create .env template file'
)
parser.add_argument(
'--no-interactive',
action='store_true',
help='Skip interactive mode: migrate all your repositories automatically'
)
args = parser.parse_args()
print_banner()
setup_logging(args.verbose)
# Handle setup command
if args.setup:
create_env_template()
return
# Check if .env file exists
if not create_env_template():
return
try:
# Initialize configuration
config = MigrationConfig()
# Select providers interactively
source_provider_type, destination_provider_type = select_providers()
# Create providers
source_provider = ProviderFactory.create_source_provider(
source_provider_type,
config.get_source_provider_config(source_provider_type)
)
destination_provider = ProviderFactory.create_destination_provider(
destination_provider_type,
config.get_destination_provider_config(destination_provider_type)
)
# Initialize migration engine
migration_engine = MigrationEngine(source_provider, destination_provider)
# Handle list command
if args.list:
print(f"{Fore.CYAN}📋 Available repositories from {source_provider_type}:{Style.RESET_ALL}")
repos = source_provider.get_accessible_repositories()
for repo in repos:
private = "🔒 Private" if repo.private else "🌐 Public"
description = repo.description or 'No description'
print(f" {Fore.BLUE}{repo.owner}/{repo.name}{Style.RESET_ALL} - {private}")
if description:
print(f" 📝 {description}")
print(f"\n{Fore.GREEN}Total repositories: {len(repos)}{Style.RESET_ALL}")
return
# Perform migration
if args.repos:
print(f"{Fore.CYAN}🎯 Migrating specific repositories: {', '.join(args.repos)}{Style.RESET_ALL}")
repositories = []
for repo_spec in args.repos:
if '/' in repo_spec:
owner, repo_name = repo_spec.split('/', 1)
else:
owner = config.get_source_provider_config(source_provider_type)['username']
repo_name = repo_spec
repo = source_provider.get_repository_info(owner, repo_name)
if repo:
repositories.append(repo)
else:
print(f"{Fore.RED}⚠️ Repository {owner}/{repo_name} not found or not accessible{Style.RESET_ALL}")
results = migration_engine.migrate_repositories(repositories)
else:
# Get all accessible repositories
all_repos = source_provider.get_accessible_repositories()
if args.no_interactive:
print(f"{Fore.CYAN}🚀 Migrating all your repositories automatically...{Style.RESET_ALL}")
# Filter to only user's repositories
user_repos = source_provider.get_user_repositories()
results = migration_engine.migrate_repositories(user_repos)
else:
print(f"{Fore.CYAN}🎯 Interactive mode - select repositories to migrate{Style.RESET_ALL}")
username = config.get_source_provider_config(source_provider_type)['username']
selected_repos = select_repositories_interactive(all_repos, username)
results = migration_engine.migrate_repositories(selected_repos)
# Print results
if results:
print_success_summary(results)
else:
print(f"{Fore.YELLOW}⚠️ No repositories found to migrate.{Style.RESET_ALL}")
except ConfigurationError as e:
print(f"{Fore.RED}❌ Configuration error: {e}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}💡 Run '{sys.argv[0]} --setup' to create a configuration template.{Style.RESET_ALL}")
sys.exit(1)
except (ProviderError, MigrationError) as e:
print(f"{Fore.RED}❌ Migration error: {e}{Style.RESET_ALL}")
sys.exit(1)
except Exception as e:
logging.error(f"Unexpected error: {e}")
print(f"{Fore.RED}❌ An unexpected error occurred. Check migration.log for details.{Style.RESET_ALL}")
sys.exit(1)
if __name__ == '__main__':
main()