Final version

main
Hugo ODY 4 days ago
parent 8fdcdc23b9
commit 957ebaaff0

@ -40,48 +40,39 @@ Cela créera un fichier `.env` que vous devrez remplir avec vos informations sel
## 🔧 Configuration ## 🔧 Configuration
### Configuration basique (Gitea → GitHub) ### Configuration avec support multi-instances
```env ```env
# Source Provider # Gitea Source Configuration
SOURCE_PROVIDER=gitea GITEA_SOURCE_URL=https://votre-instance-gitea-source.com
GITEA_URL=https://votre-instance-gitea.com GITEA_SOURCE_TOKEN=votre_token_gitea_source
GITEA_TOKEN=votre_token_gitea GITEA_SOURCE_USERNAME=votre_nom_utilisateur_gitea_source
GITEA_USERNAME=votre_nom_utilisateur_gitea
# Gitea Destination Configuration
# Destination Provider GITEA_DEST_URL=https://votre-instance-gitea-dest.com
DESTINATION_PROVIDER=github GITEA_DEST_TOKEN=votre_token_gitea_dest
GITHUB_TOKEN=votre_token_github GITEA_DEST_USERNAME=votre_nom_utilisateur_gitea_dest
GITHUB_USERNAME=votre_nom_utilisateur_github
``` # GitLab Source Configuration
GITLAB_SOURCE_URL=https://gitlab-source.com
GITLAB_SOURCE_TOKEN=votre_token_gitlab_source
GITLAB_SOURCE_USERNAME=votre_nom_utilisateur_gitlab_source
# GitLab Destination Configuration
GITLAB_DEST_URL=https://gitlab-dest.com
GITLAB_DEST_TOKEN=votre_token_gitlab_dest
GITLAB_DEST_USERNAME=votre_nom_utilisateur_gitlab_dest
### Configuration GitLab → GitHub # GitHub Configuration (same for source and destination - only one instance)
```env
# Source Provider
SOURCE_PROVIDER=gitlab
GITLAB_URL=https://gitlab.com
GITLAB_TOKEN=votre_token_gitlab
GITLAB_USERNAME=votre_nom_utilisateur_gitlab
# Destination Provider
DESTINATION_PROVIDER=github
GITHUB_TOKEN=votre_token_github GITHUB_TOKEN=votre_token_github
GITHUB_USERNAME=votre_nom_utilisateur_github GITHUB_USERNAME=votre_nom_utilisateur_github
``` ```
### Configuration GitLab → GitLab (migration entre instances) **📝 Instructions :**
```env 1. **Multi-instances** : Vous pouvez configurer différentes instances du même provider
# Source Provider 2. **Même instance** : Utilisez les mêmes credentials pour source et destination si c'est la même instance
SOURCE_PROVIDER=gitlab 3. **Migration flexible** : Supports GitLab → GitLab, Gitea → Gitea, etc. entre différentes instances
GITLAB_URL=https://gitlab-source.com 4. **Configuration minimale** : Configurez seulement les providers source/destination que vous utilisez
GITLAB_TOKEN=votre_token_gitlab_source 5. L'outil vous demandera interactivement quel provider utiliser comme source et destination
GITLAB_USERNAME=votre_nom_utilisateur_source
# Destination Provider
DESTINATION_PROVIDER=gitlab
GITLAB_DEST_URL=https://gitlab-destination.com
GITLAB_DEST_TOKEN=votre_token_gitlab_dest
GITLAB_DEST_USERNAME=votre_nom_utilisateur_dest
```
## 🔑 Configuration des tokens ## 🔑 Configuration des tokens
@ -173,33 +164,29 @@ Après la sélection, l'outil propose de renommer les repositories :
## 📋 Exemples d'utilisation ## 📋 Exemples d'utilisation
### Exemple 1 : Migration Gitea → GitHub (défaut) ### Exemple 1 : Migration interactive (défaut)
```bash ```bash
# Configuration dans .env # 1. Configurez vos providers dans .env
SOURCE_PROVIDER=gitea # 2. Lancez l'outil
DESTINATION_PROVIDER=github
# Interface interactive pour sélectionner les repos
./run.sh ./run.sh
# L'outil vous demandera :
# - Quel provider utiliser comme source
# - Quel provider utiliser comme destination
# - Puis vous pourrez sélectionner les repos à migrer
``` ```
### Exemple 2 : Migration GitLab → GitHub ### Exemple 2 : Migration automatique
```bash ```bash
# Configuration dans .env # Migre tous vos repositories automatiquement
SOURCE_PROVIDER=gitlab # (après sélection interactive des providers)
DESTINATION_PROVIDER=github
# Migration automatique
./run.sh --no-interactive ./run.sh --no-interactive
``` ```
### Exemple 3 : Migration GitLab → GitLab (entre instances) ### Exemple 3 : Migration sélective
```bash ```bash
# Configuration dans .env # Migre seulement les repositories spécifiés
SOURCE_PROVIDER=gitlab # (après sélection interactive des providers)
DESTINATION_PROVIDER=gitlab
# Migration sélective
./run.sh --repos projet-web api-backend ./run.sh --repos projet-web api-backend
``` ```
@ -211,17 +198,17 @@ DESTINATION_PROVIDER=gitlab
### Exemple 5 : Premier lancement (configuration) ### Exemple 5 : Premier lancement (configuration)
```bash ```bash
# 1. Setup initial # 1. Setup initial - crée le fichier .env template
./run.sh --setup ./run.sh --setup
# 2. Éditez le fichier .env avec vos credentials et providers # 2. Éditez le fichier .env avec vos credentials (au moins 2 providers)
nano .env nano .env
# 3. Listez vos repositories disponibles # 3. Lancez l'outil - il vous demandera quels providers utiliser
./run.sh --list
# 4. Lancez la migration interactive
./run.sh ./run.sh
# 4. Pour lister les repos disponibles (après sélection du provider source)
./run.sh --list
``` ```
### Exemple 6 : Migration avec renommage ### Exemple 6 : Migration avec renommage
@ -229,12 +216,13 @@ nano .env
# 1. Lancer le mode interactif # 1. Lancer le mode interactif
./run.sh ./run.sh
# 2. Sélectionner les repos à migrer # 2. Sélectionner les providers source et destination
# 3. Choisir "Y" pour le renommage # 3. Sélectionner les repos à migrer
# 4. Renommer les repos un par un # 4. Choisir "Y" pour le renommage
# 5. Renommer les repos un par un
# - Appuyer sur ENTRÉE pour garder le nom original # - Appuyer sur ENTRÉE pour garder le nom original
# - Taper un nouveau nom pour renommer # - Taper un nouveau nom pour renommer
# 5. Confirmer et lancer la migration # 6. Confirmer et lancer la migration
``` ```
## 📊 Résultats ## 📊 Résultats
@ -314,17 +302,20 @@ GitMigrator/
- L'outil vérifie automatiquement l'existence sur le provider de destination - L'outil vérifie automatiquement l'existence sur le provider de destination
- Les repositories existants sont ignorés avec un avertissement - Les repositories existants sont ignorés avec un avertissement
### Provider non supporté ### Provider non supporté ou non configuré
- Vérifiez que le provider est bien configuré dans SOURCE_PROVIDER ou DESTINATION_PROVIDER - Vérifiez que vos providers sont bien configurés dans le fichier .env
- Providers disponibles : gitea, gitlab (source) | github, gitlab (destination) - Assurez-vous d'avoir au moins 2 providers configurés
- Providers disponibles : gitea, gitlab, github
- L'outil vous indiquera quels providers sont configurés au démarrage
## 📝 Logs ## 📝 Logs
Tous les détails d'exécution sont sauvegardés dans `migration.log` : Tous les détails d'exécution sont sauvegardés dans `migration.log` :
- Timestamps des opérations - Timestamps des opérations
- Sélection des providers source et destination
- Détails des erreurs - Détails des erreurs
- Statistiques de migration - Statistiques de migration
- Informations sur les providers utilisés - Informations complètes sur le processus de migration
## 🚀 Extensibilité ## 🚀 Extensibilité

