Rename and sort

main
Hugo ODY 3 days ago
parent 7595c7b988
commit 3f546b745b

@ -8,6 +8,7 @@ Ce projet fournit un outil pratique et modulable pour migrer vos repositories de
- **Mode interactif par défaut** : Interface élégante pour sélectionner/déselectionner les repos à migrer
- **Vision complète** : Voit tous les repositories accessibles (vos repos + ceux d'organisations)
- **Sélection intelligente** : Vos repositories sont pré-sélectionnés, les autres sont désélectionnés par défaut
- **Renommage intelligent** : Possibilité de renommer les repositories lors de la migration
- **Migration sélective** : Choisissez spécifiquement quels repositories migrer en ligne de commande
- **Interface en ligne de commande** : Interface colorée et intuitive avec navigation au clavier
- **Logging complet** : Suivi détaillé des opérations avec fichier de log
@ -104,15 +105,23 @@ Le mode interactif (activé par défaut) offre une **interface utilisateur élé
- **ESPACE** : Cocher/décocher un repository
- **A** : Sélectionner tous les repositories
- **N** : Désélectionner tous les repositories
- **ENTRÉE** : Confirmer la sélection et lancer la migration
- **ENTRÉE** : Confirmer la sélection et passer au renommage (optionnel)
- **Q** : Quitter sans migrer
### Interface de renommage :
Après la sélection, l'outil propose de renommer les repositories :
- **Y** : Ouvrir l'interface de renommage
- **N/ENTRÉE** : Conserver les noms actuels
- **Validation automatique** des noms de repositories GitHub
### Fonctionnalités :
- ✅ **Checkboxes visuelles** avec émojis
- 👤 **Distinction propriétaire** : Vos repos vs repos d'autres utilisateurs
- 🎯 **Sélection intelligente** : Vos repos pré-sélectionnés par défaut
- 📋 **Tri intelligent** : Vos repos en premier, puis les autres, tous par ordre alphabétique
- ✏️ **Renommage optionnel** : Possibilité de renommer les repos sur GitHub
- 📄 **Pagination automatique** (15 repos par page)
- 🎨 **Interface colorée** avec mise en surbrillance
- 🎨 **Interface colorée** avec mise en surbrillance et séparateurs visuels
- 📊 **Compteur en temps réel** des repos sélectionnés
- 🔒 **Indicateurs visuels** (privé/public)
- 📝 **Descriptions tronquées** pour un affichage propre
@ -154,8 +163,21 @@ nano .env
# 3. Listez vos repositories disponibles
./run.sh --list
# 4. Lancez la migration
# 4. Lancez la migration interactive
./run.sh
```
### Exemple 6 : Migration avec renommage
```bash
# 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
# - Appuyer sur ENTRÉE pour garder le nom original
# - Taper un nouveau nom pour renommer
# 5. Confirmer et lancer la migration
```
## 📊 Résultats

@ -13,10 +13,11 @@ class InteractiveSelector:
"""Interactive repository selector with keyboard navigation"""
def __init__(self, repositories: List[Dict], username: str):
self.repositories = repositories
self.username = username
# Sort repositories: user's repos first, then others, both alphabetically
self.repositories = self._sort_repositories(repositories, username)
# Only select user's own repositories by default
self.selected = set(i for i, repo in enumerate(repositories)
self.selected = set(i for i, repo in enumerate(self.repositories)
if repo['owner']['login'] == username)
self.current_index = 0
self.page_size = 15 # Number of repos to show per page
@ -64,19 +65,30 @@ class InteractiveSelector:
print()
# Display repositories for current page
last_owner_type = None # Track if we're switching from user repos to others
for i in range(start_idx, end_idx):
repo = self.repositories[i]
is_selected = i in self.selected
is_current = i == self.current_index
# Check if we need to add a separator
owner = repo['owner']['login']
is_own_repo = owner == self.username
current_owner_type = "own" if is_own_repo else "others"
# Add separator when transitioning from own repos to others
if last_owner_type == "own" and current_owner_type == "others":
print(f" {Fore.LIGHTBLACK_EX}{'' * 50} Autres repositories {'' * 10}{Style.RESET_ALL}")
last_owner_type = current_owner_type
# Checkbox
checkbox = "☑️ " if is_selected else ""
# Repository info
owner = repo['owner']['login']
name = repo['name']
private = "🔒" if repo.get('private', False) else "🌐"
is_own_repo = owner == self.username
ownership_indicator = "👤" if is_own_repo else "👥"
description = repo.get('description', 'No description')[:45]
if len(repo.get('description', '')) > 45:
@ -155,6 +167,97 @@ class InteractiveSelector:
self.current_page += 1
self.current_index = self.current_page * self.page_size
def _rename_repositories_interface(self, selected_repos: List[Dict]) -> List[Dict]:
"""Interface for renaming selected repositories"""
print('\033[2J\033[H', end='') # Clear screen
print(f"{Fore.CYAN}╔═══════════════════════════════════════════════════════════════╗")
print(f"║ ✏️ RENAME REPOSITORIES ║")
print(f"║ ║")
print(f"║ Press ENTER to keep current name, or type new name ║")
print(f"║ Repository names should be valid GitHub repo names ║")
print(f"╚═══════════════════════════════════════════════════════════════╝{Style.RESET_ALL}")
print()
renamed_repos = []
for i, repo in enumerate(selected_repos, 1):
owner = repo['owner']['login']
original_name = repo['name']
private = "🔒" if repo.get('private', False) else "🌐"
print(f"{Fore.YELLOW}📦 Repository {i}/{len(selected_repos)}:{Style.RESET_ALL}")
print(f" Source: {Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL} {private}")
# Get new name from user
new_name = input(f" GitHub name [{Fore.GREEN}{original_name}{Style.RESET_ALL}]: ").strip()
# Validate and use new name
if new_name:
# Basic validation
if not self._is_valid_repo_name(new_name):
print(f" {Fore.RED}⚠️ Invalid repository name. Using original name: {original_name}{Style.RESET_ALL}")
new_name = original_name
else:
print(f" {Fore.GREEN}✅ Will rename to: {new_name}{Style.RESET_ALL}")
else:
new_name = original_name
print(f" {Fore.CYAN} Keeping original name: {original_name}{Style.RESET_ALL}")
# Create new repo dict with updated name
renamed_repo = repo.copy()
renamed_repo['github_name'] = new_name # Add field for GitHub name
renamed_repos.append(renamed_repo)
print()
# Summary
print(f"{Fore.GREEN}✅ Repository renaming complete!{Style.RESET_ALL}")
print(f"\n{Fore.CYAN}📋 Migration summary:{Style.RESET_ALL}")
for repo in renamed_repos:
owner = repo['owner']['login']
original_name = repo['name']
github_name = repo['github_name']
private = "🔒" if repo.get('private', False) else "🌐"
if original_name != github_name:
print(f"{Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL}{Fore.GREEN}{github_name}{Style.RESET_ALL} {private}")
else:
print(f"{Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL} {private}")
input(f"\n{Fore.YELLOW}Press ENTER to continue...{Style.RESET_ALL}")
return renamed_repos
def _sort_repositories(self, repositories: List[Dict], username: str) -> List[Dict]:
"""Sort repositories: user's repos first, then others, both alphabetically"""
def sort_key(repo):
owner = repo['owner']['login']
name = repo['name'].lower() # Case-insensitive sorting
is_user_repo = owner == username
# Return tuple: (is_not_user_repo, owner.lower(), name)
# This will sort user repos first (False < True), then alphabetically
return (not is_user_repo, owner.lower(), name)
return sorted(repositories, key=sort_key)
def _is_valid_repo_name(self, name: str) -> bool:
"""Validate GitHub repository name"""
if not name:
return False
# GitHub repo name rules (simplified)
if len(name) > 100:
return False
# Should not start or end with special characters
if name.startswith('.') or name.startswith('-') or name.endswith('.'):
return False
# Should contain only alphanumeric, hyphens, underscores, and dots
import re
return bool(re.match(r'^[a-zA-Z0-9._-]+$', name))
def run(self) -> List[Dict]:
"""Run interactive selection and return selected repositories"""
if not self.repositories:
@ -203,6 +306,16 @@ class InteractiveSelector:
private = "🔒" if repo.get('private', False) else "🌐"
print(f"{Fore.BLUE}{owner}/{name}{Style.RESET_ALL} {private}")
# Ask if user wants to rename repositories
print(f"\n{Fore.YELLOW}📝 Voulez-vous changer le nom de certains repos sur GitHub ?{Style.RESET_ALL}")
print(f"{Fore.CYAN}[Y/y] Oui - Interface de renommage{Style.RESET_ALL}")
print(f"{Fore.CYAN}[N/n ou ENTER] Non - Conserver les noms actuels{Style.RESET_ALL}")
choice = input(f"\n{Fore.YELLOW}Votre choix: {Style.RESET_ALL}").strip().lower()
if choice == 'y' or choice == 'yes' or choice == 'oui':
selected_repos = self._rename_repositories_interface(selected_repos)
print(f"\n{Fore.CYAN}🚀 Starting migration...{Style.RESET_ALL}\n")
return selected_repos

@ -55,9 +55,18 @@ class MigrationTool:
for repo in selected_repos:
repo_name = repo['name']
repo_owner = repo['owner']['login']
logger.info(f"Migrating repository: {repo_owner}/{repo_name}")
github_name = repo.get('github_name', repo_name) # Use renamed name if available
if github_name != repo_name:
logger.info(f"Migrating repository: {repo_owner}/{repo_name}{github_name}")
else:
logger.info(f"Migrating repository: {repo_owner}/{repo_name}")
success = self.migrate_repository(repo)
results[f"{repo_owner}/{repo_name}"] = success
display_name = f"{repo_owner}/{repo_name}"
if github_name != repo_name:
display_name += f"{github_name}"
results[display_name] = success
return results
@ -91,11 +100,12 @@ class MigrationTool:
"""Migrate a single repository"""
repo_name = repo_info['name']
repo_owner = repo_info['owner']['login']
github_name = repo_info.get('github_name', repo_name) # Use renamed name if available
try:
# Create GitHub repository
# Create GitHub repository with the (possibly renamed) name
success = self.github_client.create_repository(
repo_name=repo_name,
repo_name=github_name,
description=repo_info.get('description', ''),
private=repo_info.get('private', False)
)
@ -104,14 +114,17 @@ class MigrationTool:
return False
# Clone and push repository
return self._clone_and_push_repo(repo_owner, repo_name)
return self._clone_and_push_repo(repo_owner, repo_name, github_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:
def _clone_and_push_repo(self, repo_owner: str, repo_name: str, github_name: str = None) -> bool:
"""Clone repository from Gitea and push to GitHub"""
if github_name is None:
github_name = repo_name
temp_dir = None
original_cwd = os.getcwd() # Save original working directory
@ -138,7 +151,7 @@ class MigrationTool:
# Add GitHub remote (run command in the repository directory)
github_url = self.github_client.get_authenticated_clone_url(
repo_name,
github_name, # Use the GitHub name (possibly renamed)
self.config.github_token
)
@ -150,15 +163,31 @@ class MigrationTool:
return False
# Push to GitHub (run command in the repository directory)
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, cwd=str(repo_path))
if github_name != repo_name:
logger.info(f"Pushing repository to GitHub: {repo_name}{github_name}")
else:
logger.info(f"Pushing repository to GitHub: {repo_name}")
# Push branches first
push_branches_cmd = ['git', 'push', '--all', 'github']
result = subprocess.run(push_branches_cmd, capture_output=True, text=True, cwd=str(repo_path))
if result.returncode != 0:
logger.error(f"Failed to push to GitHub: {result.stderr}")
logger.error(f"Failed to push branches to GitHub: {result.stderr}")
return False
logger.info(f"Successfully migrated repository: {repo_name}")
# Push tags
push_tags_cmd = ['git', 'push', '--tags', 'github']
result = subprocess.run(push_tags_cmd, capture_output=True, text=True, cwd=str(repo_path))
if result.returncode != 0:
logger.warning(f"Failed to push tags to GitHub (this is often normal): {result.stderr}")
# Don't fail the migration if only tags fail
if github_name != repo_name:
logger.info(f"Successfully migrated repository: {repo_name}{github_name}")
else:
logger.info(f"Successfully migrated repository: {repo_name}")
return True
except Exception as e:
@ -179,6 +208,8 @@ class MigrationTool:
except Exception as e:
logger.warning(f"Failed to clean up temporary directory {temp_dir}: {e}")
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://', '')

Loading…
Cancel
Save