Tim platform memelihara pustaka utilitas di repository sendiri, sementara aplikasi toko memakai pustaka itu dan ingin memastikan setiap developer — serta pipeline build — membangun dengan versi yang sama persis. Menyalin kode pustaka ke setiap aplikasi berarti pembaruan manual; menjadikannya satu repository berarti riwayat dua proyek berbeda irama tercampur. Submodule menawari jalan tengah: repository pustaka hadir sebagai direktori di dalam repository aplikasi, dan yang dicatat aplikasi hanyalah satu commit pustaka — sematan yang berganti hanya saat kita sengaja menaikkannya. Bab ini menyematkan, meng-clone, memperbarui, dan melepas submodule sungguhan, lengkap dengan dua jebakan yang paling sering menjatuhkan orang.

Dua Repository: Pustaka dan Aplikasi #

Sandbox menyediakan dua repository lokal yang berdiri bersebelahan:

bash
mkdir latihan-pustaka && cd latihan-pustaka
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
printf 'pustaka utilitas toko v1\n' > README.md
printf 'def sapa():\n    return "halo"\n' > util.py
git add -A
git commit -m "Rilis pustaka v1"

Aplikasi dibuat dengan cara yang sama di direktori latihan-aplikasi, berisi satu README.md dan commit “Awal aplikasi”. Keduanya adalah repository Git biasa tanpa hal khusus — sifat submodule muncul ketika keduanya dijahit.

Pagar Keamanan Protokol file #

Mencoba langsung menyematkan pustaka ber-path lokal menghasilkan kegagalan yang informatif:

bash
git submodule add ../latihan-pustaka libs/pustaka

Output yang Dihasilkan:

text
Cloning into '/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-aplikasi/libs/pustaka'...
fatal: transport 'file' not allowed
fatal: clone of '/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-pustaka' into submodule path '/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-aplikasi/libs/pustaka' failed

Jalur pada keluaran di atas mengikuti lokasi sandbox dan akan menyesuaikan di mesin pembaca. Sejak Git 2.38.1, pengklonan submodule melalui protokol file ditolak secara bawaan — penutupan celah keamanan terhadap berkas .gitmodules berbahaya yang bisa memerintahkan Git membaca path lokal sewenang-wenang. Sandbox yang memang memakai path lokal melewati pagar ini dengan izin eksplisit sekali jalan:

bash
git -c protocol.file.allow=always submodule add ../latihan-pustaka libs/pustaka

Output yang Dihasilkan:

text
Cloning into '/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-aplikasi/libs/pustaka'...
done.

Pada remote HTTPS sungguhan — kasus normal di GitHub atau GitLab — pagar ini tidak pernah muncul.

Perintah add melakukan tiga hal sekaligus: meng-clone pustaka ke libs/pustaka, menulis berkas .gitmodules, dan menambahkan satu entri khusus ke index. Isi .gitmodules pantas dibaca utuh:

ini
[submodule "libs/pustaka"]
	path = libs/pustaka
	url = ../latihan-pustaka

Berkas ini adalah peta bagi siapa pun yang meng-clone aplikasi: nama submodule, lokasinya, dan dari mana mengambilnya. URL relatif seperti ../latihan-pustaka akan di-resolve terhadap URL remote superproject — pola yang nyaman untuk repositori bersaudara di satu server. Sementara itu, index menyimpan sesuatu yang berbeda dari semua bab sebelumnya:

bash
git ls-files -s

Output yang Dihasilkan:

text
100644 7d5ad918cf4c152856cf01bdea1f5cba664c72c3 0	.gitmodules
100644 eb8c99953f0d821386a0e160a1f95a2eeb2e85eb 0	README.md
160000 caef2a69e2a77a104cf0289d218c7d28b0ae6d42 0	libs/pustaka

Dua baris pertama adalah pola lama: mode 100644, hash blob, stage. Baris ketiga memakai mode 160000 — kode khusus yang oleh Git disebut gitlink: hash yang dirujuk bukan blob di repository aplikasi, melainkan commit di repository pustaka. Pohon commit memperlihatkan hal yang sama dari sisi objek:

bash
git ls-tree HEAD libs/pustaka

Output yang Dihasilkan:

text
160000 commit caef2a69e2a77a104cf0289d218c7d28b0ae6d42	libs/pustaka

Hash akan berbeda di mesin pembaca, tetapi maknanya tetap: superproject menyematkan pustaka pada commit caef2a6 — bukan pada branch, bukan pada “terbaru”, melainkan satu titik pasti. Status submodule meringkasnya:

text
 caef2a69e2a77a104cf0289d218c7d28b0ae6d42 libs/pustaka (heads/main)

Spasi di depan berarti sematan bersih; git submodule status menempatkan tanda + bila checkout submodule lebih baru dari sematan dan - bila belum diinisialisasi. Satu detail arsitektur lagi: .git di dalam direktori submodule bukan direktori, melainkan berkas penunjuk:

text
gitdir: ../../.git/modules/libs/pustaka

Repository pustaka sesungguhnya hidup di .git/modules/ milik superproject — trik yang sama yang akan kita temui lagi di git worktree . Setelah semuanya siap, sematan di-commit:

bash
git add .gitmodules
git commit -m "Sematkan pustaka"

