DevOps, CI/CD e contêineres
Do Subversion para o Git: conceitos, migração e hábitos da equipe
Intermediário12 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Tirar um projeto do Subversion envolve dois trabalhos ao mesmo tempo: converter o histórico com fidelidade e reeducar uma equipe cujo modelo mental pressupõe um único servidor central. Este guia compara os dois modelos com honestidade, traduz o vocabulário comando a comando, percorre uma conversão completa com git svn incluindo o mapeamento de branches, tags e autores, e apresenta um plano de virada que não perde trabalho.
O que você vai aprender
- Contrastar os modelos centralizado e distribuído e explicar o que cada um otimiza
- Traduzir comandos, convenções de estrutura e propriedades do Subversion para os equivalentes no Git
- Converter um repositório com git svn, incluindo um arquivo de autores e estruturas fora do padrão
- Transformar os diretórios de branches e tags do SVN importados em branches reais do Git e tags anotadas
- Planejar a virada, verificar o resultado e mudar os hábitos que a equipe traz consigo
O Subversion ainda roda uma enorme quantidade de código em produção, e os projetos que o usam raramente são os pequenos. Uma migração, portanto, cai sobre equipes com uma década de revisões, uma estrutura de diretórios que se afastou das convenções, scripts de build que interpretam números de revisão e desenvolvedores que nunca trabalharam sem um servidor central para arbitrar. A mecânica de converter o histórico é a metade fácil; a metade difícil é que o Git pede que as pessoas pensem de outro jeito sobre o que é um commit e quando ele fica visível para os outros.
Este guia cobre as duas metades. Começa pela diferença conceitual, porque a maior parte do atrito de uma migração vem dela; depois traduz o vocabulário e percorre uma conversão real com git svn, incluindo as partes em que todo mundo tropeça — o arquivo de autores, as estruturas fora do padrão e a transformação dos diretórios importados em branches e tags de verdade. Termina com a virada e com os hábitos que vale a pena mudar desde o primeiro dia.
O que realmente diferencia os dois modelos
O Subversion tem um único repositório. Ele fica em um servidor, guarda o histórico completo e atribui a cada commit um número de revisão sequencial que cresce para o repositório inteiro. Sua working copy é um checkout de um caminho em uma revisão, e não contém histórico nenhum. Fazer commit significa falar com o servidor; se ele estiver inacessível, ou se você não tiver permissão de escrita, não dá para fazer commit.
O Git dá a cada desenvolvedor um repositório completo. Clonar copia todo o banco de objetos, então histórico, branches e tags são todos locais. Fazer commit grava no seu próprio repositório e não envolve mais ninguém. Compartilhar é um ato separado e explícito — push — e receber é outro, fetch. Como um commit é identificado pelo hash do seu conteúdo e de toda a sua ancestralidade, não existe contador global para incrementar e, portanto, não há necessidade de um coordenador.
Essa única mudança estrutural produz a maior parte das diferenças práticas:
- Criar branches é barato. No Subversion, uma branch é uma cópia de um diretório no servidor; no Git, é um arquivo que contém um hash de commit. Equipes que criavam uma branch por release passam a criar uma por mudança.
- O histórico é um grafo, não uma linha. Os números de revisão do Subversion implicam uma ordem total. Commits do Git têm pais, merges têm dois, e "o que veio primeiro" só faz sentido ao longo de um caminho no grafo.
- A maioria das operações é offline e instantânea.
log,diff,blame,checkoutecommitleem dados locais. Em um repositório grande, a diferença é gritante. - Checkouts parciais não são a regra. O Subversion permite fazer checkout de um único subdiretório. O Git clona tudo, e é por isso que monorepos enormes cheios de binários exigem mais cuidado (sparse checkout, partial clone, Git LFS).
- O histórico pode ser reescrito. Amend, rebase e squash não têm equivalente no Subversion. São poderosos antes de compartilhar e disruptivos depois, o que é uma regra nova para a equipe aprender.
Nenhum dos modelos é melhor em tudo. A autoridade central do Subversion torna simples o controle de acesso por diretório e oferece bloqueio para arquivos que não podem ser mesclados. O Git abre mão disso em troca de velocidade, trabalho offline e um modelo de branches que tornou viáveis o code review moderno e a integração contínua.
Traduzindo o vocabulário
A maior parte do mapeamento do dia a dia fica mecânica quando você aceita que um comando do SVN normalmente vira dois comandos do Git, porque os passos local e remoto são separados.
| Subversion | Git | Observação |
|---|---|---|
svn checkout URL | git clone URL | O clone traz o histórico inteiro, não uma revisão. |
svn update | git fetch e depois git merge (ou git pull) | O fetch nunca mexe nos seus arquivos. |
svn commit | git commit e depois git push | O commit é local até ser enviado com push. |
svn status | git status | O Git também mostra o que está em staging. |
svn diff | git diff / git diff --staged | Dois diffs porque o Git tem um índice. |
svn add / svn rm | git add / git rm | O git add também coloca modificações em staging. |
svn revert FILE | git restore FILE | Descarta alterações não commitadas. |
svn merge -c 4711 | git cherry-pick <hash> | Aplica uma mudança em outro lugar. |
svn copy ^/trunk ^/branches/x | git switch -c x | Sem ida e volta ao servidor. |
svn switch ^/branches/x | git switch x | Trocar de branch é uma operação local em arquivos. |
svn log -v | git log --stat | Acrescente --graph para ver os merges. |
svn blame | git blame | Mesma ideia, executada localmente. |
propriedade svn:ignore | arquivo .gitignore | Um arquivo versionado, não metadado. |
svn:externals | submodule, subtree ou dependência de pacote | Sem conversão automática. |
svn lock | bloqueio de arquivos do Git LFS, ou nada | O Git não tem bloqueio nativo. |
número de revisão r4711 | hash de commit a1b2c3d | Use git describe para ter nomes legíveis. |
Duas convenções merecem um comentário. Os diretórios trunk, branches e tags do Subversion são pura convenção — o servidor vê apenas diretórios comuns, e é por isso que uma cópia é uma branch. O Git tem refs de branch e de tag de primeira classe, então, depois da conversão, esses diretórios desaparecem e viram refs. Além disso, as tags do Subversion são diretórios mutáveis nos quais às vezes alguém faz commit; as tags do Git devem ser ponteiros permanentes, então uma "tag" que recebeu commits depois de criada precisa ser convertida em branch.
Escolhendo o caminho de conversão
Três ferramentas cobrem quase todos os casos.
git svn vem com o Git, não precisa de instalação extra e faz uma importação fiel, revisão por revisão. É a escolha padrão certa para um repositório único de até algumas dezenas de milhares de revisões, e também pode funcionar como ponte bidirecional durante um período de transição.
svn2git é um wrapper fino sobre o git svn que automatiza a conversão de branches e tags descrita mais adiante. É prático, mas entenda o que ele faz — você ainda vai precisar inspecionar o resultado. Uma outra ferramenta em C++, com o mesmo nome e desenvolvida para grandes migrações open source, lê diretamente um arquivo de dump do Subversion e é muito mais rápida em repositórios muito grandes, ao custo de um arquivo de regras que você precisa escrever.
reposurgeon é a opção pesada: carrega o histórico em uma forma editável para que você possa reescrever autores, corrigir mensagens, remover caminhos ou dividir repositórios. Recorra a ele quando o histórico estiver realmente bagunçado.
Montando o arquivo de autores
O Subversion registra um nome de usuário; o Git registra um nome e um endereço de e-mail em cada commit. Sem um mapeamento, todo commit histórico é atribuído a jdoe <jdoe@localhost>, o que quebra o git shortlog, a filtragem do blame e qualquer ferramenta que vincule commits a contas. Monte o mapeamento antes de começar.
# Every distinct committer in the repository's history
svn log --quiet https://svn.example.com/shop \
| awk '/^r[0-9]+ \| /{print $3}' \
| sort -u > authors-raw.txt
Transforme essa lista em authors.txt, com uma linha por usuário:
jdoe = Jane Doe <[email protected]>
amartin = Alex Martin <[email protected]>
build = Build Service <[email protected]>
(no author) = Unknown <[email protected]>
Inclua a linha (no author): revisões criadas pelo administrador do repositório ou por importações antigas muitas vezes não têm autor, e o git svn aborta quando encontra um nome de usuário que não está no arquivo. Peça a cada pessoa que confira a própria linha — um endereço que não bate com a identidade dela no Git fará os commits históricos e os novos parecerem de duas pessoas diferentes.
Executando a conversão
Para um repositório que segue a estrutura padrão:
git svn clone https://svn.example.com/shop \
--stdlayout \
--authors-file=authors.txt \
--no-metadata \
--prefix=svn/ \
shop-git
--stdlayout é um atalho para --trunk=trunk --branches=branches --tags=tags. --no-metadata omite a linha git-svn-id: que, de outra forma, seria anexada a cada mensagem de commit; não use essa opção se pretende continuar fazendo commit de volta no Subversion pela ponte, porque a ponte precisa dessa linha. --prefix=svn/ agrupa as refs importadas em refs/remotes/svn/, o que deixa sem ambiguidade a etapa de limpeza mais adiante.
Estruturas fora do padrão são comuns. Informe os caminhos explicitamente e repita a opção quando as branches estiverem em vários lugares:
git svn clone https://svn.example.com/legacy \
--trunk=main/src \
--branches=branches --branches=feature-branches \
--tags=releases \
--authors-file=authors.txt \
--ignore-paths='^(vendor/binaries|docs/generated)/' \
legacy-git
--ignore-paths recebe uma expressão regular e é a forma mais fácil de deixar fora do novo histórico artefatos gerados, binários de terceiros ou um dump de banco de dados commitado por acidente. Para importar só o histórico recente, acrescente -r 40000:HEAD: você obtém uma conversão rápida cujo primeiro commit é um snapshot, com o servidor antigo mantido em modo somente leitura para tudo o que for mais antigo.
Se a importação parar — uma falha de rede, um autor faltando —, corrija a causa e retome com git svn fetch dentro do repositório parcialmente convertido. Ele continua a partir da última revisão convertida em vez de recomeçar do zero.
Convertendo as refs importadas em branches e tags
Depois do clone, git branch -a mostra entradas como remotes/svn/trunk, remotes/svn/release-2.4 e remotes/svn/tags/v2.4.0. São remote-tracking refs, não branches de verdade, e enviá-las para um servidor novo não adianta nada. Converta-as:
cd shop-git
# SVN tag directories become annotated Git tags
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/svn/tags \
| while read -r ref sha; do
name="${ref#svn/tags/}"
git tag -a "$name" -m "Imported from Subversion tag $name" "$sha"
git branch -r -d "$ref"
done
# The remaining SVN branches become local branches
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/svn \
| while read -r ref sha; do
name="${ref#svn/}"
[ "$name" = trunk ] && continue
git branch "$name" "$sha"
git branch -r -d "$ref"
done
git branch -m master main # if trunk was checked out as master
Antes do push, faça a faxina que o repositório antigo nunca permitiu. Converta as propriedades de ignore do Subversion em um arquivo de verdade e adicione as entradas que antes eram tratadas pelo servidor:
git svn show-ignore >> .gitignore
git add .gitignore && git commit -m "Add .gitignore from svn:ignore properties"
Depois, procure o que está engordando o repositório. git count-objects -vH informa o tamanho do repositório, e este pipeline lista os maiores blobs já commitados:
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" {print $3, $4}' | sort -nr | head -20
Se o topo dessa lista for de pacotes de release ou jars de terceiros, remova-os com git filter-repo --path <dir> --invert-paths antes do primeiro push. Reescrever o histórico depois invalida todos os clones.
Verificando e fazendo a virada
Nunca confie em uma conversão que você não conferiu. Três verificações baratas pegam quase tudo:
Compare as pontas
Exporte a ponta do trunk do Subversion e a ponta damaindo Git para dois diretórios e rodediff -r, ignorando.svne.git. As únicas diferenças devem ser os arquivos que você excluiu de propósito.Compare as contagens
svn log --quiet | grep -c '^r'contragit rev-list --count --all. Os números não vão bater exatamente — merges e caminhos excluídos mudam o formato —, mas uma diferença de uma ordem de grandeza significa que as opções de estrutura estavam erradas.Faça o build de cada tag
Faça checkout de duas ou três tags de release e rode o build. Uma release que você não consegue reconstruir a partir do novo repositório é uma migração que falhou em silêncio.
Para a virada em si, congele as escritas em vez de tentar manter os dois sistemas em paralelo indefinidamente. Anuncie uma data, deixe o repositório Subversion em modo somente leitura com um hook pre-commit, rode um último git svn fetch para pegar as revisões finais, refaça a conversão de branches e tags e então faça o push:
git remote add origin [email protected]:acme/shop.git
git push --all origin
git push --tags origin
Um curto período de ponte é razoável se algumas pessoas não puderem migrar imediatamente. Com os metadados mantidos (sem --no-metadata), git svn rebase traz as novas revisões do SVN para o clone Git e git svn dcommit reaplica os commits locais do Git como revisões do SVN. Trate isso como uma concessão de duas semanas, não como arquitetura: a ponte só funciona a partir de um repositório cujo histórico não foi reescrito, e escrever nos dois lados é um convite à divergência.
Atualize tudo o que depende do repositório na mesma janela. Jobs de build que referenciavam ${SVN_REVISION} precisam de um novo esquema de versão — git describe --tags ou o hash curto. Links para r4711 no issue tracker quebram. Scripts de release que faziam svn copy para criar uma tag agora fazem git tag -a e git push origin <tag>. O guia de pipeline de entrega aborda estratégias de versionamento que funcionam com hashes em vez de contadores.
O que a equipe precisa desaprender
Três hábitos causam quase toda a confusão inicial, e dar nome a eles com antecedência economiza duas semanas de perguntas ao suporte.
"Commit" não significa mais "compartilhar". Desenvolvedores acostumados ao Subversion ou acumulam mudanças por dias porque fazer commit era público, ou fazem commit e se perguntam por que ninguém vê o trabalho deles. Diga explicitamente: faça commit cedo e com frequência localmente, e faça push quando o trabalho estiver coerente.
"Atualizar" não reescreve mais seus arquivos pelas suas costas. git fetch é seguro e pode ser rodado o tempo todo; o merge ou o rebase é a etapa que mexe no working tree.
Branches são para mudanças, não para releases. O instinto herdado do Subversion é que uma branch é um evento pesado, que exige aprovação. Escolha um modelo e registre-o por escrito — o guia de workflows de branches compara as opções realistas, e o trunk-based development costuma ser a menor mudança em relação aos hábitos de uma equipe Subversion.
Apoie a transição de forma prática: uma página com a tradução dos comandos pregada na parede e uma pessoa designada para responder perguntas sobre Git durante o primeiro mês. Garanta que todos leiam a parte dos fundamentos de Git sobre como desfazer alterações, porque, do contrário, "como eu recupero meu arquivo?" vai ser a pergunta de todo dia.
Armadilhas que vale a pena antecipar
- Autores ausentes abortam a importação depois de horas. Gere a lista antes e inclua
(no author). - Tags que receberam commits. Elas são convertidas em tags que apontam para o último estado, perdendo em silêncio as revisões intermediárias. Verifique com
svn log ^/tagsquais caminhos de tag têm mais de uma revisão e converta-os em branches. - Diretórios vazios somem. O Git rastreia arquivos, não diretórios. Se o build precisar de um diretório vazio, adicione um arquivo
.gitkeep. - Finais de linha e propriedades de arquivo.
svn:eol-stylevira uma entrada no.gitattributes(* text=auto), esvn:executablecorresponde ao modo do arquivo. Configure o.gitattributesno primeiro commit após a conversão em vez de descobrir o problema mais tarde em uma máquina Windows. - Ninguém é dono do servidor antigo. Decida quem mantém a instância Subversion somente leitura funcionando, por quanto tempo e onde ficam os backups dela. "Vamos desativar no próximo trimestre" costuma significar que ele vai sumir sem nenhum aviso.
Principais pontos
- O Subversion centraliza o histórico em um servidor e numera as revisões globalmente; o Git dá a cada desenvolvedor o histórico completo, e é por isso que criar branches, consultar o log e fazer commit são operações locais e instantâneas.
- A maioria dos comandos passa de um para dois: um commit do SVN vira um commit do Git mais um push, e um update do SVN vira um fetch mais um merge.
- Monte o arquivo de autores antes de converter, inclua
(no author)e espelhe o repositório localmente se o servidor for lento. git svn clone --stdlayoutresolve os repositórios comuns;--trunk,--branches,--tagse--ignore-pathsexplícitos resolvem o resto, e a importação pode ser retomada comgit svn fetch.- Refs importadas não são branches de verdade: converta diretórios de tags em tags anotadas e diretórios de branches em branches, adicione um
.gitignoree um.gitattributese remova os binários grandes antes do primeiro push. - Verifique comparando as pontas, conferindo a contagem de commits e refazendo o build das tags publicadas; depois congele o Subversion, faça o push uma única vez e mantenha curto o período de ponte.
- Reserve para a mudança de hábitos — commit não é compartilhar, fetch é seguro, branches são baratas — tanta atenção quanto para a conversão em si.
Fontes e leituras complementares
Perguntas frequentes
- Devemos manter todo o histórico do Subversion ou começar do zero?
- As duas opções são defensáveis. Uma conversão completa preserva blame, bisect e a arqueologia do código, o que importa para código de negócio de vida longa, mas demora mais e arrasta junto cada binário grande já commitado. Começar com um repositório vazio e um único commit de importação é rápido e limpo, e funciona quando o servidor antigo continua no ar em modo somente leitura para consultas ao passado. Decida perguntando com que frequência alguém realmente lê histórico com mais de dois anos.
- Quanto tempo leva uma conversão com git svn?
- Ele percorre o histórico do Subversion uma revisão por vez pela rede, então conte com algo entre uma e algumas revisões por segundo. Um repositório com 50.000 revisões pode facilmente rodar a noite inteira. Execute a conversão em uma máquina próxima do servidor SVN, use um espelho local criado com svnsync se puder e nunca presuma que ela vai terminar dentro de uma janela de manutenção marcada para uma tarde.
- O que acontece com os svn:externals?
- Nada automático: o git svn os ignora. Um external que aponta para outro projeto normalmente vira uma dependência resolvida pelo Maven, pelo npm ou pelo gerenciador de pacotes da sua linguagem, o que quase sempre é a melhor resposta. Se você realmente precisa de um checkout aninhado, um submodule do Git ou um subtree merge reproduz o comportamento, mas analise cada external individualmente em vez de convertê-los mecanicamente.
- Ainda precisamos de bloqueio de arquivos depois de migrar para o Git?
- O Git não tem equivalente ao svn lock, porque seu modelo de merge pressupõe texto que pode ser combinado. Equipes que bloqueavam arquivos binários — arquivos de design, planilhas, documentos gerados — devem movê-los para o Git LFS, que oferece um workflow de bloqueio de arquivos, ou mantê-los em um sistema feito para binários. Bloquear código-fonte geralmente era um hábito que valia a pena perder, e não um requisito a ser reproduzido.