@ -2,75 +2,149 @@
Configuration management for migration tool Configuration management for migration tool
""" """
import os import os
from typing import Dict, Any from typing import Dict, Any, Optional
from dotenv import load_dotenv from dotenv import load_dotenv
from providers.base import ConfigurationError from providers.base import ConfigurationError
# Load environment variables from .env file
load_dotenv()
class MigrationConfig: class MigrationConfig:
"""Configuration manager for migration settings""" """Configuration manager for migration settings"""
def __init__(self): def __init__(self):
self.source_provider = os.getenv('SOURCE_PROVIDER', 'gitea').lower() # Reload environment variables from .env file each time
self.destination_provider = os.getenv('DESTINATION_PROVIDER', 'github').lower() load_dotenv(override=True)
self.source_config = self._load_source_config()
self.destination_config = self._load_destination_config()
self._validate_config() # Load all provider configurations (source and destination)
self.gitea_source_config = self._load_gitea_source_config()
self.gitea_dest_config = self._load_gitea_dest_config()
self.gitlab_source_config = self._load_gitlab_source_config()
self.gitlab_dest_config = self._load_gitlab_dest_config()
self.github_config = self._load_github_config() # Single config for GitHub
def _load_source_config(self) -> Dict[str, Any]: def _load_gitea_source_config(self) -> Dict[str, Any]:
"""Load source provider configuration""" """Load Gitea source configuration"""
if self.source_provider == 'gitea':
return { return {
'url': os.getenv('GITEA_URL', 'https://codefirst.iut.uca.fr/git'), 'url': os.getenv('GITEA_SOURCE_URL', 'https://codefirst.iut.uca.fr/git'),
'token': os.getenv('GITEA_TOKEN'), 'token': os.getenv('GITEA_SOURCE_TOKEN'),
'username': os.getenv('GITEA_USERNAME') 'username': os.getenv('GITEA_SOURCE_USERNAME')
} }
elif self.source_provider == 'gitlab':
def _load_gitea_dest_config(self) -> Dict[str, Any]:
"""Load Gitea destination configuration"""
return { return {
'url': os.getenv('GITLAB_URL', 'https://gitlab.com'), 'url': os.getenv('GITEA_DEST_URL', 'https://codefirst.iut.uca.fr/git'),
'token': os.getenv('GITLAB_TOKEN'), 'token': os.getenv('GITEA_DEST_TOKEN'),
'username': os.getenv('GITLAB_USERNAME') 'username': os.getenv('GITEA_DEST_USERNAME')
} }
else:
raise ConfigurationError(f"Unsupported source provider: {self.source_provider}")
def _load_destination_config(self) -> Dict[str, Any]: def _load_gitlab_source_config(self) -> Dict[str, Any]:
"""Load destination provider configuration""" """Load GitLab source configuration"""
if self.destination_provider == 'github':
return { return {
'token': os.getenv('GITHUB_TOKEN'), 'url': os.getenv('GITLAB_SOURCE_URL', 'https://gitlab.com'),
'username': os.getenv('GITHUB_USERNAME') 'token': os.getenv('GITLAB_SOURCE_TOKEN'),
'username': os.getenv('GITLAB_SOURCE_USERNAME')
} }
elif self.destination_provider == 'gitlab':
def _load_gitlab_dest_config(self) -> Dict[str, Any]:
"""Load GitLab destination configuration"""
return { return {
'url': os.getenv('GITLAB_DEST_URL', 'https://gitlab.com'), 'url': os.getenv('GITLAB_DEST_URL', 'https://gitlab.com'),
'token': os.getenv('GITLAB_DEST_TOKEN'), 'token': os.getenv('GITLAB_DEST_TOKEN'),
'username': os.getenv('GITLAB_DEST_USERNAME') 'username': os.getenv('GITLAB_DEST_USERNAME')
} }
def _load_github_config(self) -> Dict[str, Any]:
"""Load GitHub configuration"""
return {
'token': os.getenv('GITHUB_TOKEN'),
'username': os.getenv('GITHUB_USERNAME')
}
def get_source_provider_config(self, provider_type: str) -> Dict[str, Any]:
"""Get source configuration for a specific provider"""
if provider_type == 'gitea':
return self.gitea_source_config
elif provider_type == 'gitlab':
return self.gitlab_source_config
elif provider_type == 'github':
return self.github_config # Same config for source and dest
else: else:
raise ConfigurationError(f"Unsupported destination provider: {self.destination_provider}") raise ConfigurationError(f"Unknown source provider type: {provider_type}")
def _validate_config(self) -> None: def get_destination_provider_config(self, provider_type: str) -> Dict[str, Any]:
"""Validate configuration completeness""" """Get destination configuration for a specific provider"""
# Check source config if provider_type == 'gitea':
missing_source = [key for key, value in self.source_config.items() if not value] return self.gitea_dest_config
if missing_source: elif provider_type == 'gitlab':
raise ConfigurationError(f"Missing {self.source_provider} source configuration: {', '.join(missing_source)}") return self.gitlab_dest_config
elif provider_type == 'github':
return self.github_config # Same config for source and dest
else:
raise ConfigurationError(f"Unknown destination provider type: {provider_type}")
# Check destination config def is_source_provider_configured(self, provider_type: str) -> bool:
missing_dest = [key for key, value in self.destination_config.items() if not value] """Check if a source provider is configured (has all required fields)"""
if missing_dest: try:
raise ConfigurationError(f"Missing {self.destination_provider} destination configuration: {', '.join(missing_dest)}") config = self.get_source_provider_config(provider_type)
return all(value for value in config.values())
except ConfigurationError:
return False
def is_valid(self) -> bool: def is_destination_provider_configured(self, provider_type: str) -> bool:
"""Check if configuration is valid""" """Check if a destination provider is configured (has all required fields)"""
try: try:
self._validate_config() config = self.get_destination_provider_config(provider_type)
return True return all(value for value in config.values())
except ConfigurationError: except ConfigurationError:
return False return False
def get_available_source_providers(self) -> Dict[str, bool]:
"""Get list of source providers and their configuration status"""
return {
'gitea': self.is_source_provider_configured('gitea'),
'gitlab': self.is_source_provider_configured('gitlab'),
'github': self.is_source_provider_configured('github')
}
def get_available_destination_providers(self) -> Dict[str, bool]:
"""Get list of destination providers and their configuration status"""
return {
'gitea': self.is_destination_provider_configured('gitea'),
'gitlab': self.is_destination_provider_configured('gitlab'),
'github': self.is_destination_provider_configured('github')
}
def validate_source_provider_config(self, provider_type: str) -> None:
"""Validate source configuration for a specific provider"""
config = self.get_source_provider_config(provider_type)
missing = [key for key, value in config.items() if not value]
if missing:
raise ConfigurationError(f"Missing {provider_type} source configuration: {', '.join(missing)}")
def validate_destination_provider_config(self, provider_type: str) -> None:
"""Validate destination configuration for a specific provider"""
config = self.get_destination_provider_config(provider_type)
missing = [key for key, value in config.items() if not value]
if missing:
raise ConfigurationError(f"Missing {provider_type} destination configuration: {', '.join(missing)}")
def is_valid(self) -> bool:
"""Check if at least one source and one destination provider are configured"""
source_configured = any(self.get_available_source_providers().values())
dest_configured = any(self.get_available_destination_providers().values())
return source_configured and dest_configured
# Méthodes dépréciées pour compatibilité (si jamais utilisées ailleurs)
def get_provider_config(self, provider_type: str) -> Dict[str, Any]:
"""DEPRECATED: Use get_source_provider_config or get_destination_provider_config"""
return self.get_source_provider_config(provider_type)
def is_provider_configured(self, provider_type: str) -> bool:
"""DEPRECATED: Use is_source_provider_configured or is_destination_provider_configured"""
return self.is_source_provider_configured(provider_type)
def get_available_providers(self) -> Dict[str, bool]:
"""DEPRECATED: Use get_available_source_providers or get_available_destination_providers"""
return self.get_available_source_providers()

