commit 008cbfd586ab6400e75b90fd4f0c27c48693d7b2 Author: Hugo ODY Date: Fri Jun 27 12:06:31 2025 +0200 :sparkles: Working V1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66706cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Environment variables +.env +.env.local +.env.production +.env.development + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +migration.log + +# Temporary directories for migration +temp_* +migration_*/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3de9c23 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# 🚀 Gitea to GitHub Migration Tool + +Ce projet fournit un outil pratique et modulable pour migrer vos repositories de Gitea vers GitHub automatiquement. + +## ✨ Fonctionnalités + +- **Migration automatique** : Migre tous vos repositories Gitea vers GitHub en une seule commande +- **Migration sélective** : Choisissez spécifiquement quels repositories migrer +- **Support multi-propriétaire** : Migrez des repositories d'autres utilisateurs/organisations auxquels vous avez accès +- **Interface en ligne de commande** : Interface colorée et intuitive +- **Logging complet** : Suivi détaillé des opérations avec fichier de log +- **Gestion des erreurs** : Robuste avec gestion gracieuse des erreurs + +## 🛠 Installation + +1. **Clonez le repository** : +```bash +git clone https://github.com/votre-username/GiteaToGithubMigrator.git +cd GiteaToGithubMigrator +``` + +2. **Configuration automatique** : +```bash +./run.sh --setup +``` + +Le script va automatiquement : +- Créer un environnement virtuel Python +- Installer toutes les dépendances +- Créer le fichier de configuration `.env` + +Cela créera un fichier `.env` que vous devrez remplir avec vos informations : + +```env +# Gitea Configuration +GITEA_URL=https://votre-instance-gitea.com +GITEA_TOKEN=votre_token_gitea +GITEA_USERNAME=votre_nom_utilisateur_gitea + +# GitHub Configuration +GITHUB_TOKEN=votre_token_github +GITHUB_USERNAME=votre_nom_utilisateur_github +``` + +## 🔑 Configuration des tokens + +### Token Gitea +1. Allez dans **Settings** → **Applications** → **Generate New Token** +2. Donnez un nom au token et sélectionnez les permissions : + - `repo` (accès complet aux repositories) + - `user` (accès aux informations utilisateur) + +### Token GitHub +1. Allez dans **Settings** → **Developer settings** → **Personal access tokens** → **Tokens (classic)** +2. Cliquez sur **Generate new token (classic)** +3. Sélectionnez les permissions : + - `repo` (accès complet aux repositories privés) + - `public_repo` (accès aux repositories publics) + +## 🚀 Utilisation + +Après avoir configuré vos tokens dans le fichier `.env`, utilisez le script de lancement : + +### Migration de tous vos repositories +```bash +./run.sh +``` + +### Migration de repositories spécifiques +```bash +./run.sh --repos mon-repo autre-repo +``` + +### Migration de repositories d'autres propriétaires +```bash +./run.sh --repos proprietaire/repo-name +``` + +### Lister les repositories disponibles +```bash +./run.sh --list +``` + +### Mode verbose (plus de détails) +```bash +./run.sh --verbose +``` + +> **💡 Alternative** : Vous pouvez aussi utiliser directement `python migrate.py` si vous avez activé l'environnement virtuel (`source venv/bin/activate`) + +## 📋 Exemples d'utilisation + +### Exemple 1 : Migration complète +```bash +# Migre tous vos repositories +./run.sh +``` + +### Exemple 2 : Migration sélective +```bash +# Migre seulement les repositories spécifiés +./run.sh --repos projet-web api-backend +``` + +### Exemple 3 : Migration depuis une organisation +```bash +# Migre un repository d'une organisation +./run.sh --repos mon-org/projet-important +``` + +### Exemple 4 : Premier lancement (configuration) +```bash +# 1. Setup initial +./run.sh --setup + +# 2. Éditez le fichier .env avec vos credentials +nano .env + +# 3. Listez vos repositories disponibles +./run.sh --list + +# 4. Lancez la migration +./run.sh +``` + +## 📊 Résultats + +L'outil affiche un résumé détaillé à la fin : +- ✅ Nombre de migrations réussies +- ❌ Nombre de migrations échouées +- 📝 Détail par repository + +Tous les logs sont également sauvegardés dans `migration.log`. + +## 🔧 Structure du projet + +``` +GiteaToGithubMigrator/ +├── migrate.py # Script principal +├── config.py # Gestion de la configuration +├── gitea_client.py # Client API Gitea +├── github_client.py # Client API GitHub +├── migration_tool.py # Logique de migration +├── requirements.txt # Dépendances Python +├── .env # Configuration (à créer) +└── README.md # Documentation +``` + +## ⚠️ Prérequis + +- Python 3.7+ +- Git installé sur votre système +- Accès aux APIs Gitea et GitHub +- Tokens d'authentification valides + +## 🛡 Sécurité + +- Les tokens sont stockés dans un fichier `.env` (ajoutez-le à `.gitignore`) +- Les URLs d'authentification ne sont jamais loggées +- Nettoyage automatique des repositories temporaires + +## 🐛 Résolution de problèmes + +### Erreur d'authentification +- Vérifiez que vos tokens sont valides et ont les bonnes permissions +- Assurez-vous que les noms d'utilisateur correspondent + +### Erreur de clonage +- Vérifiez votre connexion internet +- Assurez-vous que Git est installé et accessible + +### Repository déjà existant +- L'outil vérifie automatiquement l'existence sur GitHub +- Les repositories existants sont ignorés avec un avertissement + +## 📝 Logs + +Tous les détails d'exécution sont sauvegardés dans `migration.log` : +- Timestamps des opérations +- Détails des erreurs +- Statistiques de migration + +## 🤝 Contribution + +Les contributions sont les bienvenues ! N'hésitez pas à : +- Signaler des bugs +- Proposer des améliorations +- Soumettre des pull requests + +## 📄 Licence + +Ce projet est sous licence MIT. Voir le fichier LICENSE pour plus de détails. \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..74a19c9 --- /dev/null +++ b/config.py @@ -0,0 +1,45 @@ +""" +Configuration module for Gitea to GitHub migration tool +""" +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +class Config: + """Configuration class for migration settings""" + + def __init__(self): + self.gitea_url = os.getenv('GITEA_URL', 'https://codefirst.iut.uca.fr/git') + self.gitea_token = os.getenv('GITEA_TOKEN') + self.github_token = os.getenv('GITHUB_TOKEN') + self.github_username = os.getenv('GITHUB_USERNAME') + self.gitea_username = os.getenv('GITEA_USERNAME') + + # Validate required configuration + self._validate_config() + + def _validate_config(self): + """Validate that all required configuration is present""" + missing = [] + + if not self.gitea_token: + missing.append('GITEA_TOKEN') + if not self.github_token: + missing.append('GITHUB_TOKEN') + if not self.github_username: + missing.append('GITHUB_USERNAME') + if not self.gitea_username: + missing.append('GITEA_USERNAME') + + if missing: + raise ValueError(f"Missing required environment variables: {', '.join(missing)}") + + def is_valid(self): + """Check if configuration is valid""" + try: + self._validate_config() + return True + except ValueError: + return False \ No newline at end of file diff --git a/gitea_client.py b/gitea_client.py new file mode 100644 index 0000000..b244d8a --- /dev/null +++ b/gitea_client.py @@ -0,0 +1,87 @@ +""" +Gitea API client for repository operations +""" +import requests +import json +from typing import List, Dict, Optional + +class GiteaClient: + """Client for interacting with Gitea API""" + + def __init__(self, base_url: str, token: str, username: str): + self.base_url = base_url.rstrip('/') + self.token = token + self.username = username + self.session = requests.Session() + self.session.headers.update({ + 'Authorization': f'token {token}', + 'Content-Type': 'application/json' + }) + + def get_user_repos(self) -> List[Dict]: + """Get all repositories owned by the user""" + url = f"{self.base_url}/api/v1/user/repos" + params = { + 'limit': 100, # Adjust as needed + 'page': 1 + } + + all_repos = [] + + while True: + response = self.session.get(url, params=params) + response.raise_for_status() + + repos = response.json() + if not repos: + break + + all_repos.extend(repos) + params['page'] += 1 + + # Break if we got less than the limit (last page) + if len(repos) < params.get('limit', 100): + break + + return all_repos + + def get_repo_info(self, owner: str, repo_name: str) -> Optional[Dict]: + """Get information about a specific repository""" + url = f"{self.base_url}/api/v1/repos/{owner}/{repo_name}" + + try: + response = self.session.get(url) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException: + return None + + def get_repo_clone_url(self, owner: str, repo_name: str) -> str: + """Get the clone URL for a repository with authentication""" + return f"{self.base_url}/{owner}/{repo_name}.git" + + def list_accessible_repos(self) -> List[Dict]: + """List all repositories the user has access to (including organizations)""" + url = f"{self.base_url}/api/v1/user/repos" + params = { + 'limit': 100, + 'page': 1 + } + + all_repos = [] + + while True: + response = self.session.get(url, params=params) + response.raise_for_status() + + repos = response.json() + if not repos: + break + + all_repos.extend(repos) + params['page'] += 1 + + if len(repos) < params.get('limit', 100): + break + + return all_repos \ No newline at end of file diff --git a/github_client.py b/github_client.py new file mode 100644 index 0000000..e457480 --- /dev/null +++ b/github_client.py @@ -0,0 +1,54 @@ +""" +GitHub API client for repository operations +""" +from github import Github +from github.GithubException import GithubException +from typing import Optional, Dict +import logging + +logger = logging.getLogger(__name__) + +class GitHubClient: + """Client for interacting with GitHub API""" + + def __init__(self, token: str, username: str): + self.github = Github(token) + self.username = username + self.user = self.github.get_user() + + def create_repository(self, repo_name: str, description: str = "", private: bool = False) -> bool: + """Create a new repository on GitHub""" + try: + # Check if repository already exists + if self.repository_exists(repo_name): + logger.warning(f"Repository {repo_name} already exists on GitHub") + return True + + self.user.create_repo( + name=repo_name, + description=description, + private=private, + auto_init=False # Don't auto-init since we'll push existing content + ) + logger.info(f"Created repository: {repo_name}") + return True + + except GithubException as e: + logger.error(f"Failed to create repository {repo_name}: {e}") + return False + + def repository_exists(self, repo_name: str) -> bool: + """Check if a repository exists""" + try: + self.user.get_repo(repo_name) + return True + except GithubException: + return False + + def get_repo_clone_url(self, repo_name: str) -> str: + """Get the clone URL for a GitHub repository""" + return f"https://github.com/{self.username}/{repo_name}.git" + + def get_authenticated_clone_url(self, repo_name: str, token: str) -> str: + """Get the authenticated clone URL for pushing""" + return f"https://{token}@github.com/{self.username}/{repo_name}.git" \ No newline at end of file diff --git a/migrate.py b/migrate.py new file mode 100755 index 0000000..9ddd59f --- /dev/null +++ b/migrate.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Gitea to GitHub Migration Tool + +This script migrates repositories from Gitea to GitHub. +It can migrate all user repositories or specific ones. +""" + +import argparse +import logging +import sys +from colorama import init, Fore, Style +from pathlib import Path + +from config import Config +from migration_tool import MigrationTool + +# 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""" + banner = f""" +{Fore.CYAN}╔═══════════════════════════════════════════════════════════════╗ +║ ║ +║ 🚀 Gitea to GitHub Migration Tool 🚀 ║ +║ ║ +║ Migrates your repositories from Gitea to GitHub seamlessly ║ +║ ║ +╚═══════════════════════════════════════════════════════════════╝{Style.RESET_ALL} +""" + print(banner) + +def print_success_summary(results: dict): + """Print migration results summary""" + successful = sum(1 for success in results.values() if success) + total = len(results) + + 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 Configuration +GITEA_URL=https://your-gitea-instance.com +GITEA_TOKEN=your_gitea_personal_access_token +GITEA_USERNAME=your_gitea_username + +# GitHub Configuration +GITHUB_TOKEN=your_github_personal_access_token +GITHUB_USERNAME=your_github_username +""" + 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 from Gitea to GitHub", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s # Migrate all your repositories + %(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 + """ + ) + + 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' + ) + + 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 = Config() + + # Initialize migration tool + migration_tool = MigrationTool(config) + + # Handle list command + if args.list: + print(f"{Fore.CYAN}📋 Available repositories:{Style.RESET_ALL}") + repos = migration_tool.list_available_repos() + + for repo in repos: + owner = repo['owner']['login'] + name = repo['name'] + private = "🔒 Private" if repo.get('private', False) else "🌐 Public" + description = repo.get('description', 'No description') + + print(f" {Fore.BLUE}{owner}/{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}") + results = migration_tool.migrate_specific_repos(args.repos) + else: + print(f"{Fore.CYAN}🚀 Migrating all your repositories...{Style.RESET_ALL}") + results = migration_tool.migrate_all_user_repos() + + # Print results + if results: + print_success_summary(results) + else: + print(f"{Fore.YELLOW}⚠️ No repositories found to migrate.{Style.RESET_ALL}") + + except ValueError 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 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() \ No newline at end of file diff --git a/migration_tool.py b/migration_tool.py new file mode 100644 index 0000000..f9b9368 --- /dev/null +++ b/migration_tool.py @@ -0,0 +1,161 @@ +""" +Main migration tool for transferring repositories from Gitea to GitHub +""" +import os +import subprocess +import tempfile +import shutil +import logging +from typing import List, Dict, Optional +from pathlib import Path + +from config import Config +from gitea_client import GiteaClient +from github_client import GitHubClient + +logger = logging.getLogger(__name__) + +class MigrationTool: + """Main tool for migrating repositories from Gitea to GitHub""" + + def __init__(self, config: Config): + self.config = config + self.gitea_client = GiteaClient( + config.gitea_url, + config.gitea_token, + config.gitea_username + ) + self.github_client = GitHubClient( + config.github_token, + config.github_username + ) + + def migrate_all_user_repos(self) -> Dict[str, bool]: + """Migrate all repositories owned by the user""" + logger.info("Fetching user repositories from Gitea...") + repos = self.gitea_client.get_user_repos() + + results = {} + for repo in repos: + if repo['owner']['login'] == self.config.gitea_username: + repo_name = repo['name'] + logger.info(f"Migrating repository: {repo_name}") + success = self.migrate_repository(repo) + results[repo_name] = success + + return results + + def migrate_specific_repos(self, repo_specs: List[str]) -> Dict[str, bool]: + """ + Migrate specific repositories + repo_specs: List of repository specifications in format 'owner/repo' or just 'repo' + """ + results = {} + + for repo_spec in repo_specs: + if '/' in repo_spec: + owner, repo_name = repo_spec.split('/', 1) + else: + owner = self.config.gitea_username + repo_name = repo_spec + + logger.info(f"Migrating repository: {owner}/{repo_name}") + repo_info = self.gitea_client.get_repo_info(owner, repo_name) + + if repo_info: + success = self.migrate_repository(repo_info) + results[f"{owner}/{repo_name}"] = success + else: + logger.error(f"Repository {owner}/{repo_name} not found or not accessible") + results[f"{owner}/{repo_name}"] = False + + return results + + def migrate_repository(self, repo_info: Dict) -> bool: + """Migrate a single repository""" + repo_name = repo_info['name'] + repo_owner = repo_info['owner']['login'] + + try: + # Create GitHub repository + success = self.github_client.create_repository( + repo_name=repo_name, + description=repo_info.get('description', ''), + private=repo_info.get('private', False) + ) + + if not success: + return False + + # Clone and push repository + return self._clone_and_push_repo(repo_owner, repo_name) + + except Exception as e: + logger.error(f"Failed to migrate repository {repo_name}: {e}") + return False + + def _clone_and_push_repo(self, repo_owner: str, repo_name: str) -> bool: + """Clone repository from Gitea and push to GitHub""" + temp_dir = None + + try: + # Create temporary directory + temp_dir = tempfile.mkdtemp(prefix=f"migration_{repo_name}_") + repo_path = Path(temp_dir) / repo_name + + # Clone from Gitea + gitea_url = self._get_authenticated_gitea_url(repo_owner, repo_name) + clone_cmd = ['git', 'clone', '--mirror', gitea_url, str(repo_path)] + + logger.info(f"Cloning repository from Gitea: {repo_owner}/{repo_name}") + result = subprocess.run(clone_cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Failed to clone repository: {result.stderr}") + return False + + # Change to repository directory + os.chdir(repo_path) + + # Add GitHub remote + github_url = self.github_client.get_authenticated_clone_url( + repo_name, + self.config.github_token + ) + + add_remote_cmd = ['git', 'remote', 'add', 'github', github_url] + result = subprocess.run(add_remote_cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Failed to add GitHub remote: {result.stderr}") + return False + + # Push to GitHub + logger.info(f"Pushing repository to GitHub: {repo_name}") + push_cmd = ['git', 'push', '--mirror', 'github'] + result = subprocess.run(push_cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Failed to push to GitHub: {result.stderr}") + return False + + logger.info(f"Successfully migrated repository: {repo_name}") + return True + + except Exception as e: + logger.error(f"Error during repository migration: {e}") + return False + + finally: + # Clean up temporary directory + if temp_dir and os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + def _get_authenticated_gitea_url(self, owner: str, repo_name: str) -> str: + """Get authenticated Gitea URL for cloning""" + base_url = self.config.gitea_url.replace('https://', '').replace('http://', '') + return f"https://{self.config.gitea_username}:{self.config.gitea_token}@{base_url}/{owner}/{repo_name}.git" + + def list_available_repos(self) -> List[Dict]: + """List all repositories available for migration""" + return self.gitea_client.list_accessible_repos() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9b0e80 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests==2.31.0 +PyGithub==1.59.1 +python-dotenv==1.0.0 +colorama==0.4.6 \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..9084f9a --- /dev/null +++ b/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Gitea to GitHub Migration Tool Launcher + +# Check if virtual environment exists +if [ ! -d "venv" ]; then + echo "🔧 Setting up virtual environment..." + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt +else + source venv/bin/activate +fi + +# Run the migration tool with all passed arguments +python migrate.py "$@" \ No newline at end of file