Migrar de forma incremental un repositorio - AWS CodeCommit

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Migrar de forma incremental un repositorio

A la hora de migrar a AWS CodeCommit, considere la posibilidad de enviar el repositorio en incrementos o en fragmentos para reducir las posibilidades de error intermitente de la red o de bajo rendimiento de la red, lo que impide enviar el repositorio completo correctamente. Al realizar envíos incrementales con un script como el incluido aquí, puede reiniciar la migración y enviar únicamente aquellas confirmaciones que no se hayan podido enviar en el primer intento.

Los procedimientos incluidos en este tema explican cómo crear y ejecutar un script que migre su repositorio de forma incremental y envíe de nuevo solo aquellos fragmentos que no se han podido enviar hasta que se complete toda la migración.

Estas instrucciones se han redactado presuponiendo que ya ha completado los pasos indicados en Configuración y Creación de un repositorio.

Paso 0: Determinar si realizar una migración de forma incremental

A la hora de determinar el tamaño total de su repositorio y decidir si migrar de forma incremental, debe considerar determinados factores. El factor más evidente es el tamaño total de los elementos del repositorio. Los factores, como el historial acumulado del repositorio, también pueden contribuir al tamaño. Un repositorio con años de historia y ramificaciones puede ser muy grande, aunque sus elementos individuales no lo sean. Puede utilizar distintas estrategias para simplificar la migración de estos repositorios y que sea más eficaz. Por ejemplo, puede utilizar una estrategia de clonación superficial para clonar un repositorio con un largo historial de desarrollo o puede desactivar la compresión delta para archivos binarios grandes. Puede buscar otras opciones en la documentación de Git o puede optar por establecer y configurar envíos incrementales para migrar el repositorio utilizando el script de muestra incluido en este tema incremental-repo-migration.py.

Es posible que le interese configurar envíos incrementales en caso de que se cumpla una o más de las siguientes condiciones:

  • El repositorio que desea migrar tiene más de cinco años de historia.

  • Su conexión a Internet está sometida a interrupciones intermitentes, pérdidas de paquetes, respuestas lentas o cualquier otra interrupción del servicio.

  • El tamaño total del repositorio es superior a 2 GB e intenta migrar todo el repositorio.

  • El repositorio contiene elementos grandes o binarios que no se comprimen bien, como archivos de imágenes grandes con más de cinco versiones controladas.

  • Ha intentado una migración previa a CodeCommit y recibido el mensaje "Error de servicio interno".

Aunque no se cumpla ninguna de las condiciones anteriores, también puede optar por enviar de forma incremental.

Paso 1: Instalar los requisitos previos y añadir el repositorio de CodeCommit como remoto

Puede crear su propio script personalizado con sus propios requisitos previos. Si utiliza el ejemplo incluido en este tema, debe:

  • Instalar los requisitos previos.

  • Clonar el repositorio en su equipo local.

  • Añada el repositorio de CodeCommit como remoto para el repositorio que desea migrar.

Configuración para ejecutar incremental-repo-migration.py
  1. En el equipo local, instale Python 2.6 o posterior. Para obtener más información y las versiones más recientes, consulte el sitio web de Python.

  2. En el mismo equipo, instale GitPython, que es una biblioteca de Python que se utiliza para interactuar con los repositorios Git. Para obtener más información, consulte la documentación de GitPython.

  3. Utilice el comando git clone --mirror para clonar el repositorio que desea migrar a su equipo local. Desde el terminal (Linux, macOS o Unix) o el símbolo del sistema (Windows), utilice el comando git clone --mirror para crear un repositorio local para el repositorio, incluido el directorio en el que desea crear el repositorio local. Por ejemplo, para clonar un repositorio Git denominado MyMigrationRepo con una URL de https://example.com/my-repo/ en un directorio denominado my-repo:

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

    El resultado debería ser similar al siguiente, que indica que el repositorio se ha clonado en un repositorio local vacío denominado 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. Cambie los directorios del repositorio local por el repositorio que acaba de clonar (por ejemplo, my-repo). En ese directorio, utilice el comando git remote add DefaultRemoteName RemoteRepositoryURL para añadir el repositorio de CodeCommit como repositorio remoto del repositorio local.

    nota

    Si envía repositorios de gran tamaño, considere la posibilidad de utilizar SSH en lugar de HTTPS. Si envía un cambio grande, un gran número de cambios o un repositorio grande, a menudo las conexiones HTTPS de ejecución prolongada suelen interrumpirse de forma prematura debido a problemas de red o de configuración del cortafuegos. Para obtener más información sobre cómo configurar CodeCommit para SSH, consulte Para conexiones SSH en Linux, macOS o Unix o Para conexiones SSH en Windows.

    Por ejemplo, utilice el comando siguiente para añadir el punto de conexión SSH para un repositorio de CodeCommit denominado codecommit como repositorio remoto para el designado como remoto:

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

    Dado que se trata de un clon, el nombre remoto predeterminado (origin) ya está en uso. Deberá utilizar otro nombre remoto. Aunque el ejemplo utiliza codecommit, puede usar el nombre que desee. Utilice el comando git remote show para revisar la lista de remotos configurados para su repositorio local.

  5. Utilice el comando git remote -v para mostrar la búsqueda y el envío de la configuración de su repositorio local y confirmar que están configurados correctamente. Por ejemplo:

    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)
    sugerencia

    Si siguen apareciendo entradas de búsqueda y envío para otro repositorio remoto (por ejemplo, entradas de origen), utilice el comando git remote set-url --delete para eliminarlas.

