Migrar um repositório de forma incremental - AWS CodeCommit

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Migrar um repositório de forma incremental

Ao migrar para o AWS CodeCommit, considere enviar o repositório em incrementos ou fragmentos para reduzir a possibilidade de que um problema de rede intermitente ou desempenho de rede degradado cause uma falha em todo o envio. Ao usar pushes em incrementos com um script como o que está incluído aqui, você poderá reiniciar a migração e enviar por push somente as confirmações que não obtiveram êxito na tentativa anterior.

Os procedimentos neste tópico mostram como criar e executar um script que migra o repositório em incrementos e reenvia por push somente os incrementos que não obtiveram êxito até a migração estar concluída.

Essas instruções foram escritas presumindo que você já tenha concluído as etapas em Configuração e Criar um repositório.

Etapa 0: determinar se a migração será incremental

Há vários fatores a considerar para determinar o tamanho total do seu repositório e se migrará de maneira incremental. O mais óbvio é o tamanho total dos artefatos no repositório. Fatores como histórico acumulado do repositório também podem contribuir para o tamanho. Um repositório com anos de histórico e ramificações pode ser muito grande, mesmo que os recursos individuais não sejam. Há uma série de estratégias que você pode utilizar para tornar a migração desses repositórios mais simples e mais eficiente. Por exemplo, você pode usar uma estratégia de clonagem superficial ao clonar um repositório com um longo histórico de desenvolvimento, ou pode desativar a compactação delta para grandes arquivos binários. Você pode pesquisar opções consultando a documentação do Git ou optar pela configuração de push em incrementos para migrar o seu repositório usando a amostra de script incluída neste tópico, incremental-repo-migration.py

Você pode configurar pushes em incrementos se uma ou mais das seguintes condições forem verdadeiras:

  • O repositório que você quer migrar tem um histórico de mais de cinco anos.

  • Sua conexão com a Internet está sujeita a interrupções intermitentes, queda de pacotes, resposta lenta ou outras interrupções no serviço.

  • O tamanho total do repositório é maior que 2 GB e você pretende migrá-lo por inteiro.

  • O repositório contém grandes artefatos ou binários que não são bem compactados, como arquivos grandes de imagem de mais de cinco versões controladas.

  • Você já tentou migrar para o CodeCommit antes e recebeu uma mensagem de "Erro de serviço interno".

Mesmo se nenhuma das condições acima for verdadeira, você ainda poderá escolher enviar pushes de maneira incremental.

Etapa 1: instalar pré-requisitos e adicionar o repositório do CodeCommit como remoto

É possível criar o seu próprio script personalizado que tenha pré-requisitos próprios. Se você usar os exemplos incluídos neste tópico, deverá:

  • Instalar seus pré-requisitos.

  • Clonar o repositório para o computador local.

  • Adicionar o repositório do CodeCommit como remoto para o repositório que deseja migrar.

Configuração para executar incremental-repo-migration.py
  1. No computador local, instale o Python 2.6 ou posterior. Para obter mais informações sobre as versões mais recentes, consulte o site da Python.

  2. No mesmo computador, instale GitPython, uma biblioteca Python usada para interagir com repositórios do Git. Para obter mais informações, consulte a documentação do GitPython.

  3. Use o comando git clone --mirror para clonar o repositório que você deseja migrar para o seu computador local. No terminal (Linux, macOS ou Unix) ou no prompt de comando (Windows), use o comando git clone --mirror para criar um repositório local para o repositório, incluindo o diretório onde você deseja criá-lo. Por exemplo, para clonar um repositório do Git chamado MyMigrationRepo com o URL de https://example.com/my-repo/ para um diretório chamado my-repo:

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

    Você verá uma saída semelhante à seguinte, que indica que o repositório foi clonado em um repositório local bare chamado 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. Altere os diretórios para o repositório local do repositório que você acabou de clonar (por exemplo, my-repo). Nesse diretório, use o comando git remote add DefaultRemoteName RemoteRepositoryURL para adicionar o repositório do CodeCommit como um repositório remoto para o repositório local.

    nota

    Ao enviar grandes repositórios por push, considere utilizar SSH em vez de HTTPS. Ao enviar por push uma grande alteração, um grande número de alterações ou um grande repositório, as conexões HTTPS de execução prolongada costumam ser encerradas prematuramente devido a problemas de rede ou configurações do firewall. Para obter mais informações sobre a configuração do CodeCommit para SSH, consulte Para conexões SSH no Linux, macOS ou Unix ou Para conexões SSH no Windows.

    Por exemplo, use o seguinte comando para adicionar o endpoint SSH em um repositório do CodeCommit denominado MyDestinationRepo como um repositório remoto para o remoto denominado codecommit:

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

    Por ser um clone, o nome do repositório remoto padrão (origin) já está em uso. Você deverá usar outro nome de repositório remoto. Embora o exemplo use codecommit, você pode usar o nome que quiser. Use o comando git remote show para analisar a lista de conjunto de repositórios remotos do seu repositório local.

  5. Use o comando git remote -v para exibir as configurações de busca e push do seu repositório local e confirmar se estão corretas. Por exemplo:

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

    Se você ainda vir entradas de busca e push de um repositório remoto diferente (por exemplo, entradas para origin), use o comando git remote set-url --delete para removê-las.

