Git submodules: o que são, como usar e quando vale a pena

O que são os submódulos do Git, por que existem, exemplos de uso com comandos, prós e contras, projetos reais no GitHub e um quiz para decidir se vale a pena.

Iago Frota
ferramentas

O GIT é um mundo e a gente usa as funções mais básicas dele no dia a dia (e tá tudo bem!). Mas de vez em quando eu gosto de revisar algo e descobrir o que posso estar melhorando no meu operacional.

Já escrevi aqui no blog sobre outras partes do Git:

  1. Em abril de 2020, o git bare, para automatizar o deploy das minhas aplicações.
  2. No mês seguinte, os estágios do Git. Lá eu admiti que, por muito tempo, utilizava certos comandos sem entender o que realmente estava acontecendo.
  3. Em setembro de 2023, o git stash.

Esses dias fui procurar na minha máquina quais repositórios usam submódulos. Achei um arquivo .gitmodules em um projeto pessoal e em alguns dos projetos da empresa onde eu trabalho.

Ver o arquivo é fácil. Explicar de cabeça por que ele está ali, nem tanto. Era a curiosidade batendo outra vez.

Por isso fui ler a documentação e rodei os comandos principais abaixo em um repositório de teste, com o Git 2.43, antes de escrever.

Neste artigo eu cubro seis coisas:

  1. O que são os submódulos.
  2. Por que o Git ganhou esse recurso.
  3. Exemplos de uso, com os comandos.
  4. Um quiz para você decidir se vale a pena no seu caso.
  5. Os prós e os contras.
  6. Projetos no GitHub que usam submódulos.

Bora lá.

O que são submódulos

O Pro Git, o livro oficial, define assim: submódulos permitem manter um repositório Git como subdiretório de outro, com os commits separados.

Na prática, o repositório de fora (o superprojeto) não guarda os arquivos do de dentro. Ele guarda duas coisas: o endereço, no arquivo .gitmodules, e um commit exato do repositório de dentro.

Terminal window
git submodule add https://github.com/exemplo/lib vendor/lib
git status --short
A .gitmodules
A vendor/lib

O .gitmodules é versionado junto com o resto, então todo mundo que clonar sabe de onde buscar:

[submodule "vendor/lib"]
path = vendor/lib
url = https://github.com/exemplo/lib

E o commit fica registrado como um item especial, de modo 160000:

Terminal window
git ls-files -s vendor/lib
160000 7ee8d1e49243cb53e518db1bbf8c3af03c082513 0 vendor/lib

O Git não guarda os arquivos. Guarda o endereço e o commit.

Repositório dentro de repositório não é, por si só, submódulo. O meu blog, por exemplo, tem uma pasta iagofrota-blog/ que é um repositório próprio, mas o .gitignore do repositório de fora a ignora. Ele nem sabe que ela existe. Submódulo é quando o Git de fora registra a relação.

Por que existem

O Pro Git abre o assunto com um problema comum. Você está construindo um site e quer usar uma biblioteca que já existe. Se pegar pelo gerenciador de pacotes, fica difícil customizar. Se copiar o código para dentro do projeto, suas alterações viram uma dor de cabeça quando a biblioteca lançar versão nova.

O submódulo é a resposta do Git para isso: você clona o outro repositório dentro do seu e mantém os commits de cada um separados.

Quanto à história, o Git ganhou o comando git submodule na versão 1.5.3, lançada em setembro de 2007. As notas dessa versão dizem que o suporte era mínimo, e que isso era de propósito. Uma das decisões de desenho foi esta: as operações no superprojeto não entram nos submódulos por padrão.

Isso explica o comportamento que mais confunde quem chega: você clona o projeto e a pasta do submódulo vem vazia. Não é bug. É decisão de 2007.

Na prática

Clonar um projeto com submódulos

Sem nenhuma flag, o clone cria a pasta e deixa ela vazia. No meu teste, ls na pasta devolveu zero arquivos, e o git submodule status mostrou o hífen que marca um submódulo não inicializado:

Terminal window
git clone https://github.com/exemplo/app
cd app
git submodule status
-7ee8d1e49243cb53e518db1bbf8c3af03c082513 vendor/lib

