Migrer un référentiel de manière incrémentielle - AWS CodeCommit

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Migrer un référentiel de manière incrémentielle

Lors de la migration vers AWS CodeCommit, vous pouvez choisir de transmettre votre référentiel par incréments ou fragments pour limiter les risques qu'un problème d'interruption de connexion réseau ou de performances réseau dégradées entraîne l'échec de la totalité de la transmission. En utilisant des transmissions incrémentielles avec un script semblable à celui qui est inclus ici, vous pouvez redémarrer la migration et ne transmettre que les validations qui n'ont pas abouti lors de la tentative précédente.

Les procédures décrites dans cette rubrique vous montrent comment créer et exécuter un script qui migre votre référentiel par incréments, et ne retransmet que les incréments qui n'ont pas abouti jusqu'à ce que la migration soit terminée.

Ces instructions supposent que vous avez déjà effectué les étapes indiquées dans Configuration et Création d'un référentiel .

Étape 0 : Déterminer s'il faut effectuer une migration incrémentielle

Vous devez prendre en compte plusieurs facteurs pour déterminer la taille globale de votre référentiel et savoir si une migration incrémentielle est nécessaire. Le facteur le plus évident est la taille des artefacts du référentiel. Des facteurs tels que l'historique accumulé du référentiel peuvent également contribuer à sa taille. Un référentiel avec des années d'historique et de branches peut être très volumineux, même si les ressources individuelles ne le sont pas. Il existe un certain nombre de stratégies que vous pouvez mettre en place pour simplifier la migration de ces référentiels et la rendre plus efficace. Par exemple, vous pouvez utiliser une stratégie de clone superficiel lors du clonage d'un référentiel avec un grand historique de développement, ou vous pouvez désactiver la compression delta pour les fichiers binaires volumineux. Vous pouvez rechercher des options en consultant votre documentation Git, ou choisir de configurer des transmissions incrémentielles pour la migration de votre référentiel à l'aide de l'exemple de script inclus dans cette rubrique : incremental-repo-migration.py.

Vous souhaiterez peut-être configurer des transmissions incrémentielles si une ou plusieurs des conditions suivantes sont réunies :

  • Le référentiel que vous souhaitez migrer a plus de cinq ans d'historique.

  • Votre connexion internet est soumise à des pannes intermittentes, des paquets abandonnés, une réponse lente ou d'autres interruptions de service.

  • La taille globale du référentiel est supérieure à 2 Go et vous avez l'intention de migrer la totalité du référentiel.

  • Le référentiel contient des artefacts volumineux ou des fichiers binaires qui ne se compressent pas correctement, par exemple de gros fichiers image avec plus de cinq versions suivies.

  • Vous avez déjà tenté une migration vers CodeCommit et reçu un message « Erreur de service interne ».

Même si aucune des conditions ci-dessus n'est vraie, vous pouvez quand-même choisir d'effectuer une transmission incrémentielle.

Étape 1 : installer les prérequis et ajouter le CodeCommit référentiel en tant que télécommande

Vous pouvez créer votre propre script personnalisé qui a ses propres prérequis. Si vous utilisez l'exemple de cette rubrique, vous devez :

  • Installer les prérequis.

  • Cloner le référentiel sur votre ordinateur local.

  • Ajoutez le CodeCommit référentiel en tant que télécommande pour le référentiel que vous souhaitez migrer.