Etapa 2: criar o script a ser usado na migração em incrementos

Estas etapas foram escritas presumindo que você esteja usando o script de exemplo incremental-repo-migration.py.

  1. Abra um editor de texto e cole os conteúdos da amostra de script em um documento vazio.

  2. Salve o documento em um diretório de documentos (não o diretório de trabalho do seu repositório local) e nomeie-o incremental-repo-migration.py. Verifique se o diretório escolhido está configurado no ambiente local ou nas variáveis PATH, para que seja possível executar o script Python a partir de uma linha de comando ou um terminal.

Etapa 3: executar o script e migrar de forma incremental para o CodeCommit

Agora que você criou o seu script incremental-repo-migration.py, poderá usá-lo para migrar um repositório local para um repositório do CodeCommit de forma incremental. Por padrão, o script envia confirmações por push em lotes de 1.000 confirmações e tenta usar as configurações do Git para o diretório de onde é executado como configurações para os repositórios local e remoto. Você pode usar as opções incluídas em incremental-repo-migration.py para definir outras configurações, se necessário.

  1. No terminal ou na linha de comando, altere os diretórios para o repositório local que você deseja migrar.

  2. Nesse diretório, execute o seguinte comando:

    python incremental-repo-migration.py
  3. O script executa e mostra progresso no terminal ou na linha de comando. Alguns repositórios grandes demoram para mostrar o progresso. O script será interrompido se um único push falhar três vezes. Então, você poderá executar novamente o script, que começará pelo lote que falhou. Execute o script novamente até que todos os pushes obtenham êxito e a migração seja concluída.

dica

Você pode executar incremental-repo-migration.py a partir de qualquer diretório desde que utilize as opções -l e -r para especificar as configurações locais e remotas a serem usadas. Por exemplo, para usar o script de qualquer diretório para migrar um repositório local localizado em /tmp/my-repo para um remoto apelidado de codecommit:

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

Você também pode querer usar a opção -b para alterar o tamanho do lote padrão usado ao enviar por push de forma incremental. Por exemplo, se você estiver enviando por push regularmente um repositório de arquivos binários muito grandes que costumam mudar frequentemente e trabalham de um local com largura de banda da rede restrita, use a opção -b para alterar o tamanho do lote para 500 em vez de 1.000. Por exemplo:

python incremental-repo-migration.py -b 500

Isso envia o repositório local por push de maneira incremental em lotes de 500 confirmações. Se você decidir alterar o tamanho do lote novamente ao migrar o repositório (por exemplo, se você decidir reduzir o tamanho do lote após uma tentativa falha), lembre-se de usar a opção -c para remover as tags do lote antes de redefinir o tamanho do lote com -b:

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

Não use a opção -c se desejar executar novamente o script após uma falha. A opção -c elimina as tags usadas para acomodar as confirmações em lote. Use a opção -c somente se desejar alterar o tamanho do lote e iniciar novamente ou se decidir que não quer mais usar o script.

Apêndice: Amostra de script incremental-repo-migration.py

Para a sua conveniência, desenvolvemos um amostra de script Python, incremental-repo-migration.py, para enviar um repositório por push de maneira incremental. Esse script é um exemplo de código aberto e é fornecido como se encontra.

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