Paso 2: Crear el script para migrar de forma incremental

Estos pasos se han escrito partiendo del supuesto de que está utilizando el script de ejemplo incremental-repo-migration.py.

  1. Abra un editor de texto y pegue el contenido del script de muestra en un documento vacío.

  2. Guarde el documento en un directorio de documentos (no el directorio de trabajo de su repositorio local) y nómbrelo incremental-repo-migration.py. Asegúrese de que el directorio que elija esté configurado en sus variables del entorno local o de la ruta, para poder ejecutar el script de Python desde una línea de comandos o terminal.

Paso 3: Ejecutar el script y migrar de forma incremental a CodeCommit

Ahora que ha creado el script incremental-repo-migration.py, puede utilizarlo para migrar un repositorio local de forma incremental a un repositorio de CodeCommit. De forma predeterminada, el script envía las confirmaciones en lotes de 1 000 e intenta utilizar la configuración de Git para el directorio desde el que se ejecuta como la configuración del repositorio local y del repositorio remoto. Puede utilizar las opciones incluidas en incremental-repo-migration.py para configurar otros ajustes, si es necesario.

  1. Desde el terminal o la línea de comandos, cambie los directorios al repositorio local al que desea migrar.

  2. Desde el directorio ejecute el siguiente comando:

    python incremental-repo-migration.py
  3. El script se ejecuta y muestra el progreso en el terminal o en la línea de comandos. El progreso de algunos repositorios grandes es más lento. El script se detiene si intenta enviarlo sin éxito tres veces seguidas. A continuación, podrá volver a ejecutar el script que empieza de nuevo desde el lote que ha devuelto un error. Podrá volver a ejecutar el script hasta que todo se envíe correctamente y se haya completado la migración.

sugerencia

Puede ejecutar incremental-repo-migration.py desde cualquier directorio siempre y cuando utilice las opciones -l y -r para especificar la configuración local y remota. Por ejemplo, para utilizar el script desde cualquier directorio para migrar un repositorio local ubicado en /tmp/my-repo a un codecommit designado como remoto:

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

También puede utilizar la opción -b para cambiar el tamaño del lote predeterminado utilizado para los envíos incrementales. Por ejemplo, si envía regularmente un repositorio con archivos binarios muy grandes que cambian con frecuencia y trabaja desde una ubicación con un ancho de banda de red limitado, le recomendamos que utilice la opción -b para cambiar el tamaño de 1 000 a 500. Por ejemplo:

python incremental-repo-migration.py -b 500

De este modo, el repositorio local envía de forma incremental en lotes de 500 confirmaciones. Si decide cambiar el tamaño del lote de nuevo al migrar el repositorio (por ejemplo, si decide reducir el tamaño del lote después de un intento erróneo), recuerde utilizar la opción -c para eliminar las etiquetas del lote antes de restablecer el tamaño del lote con -b:

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

No utilice la opción -c si desea volver a ejecutar el script después de un intento erróneo. La opción -c elimina las etiquetas que se utilizan para crear los lotes de confirmaciones. Utilice la opción -c solo si desea cambiar el tamaño del lote y empezar de nuevo, o si decide que ya no quiere utilizar el script.

Anexo: script de muestra incremental-repo-migration.py

Para su comodidad, hemos desarrollado un script de Python de muestra, incremental-repo-migration.py, para enviar un repositorio de forma incremental. Este script es un ejemplo de código abierto y se proporciona tal cual.

# 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 )