Dois caminhos resolvem. O primeiro é pedir tudo já no clone:

Terminal window
git clone --recurse-submodules https://github.com/exemplo/app

O segundo é corrigir um clone que já existe:

Terminal window
git submodule update --init --recursive

Atualizar para a versão nova

Para trazer o commit mais novo da branch remota do submódulo (por padrão, a HEAD do remoto, ou a branch definida em branch no .gitmodules):

Terminal window
git submodule update --remote
git status --short
git diff --submodule
M vendor/lib
Submodule vendor/lib 7ee8d1e..739f438:
> lib: v2

Repare que o status marca a pasta como modificada, mas não há diff de arquivo. O que mudou foi o ponteiro. Falta commitar o novo commit de referência no superprojeto, e só então o resto do time recebe a atualização.

Outro detalhe: um git pull no superprojeto traz o ponteiro novo, mas não atualiza os arquivos do submódulo. Para isso é preciso rodar git submodule update de novo. Quem se cansa disso pode ligar a configuração submodule.recurse:

Terminal window
git config --global submodule.recurse true

Mexer dentro do submódulo

Depois do update, o submódulo fica em detached HEAD, sem branch. É o comportamento padrão do checkout; com --merge ou --rebase ele é outro. No meu teste:

HEAD detached at 739f438

Se você commitar assim, o commit fica sem nenhuma branch apontando para ele, e depois é difícil de achar. Entre na pasta e crie uma branch a partir do commit atual:

Terminal window
cd vendor/lib
git switch -c minha-alteracao
# ...edite, commite...
cd ../..

Prefira criar a branch a dar um git checkout main. A main local pode estar num commit mais antigo, e o checkout devolveria o submódulo para ele, desfazendo a atualização que você acabou de fazer.

Publicar na ordem certa

Aqui mora o erro mais caro. Se você commitar no submódulo, atualizar o ponteiro no superprojeto e publicar só o superprojeto, o ponteiro aponta para um commit que só existe na sua máquina. Quem clonar depois vai tropeçar.

O Git avisa, se você pedir. Com a opção check, o push falha, e a mensagem do meu teste foi esta:

Terminal window
git push --recurse-submodules=check origin main
The following submodule paths contain changes that can
not be found on any remote:
vendor/lib
Please try
git push --recurse-submodules=on-demand

O check só confirma que o commit existe em algum remoto do submódulo. Não garante que seja o remoto dos seus colegas, nem que eles tenham acesso a ele.

Com on-demand, o Git publica o submódulo primeiro e o superprojeto depois. Se você quer isso sempre, configure uma vez:

Terminal window
git config push.recurseSubmodules on-demand

Remover um submódulo

Remover dá mais trabalho do que adicionar. O caminho seguro, no meu teste, foram dois comandos:

Terminal window
git rm vendor/lib
git commit -m "remove o submódulo vendor/lib"

O git rm tira a pasta e a entrada do .gitmodules. Se o submódulo tiver alteração local, ele recusa e avisa. Resolva o trabalho antes, em vez de forçar com -f: o -f descarta o que não foi commitado.

Sobra uma cópia do repositório do submódulo em .git/modules/vendor/lib. O Git deixa isso de propósito, para você conseguir voltar a versões antigas do projeto. Só apague com rm -rf se tiver certeza de que não há commit seu guardado apenas ali, porque esse rm -rf não tem volta.

No CI

Pipeline também clona. O actions/checkout, do GitHub Actions, aceita a opção submodules, com true ou recursive. Sem ela, o job roda com a pasta vazia e a falha aparece longe da causa.

Quando usar

Antes do quiz, a regra em quatro linhas:

  1. Se o código já está publicado em um gerenciador de pacotes (npm, Maven Central, PyPI, Packagist) e a versão publicada atende, use o gerenciador. Ele já fixa a versão para você.
  2. Se nada exige os repositórios separados (outro time, outro ciclo de release, outro controle de acesso), considere juntar em um só.
  3. Se é código de terceiro que você dificilmente vai devolver, e o clone precisa ser simples, olhe o git subtree.
  4. Se você precisa de um commit exato de outro repositório, com histórico separado, submódulo.