@ -4,8 +4,8 @@ Repository Migration Tool
A flexible tool for migrating repositories between different Git hosting providers. A flexible tool for migrating repositories between different Git hosting providers.
Currently supports: Currently supports:
- Source providers: Gitea - Source providers: Gitea, GitLab
- Destination providers: GitHub - Destination providers: GitHub, GitLab
Future providers can be easily added through the extensible provider system. Future providers can be easily added through the extensible provider system.
""" """
@ -21,6 +21,7 @@ from core.migration_engine import MigrationEngine
from providers.factory import ProviderFactory from providers.factory import ProviderFactory
from providers.base import ConfigurationError, ProviderError, MigrationError from providers.base import ConfigurationError, ProviderError, MigrationError
from ui.interactive_selector import select_repositories_interactive from ui.interactive_selector import select_repositories_interactive
from ui.provider_selector import select_providers
# Initialize colorama for cross-platform colored output # Initialize colorama for cross-platform colored output
init() init()
@ -41,21 +42,15 @@ def setup_logging(verbose: bool = False):
def print_banner(): def print_banner():
"""Print application banner""" """Print application banner"""
banner = f""" print(f"{Fore.MAGENTA}{'='*60}")
{Fore.CYAN} print(f"{'🚀 GIT MIGRATION TOOL':^60}")
print(f"{'Multi-Provider Repository Migration':^60}")
🚀 Repository Migration Tool 🚀 print(f"{'='*60}{Style.RESET_ALL}")
Migrate repositories between Git hosting providers
{Style.RESET_ALL}
"""
print(banner)
def print_success_summary(results: dict): def print_success_summary(results: dict):
"""Print migration results summary""" """Print migration results summary"""
successful = sum(1 for success in results.values() if success)
total = len(results) total = len(results)
successful = sum(1 for success in results.values() if success)
print(f"\n{Fore.GREEN}{'='*60}") print(f"\n{Fore.GREEN}{'='*60}")
print(f" MIGRATION SUMMARY") print(f" MIGRATION SUMMARY")
@ -75,28 +70,36 @@ def create_env_template():
env_file = Path('.env') env_file = Path('.env')
if not env_file.exists(): if not env_file.exists():
template = """# Source Provider Configuration template = """# Gitea Source Configuration
SOURCE_PROVIDER=gitea GITEA_SOURCE_URL=https://codefirst.iut.uca.fr/git
GITEA_URL=https://codefirst.iut.uca.fr/git GITEA_SOURCE_TOKEN=your_gitea_source_personal_access_token
GITEA_TOKEN=your_gitea_personal_access_token GITEA_SOURCE_USERNAME=your_gitea_source_username
GITEA_USERNAME=your_gitea_username
# Gitea Destination Configuration
# Alternative source provider (GitLab) GITEA_DEST_URL=https://codefirst.iut.uca.fr/git
# SOURCE_PROVIDER=gitlab GITEA_DEST_TOKEN=your_gitea_dest_personal_access_token
# GITLAB_URL=https://gitlab.com GITEA_DEST_USERNAME=your_gitea_dest_username
# GITLAB_TOKEN=your_gitlab_token
# GITLAB_USERNAME=your_gitlab_username # GitLab Source Configuration
GITLAB_SOURCE_URL=https://gitlab.com
# Destination Provider Configuration GITLAB_SOURCE_TOKEN=your_gitlab_source_token
DESTINATION_PROVIDER=github 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_TOKEN=your_github_personal_access_token
GITHUB_USERNAME=your_github_username GITHUB_USERNAME=your_github_username
# Alternative destination provider (GitLab) # Instructions:
# DESTINATION_PROVIDER=gitlab # 1. Fill in the credentials for the providers you want to use as source or destination
# GITLAB_DEST_URL=https://gitlab.com # 2. You can use the same credentials for source and dest if it's the same instance
# GITLAB_DEST_TOKEN=your_gitlab_dest_token # 3. For migrations between different instances of the same provider, use different credentials
# GITLAB_DEST_USERNAME=your_gitlab_dest_username # 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) env_file.write_text(template)
print(f"{Fore.YELLOW}📝 Created .env template file. Please fill it with your credentials.{Style.RESET_ALL}") print(f"{Fore.YELLOW}📝 Created .env template file. Please fill it with your credentials.{Style.RESET_ALL}")
@ -172,14 +175,17 @@ Supported providers:
# Initialize configuration # Initialize configuration
config = MigrationConfig() config = MigrationConfig()
# Select providers interactively
source_provider_type, destination_provider_type = select_providers()
# Create providers # Create providers
source_provider = ProviderFactory.create_source_provider( source_provider = ProviderFactory.create_source_provider(
config.source_provider, source_provider_type,
config.source_config config.get_source_provider_config(source_provider_type)
) )
destination_provider = ProviderFactory.create_destination_provider( destination_provider = ProviderFactory.create_destination_provider(
config.destination_provider, destination_provider_type,
config.destination_config config.get_destination_provider_config(destination_provider_type)
) )
# Initialize migration engine # Initialize migration engine
@ -187,7 +193,7 @@ Supported providers:
# Handle list command # Handle list command
if args.list: if args.list:
print(f"{Fore.CYAN}📋 Available repositories from {config.source_provider}:{Style.RESET_ALL}") print(f"{Fore.CYAN}📋 Available repositories from {source_provider_type}:{Style.RESET_ALL}")
repos = source_provider.get_accessible_repositories() repos = source_provider.get_accessible_repositories()
for repo in repos: for repo in repos:
@ -209,7 +215,7 @@ Supported providers:
if '/' in repo_spec: if '/' in repo_spec:
owner, repo_name = repo_spec.split('/', 1) owner, repo_name = repo_spec.split('/', 1)
else: else:
owner = config.source_config['username'] owner = config.get_source_provider_config(source_provider_type)['username']
repo_name = repo_spec repo_name = repo_spec
repo = source_provider.get_repository_info(owner, repo_name) repo = source_provider.get_repository_info(owner, repo_name)
@ -230,7 +236,7 @@ Supported providers:
results = migration_engine.migrate_repositories(user_repos) results = migration_engine.migrate_repositories(user_repos)
else: else:
print(f"{Fore.CYAN}🎯 Interactive mode - select repositories to migrate{Style.RESET_ALL}") print(f"{Fore.CYAN}🎯 Interactive mode - select repositories to migrate{Style.RESET_ALL}")
username = config.source_config['username'] username = config.get_source_provider_config(source_provider_type)['username']
selected_repos = select_repositories_interactive(all_repos, username) selected_repos = select_repositories_interactive(all_repos, username)
results = migration_engine.migrate_repositories(selected_repos) results = migration_engine.migrate_repositories(selected_repos)