Configurer pour exécuter incremental-repo-migration .py
  1. Sur votre ordinateur local, installer Python 2.6 ou une version ultérieure. Pour plus d'informations et obtenir les dernières versions, consultez le site Web Python.

  2. Sur le même ordinateur, installez GitPython, qui est une bibliothèque Python utilisée pour interagir avec les référentiels Git. Pour plus d'informations, consultez la documentation GitPython.

  3. Utilisez la commande git clone --mirror pour cloner le référentiel que vous souhaitez migrer vers votre ordinateur local. À partir du terminal (Linux, macOS ou Unix) ou de l'invite de commande (Windows), utilisez la git clone --mirror commande pour créer un dépôt local pour le référentiel, y compris le répertoire dans lequel vous souhaitez créer le dépôt local. Par exemple, pour cloner un dépôt Git nommé MyMigrationRepoavec l'URL https://example.com/my-repo/ vers un répertoire nommé my-repo :

    git clone --mirror https://example.com/my-repo/MyMigrationRepo.git my-repo

    Vous devez voir une sortie similaire à ce qui suit, ce qui indique que le référentiel a été cloné en un référentiel local nu nommé my-repo :

    Cloning into bare repository 'my-repo'... remote: Counting objects: 20, done. remote: Compressing objects: 100% (17/17), done. remote: Total 20 (delta 5), reused 15 (delta 3) Unpacking objects: 100% (20/20), done. Checking connectivity... done.
  4. Placez-vous dans le répertoire vers le référentiel local pour le référentiel que vous venez de cloner (par exemple, my-repo). A partir de ce répertoire, utilisez la commande git remote add DefaultRemoteName RemoteRepositoryURL pour ajouter le référentiel CodeCommit en tant que référentiel distant pour le référentiel local.

    Note

    Lorsque vous transmettez des référentiels volumineux, il est préférable d'utiliser SSH plutôt que HTTPS. Lorsque vous transmettez une modification de grande taille, un grand nombre de modifications ou un référentiel volumineux, les connexions HTTPS de longue durée prennent souvent fin prématurément en raison de problèmes réseau ou de paramètres du pare-feu. Pour plus d'informations sur la configuration CodeCommit de SSH, consultez Pour les connexions SSH sous Linux, macOS ou Unix ouPour des connexions SSH sous Windows.

    Par exemple, utilisez la commande suivante pour ajouter le point de terminaison SSH pour un CodeCommit référentiel MyDestinationRepo nommé référentiel distant pour la télécommande nommée codecommit :

    git remote add codecommit ssh://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDestinationRepo
    Astuce

    Comme il s'agit d'un clone, le nom distant par défaut (origin) est déjà utilisé. Vous devez utiliser un autre nom distant. Même si l'exemple utilise codecommit, vous pouvez vous servir du nom de votre choix. Utilisez la commande git remote show pour consulter la liste des référentiels distants définis pour votre référentiel local.

  5. Utilisez la commande git remote -v pour afficher les paramètres d'extraction (fetch) et de transmission (push) pour votre référentiel local, et vérifier qu'ils sont correctement définis. Par exemple :

    codecommit ssh://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDestinationRepo (fetch) codecommit ssh://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDestinationRepo (push)
    Astuce

    Si vous voyez toujours des entrées fetch et push pour un autre référentiel distant (par exemple, des entrées pour origin), supprimez-les à l'aide de la commande git remote set-url --delete.

Étape 2 : Création du script à utiliser pour la migration incrémentielle

Ces étapes supposent que vous utilisez l'exemple de script incremental-repo-migration.py.

  1. Ouvrez un éditeur de texte et collez le contenu de l'exemple de script dans un document vide.

  2. Enregistrez le document dans un répertoire de documents (pas le répertoire de travail de votre référentiel local) et nommez-le incremental-repo-migration.py. Assurez-vous que le répertoire que vous choisissez est configuré dans vos variables d'environnement local ou path pour pouvoir exécuter le script Python à partir d'une ligne de commande ou d'un terminal.

Étape 3 : Exécuter le script et migrer progressivement vers CodeCommit

Maintenant que vous avez créé votre incremental-repo-migration.py script, vous pouvez l'utiliser pour migrer progressivement un dépôt local vers un CodeCommit référentiel. Par défaut, le script transmet des validations par lots de 1 000 validations, et tente d'utiliser les paramètres Git pour le répertoire à partir duquel il est exécuté comme paramètres pour le référentiel local et le référentiel distant. Vous pouvez utiliser les options incluses dans incremental-repo-migration.py pour configurer d'autres paramètres, si nécessaire.

  1. Depuis le terminal ou l'invite de commande, placez-vous dans le répertoire vers le référentiel local que vous souhaitez migrer.

  2. Depuis ce répertoire, exécutez la commande suivante :

    python incremental-repo-migration.py
  3. Le script s'exécute et affiche sa progression sur le terminal ou dans l'invite de commande. L'affichage de la progression est lent pour certains référentiels volumineux. Le script s'arrête si une seule transmission échoue trois fois. Vous pouvez alors relancer le script qui commence à partir du lot ayant échoué. Vous pouvez relancer le script jusqu'à ce que toutes les transmissions réussissent et que la migration soit terminée.

Astuce

Vous pouvez exécuter incremental-repo-migration.py à partir de n'importe quel répertoire dans la mesure où vous utilisez les options -l et -r pour spécifier les paramètres locaux et distants à utiliser. Par exemple, pour utiliser le script à partir de n'importe quel répertoire afin de migrer un référentiel local situé dans /tmp/my-repo vers un référentiel distant dont le pseudonyme est codecommit :

python incremental-repo-migration.py -l "/tmp/my-repo" -r "codecommit"

Vous pouvez également utiliser l'option -b pour modifier la taille de lot par défaut utilisée lors d'une transmission incrémentielle. Par exemple, si vous transmettez régulièrement un référentiel avec de très gros fichiers binaires qui changent souvent et que vous travaillez dans un endroit où la bande passante réseau est limitée, vous pouvez utiliser l'option -b pour modifier la taille de lot à 500 au lieu de 1 000. Par exemple :

python incremental-repo-migration.py -b 500

