W3docs

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.

Informação

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.

AspectoGit SubtreeGit Submodule
Arquivos após cloneJá presentesVazios até submodule update --init
Metadados extrasNenhum.gitmodules + gitlinks
Fixa um commit upstream exatoNão (o histórico é mesclado)Sim (registra um SHA)
Contribuir de volta para o upstreamManual (git subtree split + push)Natural (commit dentro do submodule)
Tamanho do repositórioMaior (o histórico do subprojeto é copiado)Menor (apenas um ponteiro)
Curva de aprendizado para consumidoresNenhumaÉ 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 --squash

As partes deste comando:

  • --prefix .vim/bundle/example — o diretório onde o subprojeto ficará. Esta opção é obrigatória para todo comando subtree.
  • 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: b999b09cd9d69f359fa5668e81b09dcfde455cca

Atualizando 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 --squash

Isso 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~5

No editor, você pode descartar ou comprimir os commits de merge do subtree, depois salvar e executar:

git rebase --continue

Como 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çãoDescrição
-q, --quietSuprime mensagens de resultado desnecessárias no stderr.
-d, --debugProduz 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.
--squashImporta 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 --squash

O 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 --squash

Contribuindo 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-branch

Isso 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:master

Essa 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.git

Adicione o subtree:

git subtree add --prefix .vim/bundle/vim-double-upon vim-double-upon master --squash

Atualize o subprojeto assim:

git fetch vim-double-upon master
git subtree pull --prefix .vim/bundle/vim-double-upon vim-double-upon master --squash

Resumo

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.

Prática

Prática
Quais são as características e o uso do Git subtree?
Quais são as características e o uso do Git subtree?
Was this page helpful?