@ -0,0 +1,92 @@
"""
Gitea destination provider implementation
"""
import logging
import requests
from typing import Dict
from ..base import DestinationProvider, Repository, ProviderError, ConfigurationError
logger = logging.getLogger(__name__)
class GiteaDestinationProvider(DestinationProvider):
"""Gitea destination provider implementation"""
def _validate_config(self) -> None:
"""Validate Gitea-specific configuration"""
required_keys = ['url', 'token', 'username']
missing = [key for key in required_keys if not self.config.get(key)]
if missing:
raise ConfigurationError(f"Missing Gitea configuration: {', '.join(missing)}")
self.base_url = self.config['url'].rstrip('/')
self.token = self.config['token']
self.username = self.config['username']
# Setup HTTP session
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'token {self.token}',
'Content-Type': 'application/json'
})
# Verify authentication
try:
response = self.session.get(f"{self.base_url}/api/v1/user")
response.raise_for_status()
except requests.RequestException as e:
raise ConfigurationError(f"Failed to authenticate with Gitea: {e}")
def create_repository(self, repository: Repository, target_name: str) -> bool:
"""Create a new repository on Gitea"""
response = None
try:
# Check if repository already exists
if self.repository_exists(target_name):
logger.warning(f"Repository {target_name} already exists on Gitea")
return True
repo_data = {
'name': target_name,
'description': repository.description or '',
'private': repository.private,
'auto_init': False # Don't auto-init since we'll push existing content
}
response = self.session.post(
f"{self.base_url}/api/v1/user/repos",
json=repo_data
)
response.raise_for_status()
logger.info(f"Created repository: {target_name}")
return True
except requests.RequestException as e:
logger.error(f"Failed to create repository {target_name}: {e}")
if response and response.status_code == 409:
# Conflict - repository already exists
logger.warning(f"Repository {target_name} already exists (conflict)")
return True
elif response and response.status_code == 422:
# Unprocessable Entity - might already exist or name is invalid
logger.warning(f"Repository creation failed, possibly already exists: {target_name}")
return self.repository_exists(target_name)
raise ProviderError(f"Failed to create Gitea repository: {e}")
except Exception as e:
logger.error(f"Unexpected error creating repository {target_name}: {e}")
return False
def repository_exists(self, name: str) -> bool:
"""Check if a repository exists"""
try:
response = self.session.get(f"{self.base_url}/api/v1/repos/{self.username}/{name}")
return response.status_code == 200
except requests.RequestException:
return False
def get_authenticated_push_url(self, name: str) -> str:
"""Get authenticated URL for pushing to repository"""
base_url = self.base_url.replace('https://', '').replace('http://', '')
return f"https://{self.username}:{self.token}@{base_url}/{self.username}/{name}.git"