Output yang Dihasilkan:

text
[main d9adeca] Sematkan pustaka
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 libs/pustaka

Baris create mode 160000 menegaskan yang di-commit adalah gitlink, bukan isi pustaka.

Clone: Kosong Sampai Diinisialisasi #

Apa yang diterima orang yang meng-clone aplikasi? Git sengaja tidak mengunduh submodule secara otomatis:

bash
cd ..
git clone latihan-aplikasi klon-aplikasi
ls klon-aplikasi
ls klon-aplikasi/libs/pustaka

Output yang Dihasilkan:

text
README.md
libs
ls libs/pustaka:

Direktori libs hadir tetapi kosong. Perintah satu arus membangunkannya:

bash
cd klon-aplikasi
git submodule update --init

Output yang Dihasilkan:

text
Submodule 'libs/pustaka' (/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-pustaka) registered for path 'libs/pustaka'
Cloning into '/home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/klon-aplikasi/libs/pustaka'...
done.
Submodule path 'libs/pustaka': checked out 'caef2a69e2a77a104cf0289d218c7d28b0ae6d42'

Flag --init mendaftarkan submodule dari .gitmodules sekali gus sebelum meng-clone dan checkout sematan caef2a6. Klon baru sejak awal juga bisa melakukannya sekaligus: git clone --recurse-submodules latihan-aplikasi klon-rekursif menjalankan urutan yang sama selama proses klon, dan ls klon-rekursif/libs/pustaka menampilkan README.md serta util.py langsung tersedia.

Satu kejutan menanti siapa yang masuk ke dalam submodule:

bash
git -C libs/pustaka status

Output yang Dihasilkan:

text
HEAD detached at caef2a6
nothing to commit, working tree clean

HEAD detached adalah perilaku yang disengaja: sematan menunjuk commit, bukan branch, sehingga checkout submodule memang berdiri di titik lepas. Kita bebas berpindah branch di dalamnya, tetapi status default superproject selalu membaca commit mana yang sedang dicheckout.

Memperbarui Sematan #

Pustaka melaju: upstream merilis v2 dengan README baru di latihan-pustaka. Aplikasi menaikkan sematannya dari dalam klon:

bash
cd ../klon-rekursif
git -C libs/pustaka switch main
git -C libs/pustaka pull
git status

Output yang Dihasilkan (switch):

text
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

Output yang Dihasilkan (pull):

text
From /home/z/my-project/tutorial-extracted/work/sandbox/mengelola-file-dan-repository/part-b/latihan-pustaka
   caef2a6..2f204ba  main       -> origin/main
Updating caef2a6..2f204ba
Fast-forward
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Output yang Dihasilkan (status):

text
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   libs/pustaka (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

Frasa libs/pustaka (new commits) adalah cara Git berkata: checkout submodule kini berdiri di commit lain, dan superproject menawarkan pencatatan itu sebagai perubahan. Penawaran diterima seperti perubahan biasa:

bash
git add libs/pustaka
git commit -m "Naikkan pustaka ke v2"
git ls-files -s libs/pustaka

Output yang Dihasilkan:

text
[main 88d53ee] Naikkan pustaka ke v2
 1 file changed, 1 insertion(+), 1 deletion(-)

Output yang Dihasilkan (ls-files -s):

text
160000 2f204baa1f7752ae293617dcb8fb3fff5f89cfcb 0	libs/pustaka

Gitlink bergerak dari caef2a6 ke 2f204ba — seluruh “pembaruan dependensi” di superproject sesungguhnya hanya mengganti satu baris sematan.

Melepas Submodule #

Melepas dilakukan dua tahap supaya bersih. git submodule deinit mengosongkan direktori dan mencabut pendaftaran:

bash
git submodule deinit -f libs/pustaka

Output yang Dihasilkan:

text
Cleared directory 'libs/pustaka'
Submodule 'libs/pustaka' (../latihan-pustaka) unregistered for path 'libs/pustaka'

Lalu git rm menghapus gitlink dari index beserta berkas .gitmodules dan sisa metadata di .git/modules:

bash
git rm -f libs/pustaka
git status --short

Output yang Dihasilkan:

text
rm 'libs/pustaka'

Output yang Dihasilkan (status):

text
M  .gitmodules
D  libs/pustaka

Penghapusan tinggal di-commit seperti biasa. Tanpa git rm, sisa direktori .git/modules/libs/pustaka dan entri submodule.libs/pustaka.* di konfigurasi lokal akan mengganggu penyematan ulang di masa depan — Pro Git bab Git Tools menyarankan urutan deinit-rm-commit ini, dan dokumentasi git submodule merinci tiap langkahnya.

Jebakan Klasik Submodule #

Submodule juga bukan manajer dependensi: tidak ada batasan versi, tidak ada resolusi konflik otomatis antar dua sematan, dan setiap pembaruan adalah commit tersendiri di superproject. Bila kebutuhan hanya berbagi kode, registri paket bahasa atau sparse checkout di atas monorepo sering lebih sederhana.

Submodule menjahit banyak repository menjadi satu pohon direktori. Teknik penutup bab section ini bekerja ke arah sebaliknya — satu repository dibuka menjadi banyak direktori kerja sekaligus: git worktree .