Git Subtree
Introdução ao Git subtree: vantagens, desvantagens e diferenças em relação ao submodule, com exemplos práticos de uso.
Conforme mencionado na página anterior, o Git Submodule é útil em casos específicos. Para rastrear dependências de software dentro de um único repositório, muitos desenvolvedores preferem o Git Subtree.
Esta página explica o que é um Git subtree, quando utilizá-lo em vez de um submodule, e como adicionar, atualizar e contribuir alterações de volta para um subtree. Também aborda como fazer rebase em um repositório que contém subtrees e as opções de comando mais utilizadas.
O que é Git Subtree
Git Subtree é uma alternativa ao Git Submodule. Ele permite aninhar um repositório dentro de outro como um subdiretório, preservando o histórico do projeto embutido ("sub"). É uma das formas de rastrear o histórico de dependências de software.
A principal diferença em relação aos submodules é que um subtree é apenas um diretório. Ao contrário dos submodules, os subtrees não precisam de um arquivo .gitmodules nem de gitlinks especiais no repositório. Os arquivos ficam diretamente na sua árvore de trabalho, são confirmados junto com todo o resto e acompanham automaticamente cada clone, branch e merge. Qualquer pessoa que clone seu repositório obtém o código da dependência sem precisar executar nenhum comando adicional.
Internamente, o git subtree é um comando porcelana (um wrapper) em torno de plumbing padrão como git merge, git read-tree e git filter-branch/split. Você não precisa aprender um novo formato de armazenamento — apenas alguns novos comandos.
Com submodules, um clone resulta em um diretório vazio até que você execute git submodule update --init. Com subtrees, os arquivos já estão presentes. Essa única diferença é responsável pela maioria dos prós e contras abaixo.
Subtree vs. Submodule
Ambas as abordagens incorporam um repositório dentro de outro, mas fazem concessões opostas. Escolha com base em quem consome o repositório e com que frequência o código flui de volta para o upstream.
| Aspecto | Git Subtree | Git Submodule |
|---|---|---|
Arquivos após clone | Já presentes | Vazios até submodule update --init |
| Metadados extras | Nenhum | .gitmodules + gitlinks |
| Fixa um commit upstream exato | Não (o histórico é mesclado) | Sim (registra um SHA) |
| Contribuir de volta para o upstream | Manual (git subtree split + push) | Natural (commit dentro do submodule) |
| Tamanho do repositório | Maior (o histórico do subprojeto é copiado) | Menor (apenas um ponteiro) |
| Curva de aprendizado para consumidores | Nenhuma | É necessário aprender os comandos de submodule |
Uma regra geral: prefira subtrees quando você principalmente consome uma dependência e quer que cada clone funcione imediatamente; prefira submodules quando o subprojeto é ativamente co-desenvolvido e você precisa fixar ou enviar para um commit upstream exato.
Por que usar Git Subtree
Prós
- Suportado pelo Git 1.7.10 e versões posteriores (o comando
subtreeé distribuído com o próprio Git). - Fluxo de trabalho simples para quem clona seu repositório — sem comandos extras para aprender.
- O código do subprojeto está presente imediatamente após clonar o superprojeto.
- Não adiciona novos arquivos de metadados (por exemplo,
.gitmodules). - Permite modificar a dependência no local sem precisar de um checkout separado do repositório.
Contras
- Requer aprender uma nova estratégia de merge e alguns comandos específicos do
subtree. - Contribuir código de volta para o upstream é um processo de várias etapas (
git subtree split, depois push). - O código do superprojeto e do subprojeto ficam misturados no mesmo repositório, o que aumenta seu tamanho e pode poluir o histórico.
Como adicionar um Subtree
Suponha que existe um projeto externo e você deseja adicioná-lo ao seu repositório em um diretório específico.
Por exemplo, para adicionar uma extensão Vim em um repositório que armazena sua configuração do Vim, execute:
git subtree add --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squashAs partes deste comando:
--prefix .vim/bundle/example— o diretório onde o subprojeto ficará. Esta opção é obrigatória para todo comandosubtree.https://github.com/Example/vim-example.git— o repositório de origem (uma URL ou, posteriormente, um remote nomeado).master— o branch (ou commit/tag) de onde será feito o pull.--squash— reduz o histórico completo do subprojeto em um único commit, para não poluir seu log. Omita se quiser que todo o histórico upstream seja mesclado.
Com --squash, o Git registra o SHA-1 do master naquele momento para referência futura e produz dois commits — a importação comprimida e o merge:
commit 6d7054b3acea64e2e31f4d6fb2e3be12e5865e87
Merge: 87fa91e ef86deb
Author: Ann Smith<[email protected]m>
Date: Tue Jun 10 13:37:03 2016 +0200
Merge commit 'fe67ddf158faccff4082d78a25c45d8cd93e8ba8' as '.vim/bundle/example'
commit fe67ddf158faccff4082d78a25c45d8cd93e8ba8
Author: Ann Smith<[email protected]m>
Date: Tue May 12 13:37:03 2015 +0200
Squashed '.vim/bundle/example/' content from commit b999b09
git-subtree-dir: .vim/bundle/example
git-subtree-split: b999b09cd9d69f359fa5668e81b09dcfde455ccaAtualizando um Subtree
Para atualizar a subpasta para a versão mais recente do repositório filho, execute um subtree pull com o mesmo prefixo e origem:
git subtree pull --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squashIsso busca o branch upstream e o mescla no diretório do seu subtree, criando um novo commit de merge. Sempre use o mesmo --prefix e a mesma opção --squash/sem---squash que você usou em subtree add, caso contrário o Git não alinhará os históricos corretamente.
Observe que o git subtree armazena os IDs de commit do subprojeto nos metadados da mensagem de commit, não em referências simbólicas. Para encontrar o nome do branch ou tag associado a um commit armazenado, consulte o remote:
git ls-remote https://github.com/Example/vim-example.git | grep <commit-sha>Substitua <commit-sha> pelo hash real do commit da linha git-subtree-split: do seu commit de importação.
Rebase após Git Subtree
Para fazer rebase em um repositório que contém subtrees, use o modo --interactive do git rebase:
git rebase --interactive HEAD~5No editor, você pode descartar ou comprimir os commits de merge do subtree, depois salvar e executar:
git rebase --continueComo o rebase reescreve o histórico, os commits de merge do subtree podem ser removidos ou reorganizados. Após tal reescrita, normalmente é necessário restabelecer o subtree executando novamente git subtree add ou git subtree pull. Esteja ciente de que a estrutura de commits alterada também pode provocar conflitos de merge durante o rebase, por isso prefira fazer rebase em branches que ainda não foram enviados e compartilhados.
Opções Comuns
| Opção | Descrição |
|---|---|
-q, --quiet | Suprime mensagens de resultado desnecessárias no stderr. |
-d, --debug | Produz mensagens de depuração adicionais no stderr. |
-P <prefix>, --prefix=<prefix> | Define o caminho no repositório para o subtree que você deseja manipular. Obrigatório para todos os comandos. |
-m <message>, --message=<message> | Especifica <message> como a mensagem de commit para o commit de merge. Válido para add, merge e pull. |
--squash | Importa o subprojeto como um único commit em vez de mesclar todo o seu histórico. |
Usando Git Subtree sem rastreamento remoto
Adicione o git subtree em uma pasta com o prefixo especificado. Use a flag --squash para preservar todo o histórico do subprojeto no seu repositório principal:
git subtree add --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squashO comando realiza um fetch e comprime o histórico. A saída normalmente mostra o progresso do fetch seguido da confirmação de adição:
git fetch https://hostname.org/example/vim-plugins.git master
warning: no common commits
remote: Counting objects: 325, done.
remote: Compressing objects: 100% (145/145), done.
remote: Total 325 (delta 101), reused 313 (delta 89)
Receiving objects: 100% (325/325), 61.47 KiB, done.
Resolving deltas: 100% (110/110), done.
From https://hostname.org/vim-plugins.git
* branch master -> FETCH_HEAD
Added dir '.vim/bundle/vim-double-upon'Isso cria um commit de merge comprimindo todo o histórico do subprojeto em um único:
3bca0ad [4 minutes ago] (HEAD, stree) Merge commit 'fa2f5dc4f1b94356bca8a440c786a94f75dc0a45' as '.vim/bundle/vim-double-upon' [John Brown]
fa2f5dc [4 minutes ago] Squashed '.vim/bundle/vim-double-upon/' content from commit 13189ec [John Brown]Para atualizar o código do plugin a partir do repositório upstream, faça um git subtree pull:
git subtree pull --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squashContribuindo alterações de volta para o upstream
Como o código do subprojeto está misturado ao seu repositório, você não pode simplesmente enviá-lo de volta. Primeiro é necessário extrair as alterações que afetaram o diretório do subtree em um branch independente usando git subtree split:
git subtree split --prefix .vim/bundle/vim-double-upon -b split-branchIsso reescreve apenas os commits que afetam .vim/bundle/vim-double-upon em um novo branch (split-branch) cujos caminhos são relativos à raiz do subprojeto. Você pode então fazer push desse branch para o repositório upstream:
git push https://hostname.org/example/vim-plugins.git split-branch:masterEssa extração unidirecional é o motivo pelo qual "contribuir de volta para o upstream" é listado como um contraponto acima — não há sincronização bidirecional nativa.
Para tornar os comandos cotidianos de add/pull/push mais curtos, adicione o subprojeto como um remote.
Adicionando o subprojeto como Remote
Registrar a origem como um remote nomeado encurta todos os comandos posteriores — você referencia vim-double-upon em vez da URL completa. A flag -f o busca imediatamente:
git remote add -f vim-double-upon https://hostname.org/example/vim-plugins.gitAdicione o subtree:
git subtree add --prefix .vim/bundle/vim-double-upon vim-double-upon master --squashAtualize o subprojeto assim:
git fetch vim-double-upon master
git subtree pull --prefix .vim/bundle/vim-double-upon vim-double-upon master --squashResumo
Git Subtree é uma alternativa ao Git Submodule. Enquanto um submodule mantém o subprojeto como um ponteiro separado que deve ser inicializado, um subtree mantém o subprojeto como um diretório comum presente em cada clone. Use git subtree add para importar uma dependência, git subtree pull para atualizá-la, e git subtree split + push para enviar alterações para o upstream — não há sincronização bidirecional nativa. Para saber mais, explore Git Branch e Git Merge, que alimentam os comandos do subtree internamente.