@ -5,6 +5,8 @@ from typing import Dict, Type
from .base import SourceProvider, DestinationProvider, ConfigurationError from .base import SourceProvider, DestinationProvider, ConfigurationError
from .source.gitea import GiteaSourceProvider from .source.gitea import GiteaSourceProvider
from .source.gitlab import GitLabSourceProvider from .source.gitlab import GitLabSourceProvider
from .source.github import GitHubSourceProvider
from .destination.gitea import GiteaDestinationProvider
from .destination.github import GitHubDestinationProvider from .destination.github import GitHubDestinationProvider
from .destination.gitlab import GitLabDestinationProvider from .destination.gitlab import GitLabDestinationProvider
@ -15,9 +17,11 @@ class ProviderFactory:
_source_providers: Dict[str, Type[SourceProvider]] = { _source_providers: Dict[str, Type[SourceProvider]] = {
'gitea': GiteaSourceProvider, 'gitea': GiteaSourceProvider,
'gitlab': GitLabSourceProvider, 'gitlab': GitLabSourceProvider,
'github': GitHubSourceProvider,
} }
_destination_providers: Dict[str, Type[DestinationProvider]] = { _destination_providers: Dict[str, Type[DestinationProvider]] = {
'gitea': GiteaDestinationProvider,
'github': GitHubDestinationProvider, 'github': GitHubDestinationProvider,
'gitlab': GitLabDestinationProvider, 'gitlab': GitLabDestinationProvider,
} }

