Pular para o conteúdo

Submódulos

Git submódulos incorporam um repositório dentro de outro. Um submódulo é representado na árvore pai como uma entrada gitlink (modo 160000) que aponta para um commit específico no repositório do submódulo. O URL e o caminho do submódulo são declarados no arquivo .gitmodules.

Conceitos

Na árvore do repositório pai:

Código
100644 blob abc123...  README.md
040000 tree def456...  src/
160000 commit 789012... lib/external    <-- gitlink entry (submodule)

A entrada gitlink registra qual commit do submódulo deve ser verificado. O repositório real do submódulo fica em .git/modules/<name>/.

SubmoduleManager

PHP
use Pitmaster\Submodule\SubmoduleManager;

$manager = new SubmoduleManager(
    $repo->objectDatabase(),
    $repo->workDir(),
    $repo->gitDir(),
);

Listar submódulos

PHP
$submodules = $manager->list();

foreach ($submodules as $sub) {
    echo "{$sub->name}: {$sub->url} at {$sub->path}\n";
    if ($sub->branch !== null) {
        echo "  tracks branch: {$sub->branch}\n";
    }
}

Objeto de valor do submódulo

PHP
use Pitmaster\Submodule\Submodule;

$sub->name;     // Submodule name (from .gitmodules section header)
$sub->path;     // Path in the working tree
$sub->url;      // Remote URL
$sub->branch;   // Tracked branch (nullable)

Analisar .gitmodules

O arquivo .gitmodules declara a configuração dos submódulos:

Ini
[submodule "lib/external"]
    path = lib/external
    url = https://github.com/vendor/lib.git
    branch = main

[submodule "tools/helper"]
    path = tools/helper
    url = https://github.com/vendor/helper.git
PHP
$content = file_get_contents('.gitmodules');
$submodules = SubmoduleManager::parseGitmodules($content);

foreach ($submodules as $sub) {
    echo "{$sub->name} -> {$sub->url}\n";
}

O analisador lida com:

  • Cabeçalhos de seção: [submodule "<name>"]
  • Pares de chave e valor: path = ..., url = ..., branch = ...
  • Comentários (linhas que começam com #)
  • Espaços em branco e indentação

Entradas sem path e url são ignoradas. Os valores não são removidos das aspas.

Entradas gitlink são entradas de árvore com modo 160000. Elas apontam para um commit (não uma árvore ou blob) no repositório do submódulo.

PHP
$tree = $repo->readObject($treeHash);

foreach ($tree->entries as $entry) {
    if ($entry->isGitlink()) {
        echo "Submodule at {$entry->name}: commit {$entry->hash->hex}\n";
    }
}

Detecção de TreeEntry

PHP
$entry->isGitlink();  // true if mode is '160000'
$entry->mode;         // '160000'

Inicializar submódulos

PHP
$manager->init();

Para cada submódulo declarado em .gitmodules, isso:

  1. Cria um diretório .git/modules/<name>/ vazio
  2. Cria o diretório do caminho do submódulo, se ele ainda não existir
  3. Registra submodule.<name>.active = true e submodule.<name>.url em .git/config

Isso não busca nem verifica nada e não grava o arquivo .git do submódulo.

Atualizar submódulos

PHP
$manager->update($repo->head()->tree);

Para cada submódulo fixado na árvore indicada, update() copia o diretório git do repositório de origem para .git/modules/<name>/, substitui os arquivos de trabalho do submódulo pelos da origem, grava o arquivo .git apontando para o diretório do módulo, define core.worktree e desanexa HEAD no commit fixado.

O URL do submódulo precisa ser um caminho local: absoluto ou relativo à árvore de trabalho do pai (git resolve URLs relativas com base no remoto URL do pai). Um URL com esquema (https://...) lança RuntimeException('Submodule update currently supports only local paths: ...').

Após uma atualização, você pode abrir cada submódulo como um repositório separado:

PHP
$subRepo = Pitmaster::open($repo->workDir() . '/lib/external');

Verificar commits fixados

PHP
$headCommit = $repo->head();
$pinnedId = $manager->pinnedCommit('lib/external', $headCommit->tree);

if ($pinnedId !== null) {
    echo "lib/external is pinned to commit {$pinnedId->hex}\n";
}

O método pinnedCommit() percorre a hierarquia da árvore para encontrar a entrada gitlink no caminho indicado e retorna o hash do commit de destino.

Status do submódulo

PHP
$headCommit = $repo->head();
$statuses = $manager->status($headCommit->tree);

foreach ($statuses as $status) {
    $expected = $status['expected'] ?? 'not pinned';
    $actual = $status['actual'] ?? 'not initialized';
    $dirty = $status['dirty'] ? ' (dirty)' : '';

    echo "{$status['name']}: expected={$expected}, actual={$actual}{$dirty}\n";
}

Campos de status:

  • name: nome do submódulo
  • path: caminho na árvore de trabalho
  • expected: hash do commit fixado na árvore pai (null se não estiver presente)
  • actual: HEAD atual do submódulo (null se não inicializado)
  • dirty: true quando ambos os commits são conhecidos e diferem. Alterações dentro da árvore de trabalho do submódulo não são verificadas.

Estrutura de diretórios

Código
project/
  .git/
    modules/
      lib/
        external/          # Submodule's actual git directory (.git/modules/<name>)
        HEAD
        objects/
        refs/
        ...
  .gitmodules              # Submodule declarations
  lib/
    external/
      .git                 # File: "gitdir: ../../.git/modules/lib/external"
      src/
      README.md
  src/
  README.md

Fluxo de trabalho com submódulos

PHP
// 1. Open the parent repo
$repo = Pitmaster::open('/path/to/project');

// 2. List submodules
$manager = new SubmoduleManager(
    $repo->objectDatabase(),
    $repo->workDir(),
    $repo->gitDir(),
);

$submodules = $manager->list();

// 3. Populate the submodules pinned at HEAD (local source paths only)
$manager->update($repo->head()->tree);

// 4. Check status
$statuses = $manager->status($repo->head()->tree);

// 5. Open a submodule as its own repo
$subRepo = Pitmaster::open($repo->workDir() . '/lib/external');
$subHead = $subRepo->head();
echo "Submodule HEAD: {$subHead->id->hex}\n";