Cela transmet le référentiel local de façon incrémentielle par lots de 500 validations. Si vous décidez de modifier à nouveau la taille de lot lors de la migration du référentiel (par exemple, si vous décidez de diminuer la taille de lot après une tentative infructueuse), pensez à utiliser l'option -c pour supprimer les balises de traitement par lots avant de réinitialiser la taille de lot avec -b :

python incremental-repo-migration.py -c python incremental-repo-migration.py -b 250
Important

N'utilisez pas l'option -c pour relancer le script après un échec. L'option -c supprime les balises utilisées pour regrouper les validations par lots. Utilisez l'option -c uniquement si vous voulez modifier la taille de lot et recommencer, ou si vous décidez que vous n'avez plus besoin d'utiliser le script.

Annexe : Exemple de script incremental-repo-migration.py

Pour plus de commodité, nous avons développé un exemple de script Python, incremental-repo-migration.py, pour transmettre un référentiel de façon incrémentielle. Ce script est un exemple de code open source fourni en l'état.

# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Amazon Software License (the "License"). # You may not use this file except in compliance with the License. A copy of the License is located at # http://aws.amazon.com/asl/ # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for # the specific language governing permissions and limitations under the License. #!/usr/bin/env python import os import sys from optparse import OptionParser from git import Repo, TagReference, RemoteProgress, GitCommandError class PushProgressPrinter(RemoteProgress): def update(self, op_code, cur_count, max_count=None, message=""): op_id = op_code & self.OP_MASK stage_id = op_code & self.STAGE_MASK if op_id == self.WRITING and stage_id == self.BEGIN: print("\tObjects: %d" % max_count) class RepositoryMigration: MAX_COMMITS_TOLERANCE_PERCENT = 0.05 PUSH_RETRY_LIMIT = 3 MIGRATION_TAG_PREFIX = "codecommit_migration_" def migrate_repository_in_parts( self, repo_dir, remote_name, commit_batch_size, clean ): self.next_tag_number = 0 self.migration_tags = [] self.walked_commits = set() self.local_repo = Repo(repo_dir) self.remote_name = remote_name self.max_commits_per_push = commit_batch_size self.max_commits_tolerance = ( self.max_commits_per_push * self.MAX_COMMITS_TOLERANCE_PERCENT ) try: self.remote_repo = self.local_repo.remote(remote_name) self.get_remote_migration_tags() except (ValueError, GitCommandError): print( "Could not contact the remote repository. The most common reasons for this error are that the name of the remote repository is incorrect, or that you do not have permissions to interact with that remote repository." ) sys.exit(1) if clean: self.clean_up(clean_up_remote=True) return self.clean_up() print("Analyzing repository") head_commit = self.local_repo.head.commit sys.setrecursionlimit(max(sys.getrecursionlimit(), head_commit.count())) # tag commits on default branch leftover_commits = self.migrate_commit(head_commit) self.tag_commits([commit for (commit, commit_count) in leftover_commits]) # tag commits on each branch for branch in self.local_repo.heads: leftover_commits = self.migrate_commit(branch.commit) self.tag_commits([commit for (commit, commit_count) in leftover_commits]) # push the tags self.push_migration_tags() # push all branch references for branch in self.local_repo.heads: print("Pushing branch %s" % branch.name) self.do_push_with_retries(ref=branch.name) # push all tags print("Pushing tags") self.do_push_with_retries(push_tags=True) self.get_remote_migration_tags() self.clean_up(clean_up_remote=True) print("Migration to CodeCommit was successful") def migrate_commit(self, commit): if commit in self.walked_commits: return [] pending_ancestor_pushes = [] commit_count = 1 if len(commit.parents) > 1: # This is a merge commit # Ensure that all parents are pushed first for parent_commit in commit.parents: pending_ancestor_pushes.extend(self.migrate_commit(parent_commit)) elif len(commit.parents) == 1: # Split linear history into individual pushes next_ancestor, commits_to_next_ancestor = self.find_next_ancestor_for_push( commit.parents[0] ) commit_count += commits_to_next_ancestor pending_ancestor_pushes.extend(self.migrate_commit(next_ancestor)) self.walked_commits.add(commit) return self.stage_push(commit, commit_count, pending_ancestor_pushes) def find_next_ancestor_for_push(self, commit): commit_count = 0 # Traverse linear history until we reach our commit limit, a merge commit, or an initial commit while ( len(commit.parents) == 1 and commit_count < self.max_commits_per_push and commit not in self.walked_commits ): commit_count += 1 self.walked_commits.add(commit) commit = commit.parents[0] return commit, commit_count def stage_push(self, commit, commit_count, pending_ancestor_pushes): # Determine whether we can roll up pending ancestor pushes into this push combined_commit_count = commit_count + sum( ancestor_commit_count for (ancestor, ancestor_commit_count) in pending_ancestor_pushes ) if combined_commit_count < self.max_commits_per_push: # don't push anything, roll up all pending ancestor pushes into this pending push return [(commit, combined_commit_count)] if combined_commit_count <= ( self.max_commits_per_push + self.max_commits_tolerance ): # roll up everything into this commit and push self.tag_commits([commit]) return [] if commit_count >= self.max_commits_per_push: # need to push each pending ancestor and this commit self.tag_commits( [ ancestor for (ancestor, ancestor_commit_count) in pending_ancestor_pushes ] ) self.tag_commits([commit]) return [] # push each pending ancestor, but roll up this commit self.tag_commits( [ancestor for (ancestor, ancestor_commit_count) in pending_ancestor_pushes] ) return [(commit, commit_count)] def tag_commits(self, commits): for commit in commits: self.next_tag_number += 1 tag_name = self.MIGRATION_TAG_PREFIX + str(self.next_tag_number) if tag_name not in self.remote_migration_tags: tag = self.local_repo.create_tag(tag_name, ref=commit) self.migration_tags.append(tag) elif self.remote_migration_tags[tag_name] != str(commit): print( "Migration tags on the remote do not match the local tags. Most likely your batch size has changed since the last time you ran this script. Please run this script with the --clean option, and try again." ) sys.exit(1) def push_migration_tags(self): print("Will attempt to push %d tags" % len(self.migration_tags)) self.migration_tags.sort( key=lambda tag: int(tag.name.replace(self.MIGRATION_TAG_PREFIX, "")) ) for tag in self.migration_tags: print( "Pushing tag %s (out of %d tags), commit %s" % (tag.name, self.next_tag_number, str(tag.commit)) ) self.do_push_with_retries(ref=tag.name) def do_push_with_retries(self, ref=None, push_tags=False): for i in range(0, self.PUSH_RETRY_LIMIT): if i == 0: progress_printer = PushProgressPrinter() else: progress_printer = None try: if push_tags: infos = self.remote_repo.push(tags=True, progress=progress_printer) elif ref is not None: infos = self.remote_repo.push( refspec=ref, progress=progress_printer ) else: infos = self.remote_repo.push(progress=progress_printer) success = True if len(infos) == 0: success = False else: for info in infos: if ( info.flags & info.UP_TO_DATE or info.flags & info.NEW_TAG or info.flags & info.NEW_HEAD ): continue success = False print(info.summary) if success: return except GitCommandError as err: print(err) if push_tags: print("Pushing all tags failed after %d attempts" % (self.PUSH_RETRY_LIMIT)) elif ref is not None: print("Pushing %s failed after %d attempts" % (ref, self.PUSH_RETRY_LIMIT)) print( "For more information about the cause of this error, run the following command from the local repo: 'git push %s %s'" % (self.remote_name, ref) ) else: print( "Pushing all branches failed after %d attempts" % (self.PUSH_RETRY_LIMIT) ) sys.exit(1) def get_remote_migration_tags(self): remote_tags_output = self.local_repo.git.ls_remote( self.remote_name, tags=True ).split("\n") self.remote_migration_tags = dict( (tag.split()[1].replace("refs/tags/", ""), tag.split()[0]) for tag in remote_tags_output if self.MIGRATION_TAG_PREFIX in tag ) def clean_up(self, clean_up_remote=False): tags = [ tag for tag in self.local_repo.tags if tag.name.startswith(self.MIGRATION_TAG_PREFIX) ] # delete the local tags TagReference.delete(self.local_repo, *tags) # delete the remote tags if clean_up_remote: tags_to_delete = [":" + tag_name for tag_name in self.remote_migration_tags] self.remote_repo.push(refspec=tags_to_delete) parser = OptionParser() parser.add_option( "-l", "--local", action="store", dest="localrepo", default=os.getcwd(), help="The path to the local repo. If this option is not specified, the script will attempt to use current directory by default. If it is not a local git repo, the script will fail.", ) parser.add_option( "-r", "--remote", action="store", dest="remoterepo", default="codecommit", help="The name of the remote repository to be used as the push or migration destination. The remote must already be set in the local repo ('git remote add ...'). If this option is not specified, the script will use 'codecommit' by default.", ) parser.add_option( "-b", "--batch", action="store", dest="batchsize", default="1000", help="Specifies the commit batch size for pushes. If not explicitly set, the default is 1,000 commits.", ) parser.add_option( "-c", "--clean", action="store_true", dest="clean", default=False, help="Remove the temporary tags created by migration from both the local repo and the remote repository. This option will not do any migration work, just cleanup. Cleanup is done automatically at the end of a successful migration, but not after a failure so that when you re-run the script, the tags from the prior run can be used to identify commit batches that were not pushed successfully.", ) (options, args) = parser.parse_args() migration = RepositoryMigration() migration.migrate_repository_in_parts( options.localrepo, options.remoterepo, int(options.batchsize), options.clean )