Olá,

semana passada o Github disponibilizou de graça acesso a repositórios privados com quantidade ilimitada de colaboradores. Além disso, para os usuários que precisarem de recursos mais avançados, o valor por usuário caiu de U$ 9.00 para U$ 4.00 por mês.

Na empresa, somos usuários do Bitbucket há pelo menos 5 anos, principalmente pela facilidade da integração com outros softwares da Atlassian que usamos. Mas algumas coisas incomodavam nele, como não ter suporte a commits assinados, ou não ter syntax highlight no diff dos commits e dos pull requests.

Com a pandemia dando uma folga na demanda e com esse empurrão do Github, resolvemos migrar os projetos do Bitbucket para o Github. O problema é que depois de anos usando o mesmo serviço, com centenas de repositórios criados e instalados, o trabalho de migrar tudo de um lugar para outro é gigantesco.

Sabendo que tanto o Bitbucket quanto o Github fornecem APIs nos seus serviços, permitindo interagir com os repositórios, resolvi automatizar o trabalho.

Clonando um repositório

Para migrar um repositório de um serviço para outro com todo o histórico de commits, branches, tags e notes, primeiro é preciso clonar ele com o parametro --mirror. Por exemplo, o comando:

git clone --mirror git@bitbucket.org:begnini/example.git

irá clonar o repositório begnini/example.git com todos os metadados dele, replicando o seu estado atual.

Depois de clonado, é necessário criar o repositório novo no github (via interface web, por exemplo), configurando as informações necessárias (nome, descrição, privado/publico, etc.).

Feito isso, é possível enviar o repositório para o Github, com o seguinte comando, executado dentro do diretório do repositório:

git push --mirror git@github.com:begnini/example.git

Com isso temos uma cópia fiel do repositório que estava no Bitbucket agora armazenada no Github.

Lembrando que para esses comandos funcionarem, é necessário ter adicionado uma chave publica ssh para o usuário.

Migrando todos repositórios

No meu caso, como tenho mais de 250 repositórios armazenados no Bibucket, seguir esse passo a passo para cada repositório demoraria dias, por isso resolvi automatizar o processo.

Para fazer o trabalho sujo do git, irei utilizar a biblioteca GitPython. Ela abstrai os comandos git em python e permite saber se tudo deu certo com a execução.

Abaixo como executar os comandos descritos acima, usando python:

1. Clonando um repositório

1
2
3
4
5
6
7
8
9
10
import git

def clone(user, repository):
    url = 'git@bitbucket.org:' + user + '/' + repository
    directory = 'repositories/' + repository

    repository = git.Repo.clone_from(
        url,
        directory,
        multi_options=['--mirror'])

2. Criando o repositório no Github

Depois de clonado o repositório localmente, é necessário criar ele no Github. Para isso vamos usar a API do Github.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def create_repository(organization, repository):
    data = {
        'name': repository,
        'private': True,
        'has_issues': True,
        'has_projects': True,
        'has_wiki': True
    }

    headers = {
        'Authorization': 'token ' + GITHUB_TOKEN
    }

    url = 'https://api.github.com/orgs/%s/repos' % (organization)
    response = requests.post(url, data=json.dumps(data), headers=headers)
    return response.ok

Note que existe uma variável no método, GITHUB_TOKEN. Ela é o token de autenticação necessário para se comunicar com a API. É possível obte-la na página https://github.com/settings/tokens/new. Para que o código acima funcione, é necessário liberar o controle completo dos repositórios para esse token. Após gerar o token, o Github irá mostrar uma hash de 40 caracteres, que deverá ser usada nessa variável.

3. Enviando o repositório para o Github

Criado o repositório, ele ainda estará vazio. Agora vamos enviar os dados do repositório clonado no passo 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def send(organization, repository):
    url = 'git@github.com:' + organization + '/' + repository
    directory = 'repositories/' + repository

    repository = git.repo.base.Repo(directory)

    remote = repository.remote()
    remote.set_url(url, push=True)

    pushInfo = remote.push(mirror=True)
    for info in pushInfo:
        if info.ERROR & info.flags != 0:
            return False

    return True

4. Listando os repositórios do bitbucket

Agora que podemos migrar 1 repositório, vamos pegar todos os que estão armazenados no bitbucket e em um laço envia-los para o github.

Para se comunicar com o bitbucket vamos usar o cliente oficial do Bitbucket para Python, o PyBitbucket.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pybitbucket.bitbucket import Client
from pybitbucket.auth import BasicAuthenticator
from pybitbucket.repository import Repository

def get_repositories(username, password, email):
    bitbucket = Client(
        BasicAuthenticator(
            username,
            password,
            email))

    repositories = Repository.find_repositories_by_owner_and_role(
        owner=username,
        role='admin',
        client=bitbucket)

    return list(repositories)

5. Juntando tudo

Agora com todas as partes necessárias criadas, basta juntar tudo em uma função só para fazer a migração:

1
2
3
4
5
6
7
8
9
def migrate(username, password, email, organization):
    repositories = get_repositories(username, password, email)

    for repo in repositores:
        print ('Migrating', repo.slug)

        clone(username, repo.slug)
        create_repository(organization, repo.slug)
        send(organization, repo.slug)

Conclusão

Com isso, em pouco mais de 1 hora foi possível mover todos os 250 repositórios de um serviço para o outro. Ainda existem vários outros lugares para mudar (Slack, CI, etc.), que serão feitos conforme a demanda, mas o importante é que todo o código está pronto para ser trabalhado pelos desenvolvedores.

Algumas bases deram erro, devido ao Github ter limitação de tamanho de arquivo e de repositório. Nesses casos, infelizmente, ainda iremos usar o bitbucket por um bom tempo.