@ -0,0 +1,84 @@
"""
GitHub source provider implementation
"""
import logging
from github import Github
from github.GithubException import GithubException
from typing import List, Dict, Optional
from ..base import SourceProvider, Repository, ProviderError, ConfigurationError
logger = logging.getLogger(__name__)
class GitHubSourceProvider(SourceProvider):
"""GitHub source provider implementation"""
def _validate_config(self) -> None:
"""Validate GitHub-specific configuration"""
required_keys = ['token', 'username']
missing = [key for key in required_keys if not self.config.get(key)]
if missing:
raise ConfigurationError(f"Missing GitHub configuration: {', '.join(missing)}")
self.token = self.config['token']
self.username = self.config['username']
try:
self.github = Github(self.token)
self.user = self.github.get_user()
except GithubException as e:
raise ConfigurationError(f"Failed to authenticate with GitHub: {e}")
def get_user_repositories(self) -> List[Repository]:
"""Get repositories owned by the authenticated user"""
repositories = self.get_accessible_repositories()
return [repo for repo in repositories if repo.owner == self.username]
def get_accessible_repositories(self) -> List[Repository]:
"""Get all repositories accessible to the authenticated user"""
all_repos = []
try:
# Get user's own repositories
user_repos = self.user.get_repos()
all_repos.extend([self._parse_repository(repo) for repo in user_repos])
# Get repositories from organizations the user belongs to
for org in self.user.get_orgs():
try:
org_repos = org.get_repos()
all_repos.extend([self._parse_repository(repo) for repo in org_repos])
except GithubException as e:
logger.warning(f"Could not fetch repositories from organization {org.login}: {e}")
continue
except GithubException as e:
raise ProviderError(f"Failed to fetch repositories from GitHub: {e}")
return all_repos
def get_repository_info(self, owner: str, name: str) -> Optional[Repository]:
"""Get information about a specific repository"""
try:
repo = self.github.get_repo(f"{owner}/{name}")
return self._parse_repository(repo)
except GithubException:
return None
def get_authenticated_clone_url(self, repository: Repository) -> str:
"""Get authenticated clone URL for a repository"""
return f"https://{self.token}@github.com/{repository.owner}/{repository.name}.git"
def _parse_repository(self, repo) -> Repository:
"""Parse repository data from GitHub API response"""
return Repository(
name=repo.name,
owner=repo.owner.login,
description=repo.description or '',
private=repo.private,
clone_url=repo.clone_url,
ssh_url=repo.ssh_url,
web_url=repo.html_url,
default_branch=repo.default_branch or 'main'
)