A Opensource.com separa os dois assim: submódulo para um repositório externo que é seu e para o qual você vai empurrar mudanças, subtree para código de terceiro que você provavelmente não vai devolver.

A regra tem uma zona cinzenta, e vale ser honesto: o PyTorch, que você vai ver mais abaixo, fixa dezenas de projetos de terceiros como submódulos. O .gitmodules mostra a fixação, e não o motivo. Minha hipótese é o commit exato, mas é só hipótese.

Teste com o seu caso

Montei as mesmas perguntas em um quiz de sim ou não. Responda na ordem. O veredito aparece assim que houver resposta suficiente para decidir.

Prós e contras

Os prós:

  1. Commit exato. O superprojeto aponta para uma versão específica do código, o que ajuda a reproduzir o build.
  2. Repositório pequeno. Ele guarda só o ponteiro, e não o código nem o histórico do outro projeto.
  3. Histórico separado. Cada repositório mantém os próprios commits, releases e permissões.
  4. Devolver mudanças é simples. Você entra na pasta, commita e publica no repositório de origem. Num subtree isso dá mais trabalho.

Os contras:

  1. O clone ganha um passo. Sem --recurse-submodules, a pasta vem vazia.
  2. Detached HEAD. Quem não sabe disso deixa commits sem branch e depois não consegue achá-los.
  3. É fácil esquecer de publicar o submódulo. O ponteiro vai para o servidor e o commit, não.
  4. Depende de um servidor acessível. O repositório de origem precisa estar no ar, e se o commit referenciado sumir de lá, o checkout quebra.
  5. Conflito de ponteiro. Quando duas pessoas movem o mesmo submódulo para commits que divergem, em branches diferentes, o merge pede que você resolva na mão.
  6. Curva de aprendizado. O artigo da Opensource.com diz que iniciantes costumam achar submódulos difíceis.
  7. Remover dá trabalho, e o -f pode custar trabalho não commitado, como vimos acima.

Projetos no GitHub

Conferi o .gitmodules de cada um em 9 de outubro de 2026. Os números mudam com o tempo.

  1. PyTorch. Tinha 36 entradas, quase todas em third_party/: googletest, pybind11, benchmark e outras.
  2. gRPC. Tinha 20, também em third_party/: abseil-cpp, protobuf, re2, zlib e googletest, entre outras.
  3. Prezto. Um framework de configuração para Zsh. Tinha 10 entradas, em modules/*/external: temas e plugins de terceiros como powerlevel10k, pure e agnoster.
  4. Hugo. O quick start manda adicionar o tema Ananke como submódulo, em themes/ananke. O repositório do Hugo em si não usa.
  5. Um projeto na minha máquina. O meeting-minutes aponta backend/whisper.cpp para um fork do whisper.cpp, na branch develop.

Na minha leitura, o padrão se repete: bibliotecas C++, onde não há um gerenciador de pacotes dominante, e temas ou plugins de terceiros que você quer atualizar no seu ritmo.

Conclusão

Por fim, submódulo é uma ferramenta de commit exato, e não de conveniência. Quem escolhe por achar que é “o jeito do Git de importar biblioteca” costuma sentir o atrito no primeiro clone.

No fim do dia, a pergunta é quanto atrito o seu time aguenta em troca de ter um commit fixado e um histórico separado. Se a resposta for “pouco”, o gerenciador de pacotes ou o subtree provavelmente servem melhor.

Escrever sobre git bare, estágios, stash e agora submódulos me lembrou de uma coisa: o Git que eu uso todo dia é uma parte pequena do Git que existe. (Sei que ainda tem muita função que eu não conheço.) É por isso que eu continuo curioso.

E você, qual foi a última função do Git que fez você abrir a documentação só por curiosidade?

Referências

  1. Pro Git: Git Tools - Submodules
  2. Git 1.5.3 Release Notes
  3. Managing Git projects with submodules and subtrees (Opensource.com)
  4. actions/checkout
  5. Hugo Quick Start
#git #submodules #versionamento #github #engenharia