diff --git a/README.md b/README.md index 4a1051e..24d1465 100644 --- a/README.md +++ b/README.md @@ -40,48 +40,39 @@ Cela créera un fichier `.env` que vous devrez remplir avec vos informations sel ## 🔧 Configuration -### Configuration basique (Gitea → GitHub) +### Configuration avec support multi-instances ```env -# Source Provider -SOURCE_PROVIDER=gitea -GITEA_URL=https://votre-instance-gitea.com -GITEA_TOKEN=votre_token_gitea -GITEA_USERNAME=votre_nom_utilisateur_gitea - -# Destination Provider -DESTINATION_PROVIDER=github -GITHUB_TOKEN=votre_token_github -GITHUB_USERNAME=votre_nom_utilisateur_github -``` +# Gitea Source Configuration +GITEA_SOURCE_URL=https://votre-instance-gitea-source.com +GITEA_SOURCE_TOKEN=votre_token_gitea_source +GITEA_SOURCE_USERNAME=votre_nom_utilisateur_gitea_source + +# Gitea Destination Configuration +GITEA_DEST_URL=https://votre-instance-gitea-dest.com +GITEA_DEST_TOKEN=votre_token_gitea_dest +GITEA_DEST_USERNAME=votre_nom_utilisateur_gitea_dest + +# 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 -```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 Configuration (same for source and destination - only one instance) GITHUB_TOKEN=votre_token_github GITHUB_USERNAME=votre_nom_utilisateur_github ``` -### Configuration GitLab → GitLab (migration entre instances) -```env -# Source Provider -SOURCE_PROVIDER=gitlab -GITLAB_URL=https://gitlab-source.com -GITLAB_TOKEN=votre_token_gitlab_source -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 -``` +**📝 Instructions :** +1. **Multi-instances** : Vous pouvez configurer différentes instances du même provider +2. **Même instance** : Utilisez les mêmes credentials pour source et destination si c'est la même instance +3. **Migration flexible** : Supports GitLab → GitLab, Gitea → Gitea, etc. entre différentes instances +4. **Configuration minimale** : Configurez seulement les providers source/destination que vous utilisez +5. L'outil vous demandera interactivement quel provider utiliser comme source et destination ## 🔑 Configuration des tokens @@ -173,33 +164,29 @@ Après la sélection, l'outil propose de renommer les repositories : ## 📋 Exemples d'utilisation -### Exemple 1 : Migration Gitea → GitHub (défaut) +### Exemple 1 : Migration interactive (défaut) ```bash -# Configuration dans .env -SOURCE_PROVIDER=gitea -DESTINATION_PROVIDER=github - -# Interface interactive pour sélectionner les repos +# 1. Configurez vos providers dans .env +# 2. Lancez l'outil ./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 -# Configuration dans .env -SOURCE_PROVIDER=gitlab -DESTINATION_PROVIDER=github - -# Migration automatique +# Migre tous vos repositories automatiquement +# (après sélection interactive des providers) ./run.sh --no-interactive ``` -### Exemple 3 : Migration GitLab → GitLab (entre instances) +### Exemple 3 : Migration sélective ```bash -# Configuration dans .env -SOURCE_PROVIDER=gitlab -DESTINATION_PROVIDER=gitlab - -# Migration sélective +# Migre seulement les repositories spécifiés +# (après sélection interactive des providers) ./run.sh --repos projet-web api-backend ``` @@ -211,17 +198,17 @@ DESTINATION_PROVIDER=gitlab ### Exemple 5 : Premier lancement (configuration) ```bash -# 1. Setup initial +# 1. Setup initial - crée le fichier .env template ./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 -# 3. Listez vos repositories disponibles -./run.sh --list - -# 4. Lancez la migration interactive +# 3. Lancez l'outil - il vous demandera quels providers utiliser ./run.sh + +# 4. Pour lister les repos disponibles (après sélection du provider source) +./run.sh --list ``` ### Exemple 6 : Migration avec renommage @@ -229,12 +216,13 @@ nano .env # 1. Lancer le mode interactif ./run.sh -# 2. Sélectionner les repos à migrer -# 3. Choisir "Y" pour le renommage -# 4. Renommer les repos un par un +# 2. Sélectionner les providers source et destination +# 3. Sélectionner les repos à migrer +# 4. Choisir "Y" pour le renommage +# 5. Renommer les repos un par un # - Appuyer sur ENTRÉE pour garder le nom original # - Taper un nouveau nom pour renommer -# 5. Confirmer et lancer la migration +# 6. Confirmer et lancer la migration ``` ## 📊 Résultats @@ -314,17 +302,20 @@ GitMigrator/ - L'outil vérifie automatiquement l'existence sur le provider de destination - Les repositories existants sont ignorés avec un avertissement -### Provider non supporté -- Vérifiez que le provider est bien configuré dans SOURCE_PROVIDER ou DESTINATION_PROVIDER -- Providers disponibles : gitea, gitlab (source) | github, gitlab (destination) +### Provider non supporté ou non configuré +- Vérifiez que vos providers sont bien configurés dans le fichier .env +- 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 Tous les détails d'exécution sont sauvegardés dans `migration.log` : - Timestamps des opérations +- Sélection des providers source et destination - Détails des erreurs - Statistiques de migration -- Informations sur les providers utilisés +- Informations complètes sur le processus de migration ## 🚀 Extensibilité diff --git a/core/config.py b/core/config.py index 82b5194..ee31031 100644 --- a/core/config.py +++ b/core/config.py @@ -2,75 +2,149 @@ Configuration management for migration tool """ import os -from typing import Dict, Any +from typing import Dict, Any, Optional from dotenv import load_dotenv from providers.base import ConfigurationError -# Load environment variables from .env file -load_dotenv() - class MigrationConfig: """Configuration manager for migration settings""" def __init__(self): - self.source_provider = os.getenv('SOURCE_PROVIDER', 'gitea').lower() - self.destination_provider = os.getenv('DESTINATION_PROVIDER', 'github').lower() - - self.source_config = self._load_source_config() - self.destination_config = self._load_destination_config() + # Reload environment variables from .env file each time + load_dotenv(override=True) - self._validate_config() - - def _load_source_config(self) -> Dict[str, Any]: - """Load source provider configuration""" - if self.source_provider == 'gitea': - return { - 'url': os.getenv('GITEA_URL', 'https://codefirst.iut.uca.fr/git'), - 'token': os.getenv('GITEA_TOKEN'), - 'username': os.getenv('GITEA_USERNAME') - } - elif self.source_provider == 'gitlab': - return { - 'url': os.getenv('GITLAB_URL', 'https://gitlab.com'), - 'token': os.getenv('GITLAB_TOKEN'), - 'username': os.getenv('GITLAB_USERNAME') - } + # 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_gitea_source_config(self) -> Dict[str, Any]: + """Load Gitea source configuration""" + return { + 'url': os.getenv('GITEA_SOURCE_URL', 'https://codefirst.iut.uca.fr/git'), + 'token': os.getenv('GITEA_SOURCE_TOKEN'), + 'username': os.getenv('GITEA_SOURCE_USERNAME') + } + + def _load_gitea_dest_config(self) -> Dict[str, Any]: + """Load Gitea destination configuration""" + return { + 'url': os.getenv('GITEA_DEST_URL', 'https://codefirst.iut.uca.fr/git'), + 'token': os.getenv('GITEA_DEST_TOKEN'), + 'username': os.getenv('GITEA_DEST_USERNAME') + } + + def _load_gitlab_source_config(self) -> Dict[str, Any]: + """Load GitLab source configuration""" + return { + 'url': os.getenv('GITLAB_SOURCE_URL', 'https://gitlab.com'), + 'token': os.getenv('GITLAB_SOURCE_TOKEN'), + 'username': os.getenv('GITLAB_SOURCE_USERNAME') + } + + def _load_gitlab_dest_config(self) -> Dict[str, Any]: + """Load GitLab destination configuration""" + return { + 'url': os.getenv('GITLAB_DEST_URL', 'https://gitlab.com'), + 'token': os.getenv('GITLAB_DEST_TOKEN'), + '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: - raise ConfigurationError(f"Unsupported source provider: {self.source_provider}") - - def _load_destination_config(self) -> Dict[str, Any]: - """Load destination provider configuration""" - if self.destination_provider == 'github': - return { - 'token': os.getenv('GITHUB_TOKEN'), - 'username': os.getenv('GITHUB_USERNAME') - } - elif self.destination_provider == 'gitlab': - return { - 'url': os.getenv('GITLAB_DEST_URL', 'https://gitlab.com'), - 'token': os.getenv('GITLAB_DEST_TOKEN'), - 'username': os.getenv('GITLAB_DEST_USERNAME') - } + raise ConfigurationError(f"Unknown source provider type: {provider_type}") + + def get_destination_provider_config(self, provider_type: str) -> Dict[str, Any]: + """Get destination configuration for a specific provider""" + if provider_type == 'gitea': + return self.gitea_dest_config + elif provider_type == 'gitlab': + return self.gitlab_dest_config + elif provider_type == 'github': + return self.github_config # Same config for source and dest else: - raise ConfigurationError(f"Unsupported destination provider: {self.destination_provider}") - - def _validate_config(self) -> None: - """Validate configuration completeness""" - # Check source config - missing_source = [key for key, value in self.source_config.items() if not value] - if missing_source: - raise ConfigurationError(f"Missing {self.source_provider} source configuration: {', '.join(missing_source)}") - - # Check destination config - missing_dest = [key for key, value in self.destination_config.items() if not value] - if missing_dest: - raise ConfigurationError(f"Missing {self.destination_provider} destination configuration: {', '.join(missing_dest)}") + raise ConfigurationError(f"Unknown destination provider type: {provider_type}") - def is_valid(self) -> bool: - """Check if configuration is valid""" + def is_source_provider_configured(self, provider_type: str) -> bool: + """Check if a source provider is configured (has all required fields)""" try: - self._validate_config() - return True + config = self.get_source_provider_config(provider_type) + return all(value for value in config.values()) except ConfigurationError: - return False \ No newline at end of file + return False + + def is_destination_provider_configured(self, provider_type: str) -> bool: + """Check if a destination provider is configured (has all required fields)""" + try: + config = self.get_destination_provider_config(provider_type) + return all(value for value in config.values()) + except ConfigurationError: + 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() \ No newline at end of file diff --git a/main.py b/main.py index 904a8a1..bb85f64 100755 --- a/main.py +++ b/main.py @@ -4,8 +4,8 @@ Repository Migration Tool A flexible tool for migrating repositories between different Git hosting providers. Currently supports: -- Source providers: Gitea -- Destination providers: GitHub +- Source providers: Gitea, GitLab +- Destination providers: GitHub, GitLab 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.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() @@ -41,21 +42,15 @@ def setup_logging(verbose: bool = False): def print_banner(): """Print application banner""" - banner = f""" -{Fore.CYAN}╔═══════════════════════════════════════════════════════════════╗ -║ ║ -║ 🚀 Repository Migration Tool 🚀 ║ -║ ║ -║ Migrate repositories between Git hosting providers ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝{Style.RESET_ALL} -""" - print(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""" - successful = sum(1 for success in results.values() if success) total = len(results) + successful = sum(1 for success in results.values() if success) print(f"\n{Fore.GREEN}{'='*60}") print(f" MIGRATION SUMMARY") @@ -75,28 +70,36 @@ def create_env_template(): env_file = Path('.env') if not env_file.exists(): - template = """# Source Provider Configuration -SOURCE_PROVIDER=gitea -GITEA_URL=https://codefirst.iut.uca.fr/git -GITEA_TOKEN=your_gitea_personal_access_token -GITEA_USERNAME=your_gitea_username + 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 -# Alternative source provider (GitLab) -# SOURCE_PROVIDER=gitlab -# GITLAB_URL=https://gitlab.com -# GITLAB_TOKEN=your_gitlab_token -# GITLAB_USERNAME=your_gitlab_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 -# Destination Provider Configuration -DESTINATION_PROVIDER=github +# 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 -# Alternative destination provider (GitLab) -# DESTINATION_PROVIDER=gitlab -# GITLAB_DEST_URL=https://gitlab.com -# GITLAB_DEST_TOKEN=your_gitlab_dest_token -# GITLAB_DEST_USERNAME=your_gitlab_dest_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}") @@ -172,14 +175,17 @@ Supported providers: # Initialize configuration config = MigrationConfig() + # Select providers interactively + source_provider_type, destination_provider_type = select_providers() + # Create providers source_provider = ProviderFactory.create_source_provider( - config.source_provider, - config.source_config + source_provider_type, + config.get_source_provider_config(source_provider_type) ) destination_provider = ProviderFactory.create_destination_provider( - config.destination_provider, - config.destination_config + destination_provider_type, + config.get_destination_provider_config(destination_provider_type) ) # Initialize migration engine @@ -187,7 +193,7 @@ Supported providers: # Handle list command 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() for repo in repos: @@ -209,7 +215,7 @@ Supported providers: if '/' in repo_spec: owner, repo_name = repo_spec.split('/', 1) else: - owner = config.source_config['username'] + owner = config.get_source_provider_config(source_provider_type)['username'] repo_name = repo_spec repo = source_provider.get_repository_info(owner, repo_name) @@ -230,7 +236,7 @@ Supported providers: results = migration_engine.migrate_repositories(user_repos) else: 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) results = migration_engine.migrate_repositories(selected_repos) diff --git a/providers/destination/gitea.py b/providers/destination/gitea.py new file mode 100644 index 0000000..244b122 --- /dev/null +++ b/providers/destination/gitea.py @@ -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" \ No newline at end of file diff --git a/providers/factory.py b/providers/factory.py index ec568c2..f3e93e1 100644 --- a/providers/factory.py +++ b/providers/factory.py @@ -5,6 +5,8 @@ from typing import Dict, Type from .base import SourceProvider, DestinationProvider, ConfigurationError from .source.gitea import GiteaSourceProvider from .source.gitlab import GitLabSourceProvider +from .source.github import GitHubSourceProvider +from .destination.gitea import GiteaDestinationProvider from .destination.github import GitHubDestinationProvider from .destination.gitlab import GitLabDestinationProvider @@ -15,9 +17,11 @@ class ProviderFactory: _source_providers: Dict[str, Type[SourceProvider]] = { 'gitea': GiteaSourceProvider, 'gitlab': GitLabSourceProvider, + 'github': GitHubSourceProvider, } _destination_providers: Dict[str, Type[DestinationProvider]] = { + 'gitea': GiteaDestinationProvider, 'github': GitHubDestinationProvider, 'gitlab': GitLabDestinationProvider, } diff --git a/providers/source/github.py b/providers/source/github.py new file mode 100644 index 0000000..d837682 --- /dev/null +++ b/providers/source/github.py @@ -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' + ) \ No newline at end of file diff --git a/ui/provider_selector.py b/ui/provider_selector.py new file mode 100644 index 0000000..3963168 --- /dev/null +++ b/ui/provider_selector.py @@ -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) \ No newline at end of file