commit
008cbfd586
@ -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_*/
|
@ -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.
|
@ -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
|
@ -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
|
@ -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"
|
@ -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()
|
@ -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()
|
@ -0,0 +1,4 @@
|
|||||||
|
requests==2.31.0
|
||||||
|
PyGithub==1.59.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
colorama==0.4.6
|
@ -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 "$@"
|
Loading…
Reference in new issue