@ -0,0 +1,127 @@
"""
Provider selection interface
"""
from colorama import Fore, Style
from providers.factory import ProviderFactory
from core.config import MigrationConfig
def select_providers() -> tuple[str, str]:
"""
Interactive provider selection for source and destination
Returns tuple (source_provider, destination_provider)
"""
config = MigrationConfig()
# Get available providers from factory
available_source_providers = ProviderFactory.get_available_source_providers()
available_destination_providers = ProviderFactory.get_available_destination_providers()
print(f"\n{Fore.CYAN}🔧 Provider Configuration{Style.RESET_ALL}")
print(f"{Fore.YELLOW}Available providers by type:{Style.RESET_ALL}")
print(f" 📥 Source: {', '.join(available_source_providers).upper()}")
print(f" 📤 Destination: {', '.join(available_destination_providers).upper()}")
# Check configured providers
source_provider_configs = config.get_available_source_providers()
dest_provider_configs = config.get_available_destination_providers()
print(f"\n{Fore.YELLOW}Source provider configuration status:{Style.RESET_ALL}")
configured_source_providers = []
for provider, is_configured in source_provider_configs.items():
if provider in available_source_providers:
status = f"{Fore.GREEN}✅ Configured" if is_configured else f"{Fore.RED}❌ Not configured"
print(f" {provider.upper()}: {status}{Style.RESET_ALL}")
if is_configured:
configured_source_providers.append(provider)
print(f"\n{Fore.YELLOW}Destination provider configuration status:{Style.RESET_ALL}")
configured_destination_providers = []
for provider, is_configured in dest_provider_configs.items():
if provider in available_destination_providers:
status = f"{Fore.GREEN}✅ Configured" if is_configured else f"{Fore.RED}❌ Not configured"
print(f" {provider.upper()}: {status}{Style.RESET_ALL}")
if is_configured:
configured_destination_providers.append(provider)
# Validate we have at least one configured source and destination
if not configured_source_providers:
print(f"\n{Fore.RED}❌ No source providers configured!{Style.RESET_ALL}")
print(f"{Fore.YELLOW}💡 Please configure at least one source provider in .env:{Style.RESET_ALL}")
for provider in available_source_providers:
if provider == 'gitea':
print(f" GITEA_SOURCE_URL, GITEA_SOURCE_TOKEN, GITEA_SOURCE_USERNAME")
elif provider == 'gitlab':
print(f" GITLAB_SOURCE_URL, GITLAB_SOURCE_TOKEN, GITLAB_SOURCE_USERNAME")
elif provider == 'github':
print(f" GITHUB_TOKEN, GITHUB_USERNAME")
exit(1)
if not configured_destination_providers:
print(f"\n{Fore.RED}❌ No destination providers configured!{Style.RESET_ALL}")
print(f"{Fore.YELLOW}💡 Please configure at least one destination provider in .env:{Style.RESET_ALL}")
for provider in available_destination_providers:
if provider == 'gitea':
print(f" GITEA_DEST_URL, GITEA_DEST_TOKEN, GITEA_DEST_USERNAME")
elif provider == 'github':
print(f" GITHUB_TOKEN, GITHUB_USERNAME")
elif provider == 'gitlab':
print(f" GITLAB_DEST_URL, GITLAB_DEST_TOKEN, GITLAB_DEST_USERNAME")
exit(1)
# Select source provider
print(f"\n{Fore.CYAN}📥 Select SOURCE provider:{Style.RESET_ALL}")
source_provider = _select_provider(configured_source_providers, "source")
# Select destination provider (exclude GitHub → GitHub)
available_destinations = configured_destination_providers.copy()
if source_provider == 'github' and 'github' in available_destinations:
available_destinations.remove('github')
print(f"\n{Fore.YELLOW} GitHub → GitHub migration not supported (same instance){Style.RESET_ALL}")
if not available_destinations:
print(f"\n{Fore.RED}❌ No valid destination providers available!{Style.RESET_ALL}")
if source_provider == 'github':
print(f"{Fore.YELLOW}GitHub can only migrate TO other providers, not to itself.{Style.RESET_ALL}")
exit(1)
# Select destination provider
print(f"\n{Fore.CYAN}📤 Select DESTINATION provider:{Style.RESET_ALL}")
destination_provider = _select_provider(available_destinations, "destination")
print(f"\n{Fore.GREEN}✅ Migration will be: {source_provider.upper()}{destination_provider.upper()}{Style.RESET_ALL}")
return source_provider, destination_provider
def _select_provider(providers: list, provider_type: str) -> str:
"""
Select a provider from the available list
"""
if len(providers) == 1:
print(f"{Fore.GREEN}✅ Only one {provider_type} provider available: {providers[0].upper()}{Style.RESET_ALL}")
return providers[0]
while True:
print(f"\n{Fore.YELLOW}Available {provider_type} providers:{Style.RESET_ALL}")
for i, provider in enumerate(providers, 1):
print(f" {i}. {provider.upper()}")
try:
choice = input(f"\nEnter your choice (1-{len(providers)}): ").strip()
if choice.isdigit():
index = int(choice) - 1
if 0 <= index < len(providers):
selected = providers[index]
print(f"{Fore.GREEN}✅ Selected {provider_type}: {selected.upper()}{Style.RESET_ALL}")
return selected
print(f"{Fore.RED}❌ Invalid choice. Please enter a number between 1 and {len(providers)}.{Style.RESET_ALL}")
except KeyboardInterrupt:
print(f"\n{Fore.YELLOW}🛑 Migration cancelled.{Style.RESET_ALL}")
exit(0)
except EOFError:
print(f"\n{Fore.YELLOW}🛑 Migration cancelled.{Style.RESET_ALL}")
exit(0)
Loading…
Cancel
Save