Chapter File Besar ditutup dengan satu kesimpulan: kalau aset besar memang bagian sah dari proyek, mengabaikannya bukan pilihan. Git LFS (Large File Storage) membagi kerjanya: repository tetap menyimpan sejarah lengkap, isi berkas besar pindah ke penyimpanan terpisah, dan yang masuk ke commit hanyalah berkas pointer berukuran puluhan byte. Clone jadi ringan, riwayat tetap utuh, dan video 700 MB tidak lagi diunduh setiap kali anggota tim baru masuk. Bab ini membedah mekanismenya dari pemasangan sampai migrasi riwayat yang terlanjur penuh.

Memasang Git LFS dan Filternya #

Git LFS adalah program terpisah dari Git; versinya terlihat dari git lfs version:

bash
git lfs version

Output yang Dihasilkan:

text
git-lfs/3.7.0 (GitHub; linux amd64; go 1.24.4; git 92dddf56)

Perintah git lfs install menyambungkan Git LFS ke repository: keluarannya singkat tetapi efeknya dua lapis.

bash
git init -b main latihan-lfs
cd latihan-lfs
git config user.name "Budi Santoso"
git config user.email "[email protected]"
git lfs install --local

Output yang Dihasilkan:

text
Updated Git hooks.
Git LFS initialized.

Baris Updated Git hooks. berarti Git LFS menulis hook di .git/hooks — paling penting pre-push, yang mengunggah isi berkas LFS ke server sebelum git push berjalan. Baris Git LFS initialized. berarti konfigurasi filter dipasang; isi lengkapnya terlihat dengan git config --get-regexp '^filter\.lfs' | sort (diurutkan agar stabil):

text
filter.lfs.clean git-lfs clean -- %f
filter.lfs.process git-lfs filter-process
filter.lfs.required true
filter.lfs.smudge git-lfs smudge -- %f

Pasangan filter.lfs.clean dan filter.lfs.smudge adalah jantungnya: saat git add, clean mengubah isi berkas menjadi pointer; saat checkout, smudge mengembalikan isi asli. filter.lfs.process adalah mode komunikasi batch yang lebih cepat, dan filter.lfs.required true membuat Git gagal keras saat filter bermasalah — lebih aman daripada diam-diam menyimpan isi penuh.

Melacak Pola Berkas dengan git lfs track #

LFS tidak menebak berkas mana yang besar; kita mendaftarkan polanya, sama seperti pola .gitignore :

bash
git lfs track "*.png"
cat .gitattributes

Output yang Dihasilkan:

text
Tracking "*.png"

Output yang Dihasilkan (isi .gitattributes):

text
*.png filter=lfs diff=lfs merge=lfs -text

Git LFS menulis aturan ke berkas .gitattributes — setiap *.png kini membawa empat atribut. filter=lfs mengaktifkan pasangan clean/smudge. diff=lfs membuat git diff menampilkan ringkasan pointer, bukan mencoba membedah biner. merge=lfs memasang driver merge khusus: konflik dua versi diselesaikan memilih salah satu isi utuh, bukan menyambung teks. -text mematikan normalisasi baris baru yang justru merusak berkas biner. Status atribut satu berkas bisa diverifikasi:

bash
git check-attr filter diff merge text -- hero.png

Output yang Dihasilkan:

text
hero.png: filter: lfs
hero.png: diff: lfs
hero.png: merge: lfs
hero.png: text: unset

Pointer di Repository, Isi Asli di Penyimpanan LFS #

Mekanismenya kita uji. Berkas PNG 300.000 byte dibuat dari data acak berbenih tetap agar identik di mesin mana pun, lalu ditambahkan bersama .gitattributes:

bash
python3 -c "import random; random.seed(2026); open('hero.png','wb').write(random.randbytes(300000))"
git add .gitattributes hero.png
git commit -m "Tambah gambar hero"

Output yang Dihasilkan:

text
[main (root-commit) 668c6b8] Tambah gambar hero
 2 files changed, 4 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 hero.png

Hash commit di mesin pembaca akan berbeda karena bergantung pada waktu dan identitas. Yang menarik adalah isi hero.png menurut Git:

bash
git cat-file -p :hero.png

Output yang Dihasilkan:

text
version https://git-lfs.github.com/spec/v1
oid sha256:00505388913a71dc4d96c831c480a96f26240c8b0adbfb14c2040bbc4f640067
size 300000

Tiga baris ini adalah berkas pointer. version menunjukkan spesifikasi formatnya. oid berisi hash SHA-256 dari isi asli — 300.000 byte acak dirangkum menjadi sidik jari 64 karakter. size mencatat ukuran asli sehingga alat bisa menampilkan “300 KB” tanpa mengunduh. git show HEAD:hero.png menghasilkan tiga baris yang sama: yang tersimpan di commit memang pointer, bukan gambar. Isi aslinya ada di penyimpanan lokal LFS di dalam .git:

bash
find .git/lfs/objects -type f
git lfs ls-files

Output yang Dihasilkan:

text
.git/lfs/objects/00/50/00505388913a71dc4d96c831c480a96f26240c8b0adbfb14c2040bbc4f640067

Output yang Dihasilkan:

text
0050538891 * hero.png

Path objek memakai sharding dua-dua digit: dua karakter pertama oid (00) menjadi direktori, dua berikutnya (50) subdirektori, sisanya nama berkas. git lfs ls-files merangkum statusnya: potongan oid 0050538891 mengidentifikasi objek dan tanda * berarti isi asli ada di cache lokal. Bukti terakhir: hapus berkas fisik, checkout ulang — isi 300.000 byte pulih lewat filter smudge.

bash
rm hero.png
git checkout -- hero.png
stat -c '%s %n' hero.png

Output yang Dihasilkan:

text
300000 hero.png
AspekTanpa LFSDengan LFS
Isi di commitBlob penuh 300 KBPointer 131 byte
Revisi kedua berkasBlob 300 KB baruPointer baru + satu objek LFS
Ukuran git cloneSeluruh versi ikutPointer saja; isi diunduh sesuai kebutuhan
git diff“Binary files differ”Ringkasan perubahan pointer

Push dan Clone Mentransfer Objek LFS #

Bukti terpentingnya transfer antar repository. Kita buat gudang bare lokal sebagai pengganti server, sambungkan sebagai origin, lalu push:

bash
git init --bare -b main ../gudang-lfs.git
git remote add origin ../gudang-lfs.git
git push -u origin main

Output yang Dihasilkan:

text
To ../gudang-lfs.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Keluaran push tampak biasa, tetapi hook pre-push bekerja diam-diam: sebelum Git mengirim commit, git lfs memeriksa objek LFS yang dirujuk dan menyalinnya ke gudang. Buktinya ada di dalam gudang:

bash
find ../gudang-lfs.git/lfs/objects -type f

Output yang Dihasilkan:

text
../gudang-lfs.git/lfs/objects/00/50/00505388913a71dc4d96c831c480a96f26240c8b0adbfb14c2040bbc4f640067

Objek dengan oid yang sama kini tersimpan di sisi server. Pada transfer lewat jaringan, git-lfs juga mencetak baris kemajuan seperti Uploading LFS objects: 100% (1/1), 300 KB | 1.2 MB/s, done. — pada transfer lokal baris itu kadang muncul kadang tidak karena salinan selesai seketika. Sekarang pihak lain meng-clone:

bash
cd ..
git clone gudang-lfs.git klon-lfs
stat -c '%s %n' klon-lfs/hero.png

Output yang Dihasilkan:

text
Cloning into 'klon-lfs'...
done.

Output yang Dihasilkan:

text
131 hero.png

Klon berhasil, tetapi hero.png hanya berukuran 131 byte — pointer-nya. Klon segar memang belum mengaktifkan filter LFS, sehingga checkout menyimpan pointer apa adanya. Di mesin yang sudah menjalankan git lfs install global, smudge berjalan otomatis saat checkout. Di klon ini kita aktifkannya lokal lalu menarik isi:

bash
cd klon-lfs
git lfs install --local
git lfs pull
stat -c '%s %n' hero.png
git lfs ls-files

Output yang Dihasilkan:

text
Updated Git hooks.
Git LFS initialized.

Perintah git lfs pull menyelesaikan tanpa keluaran; hasilnya terlihat dari dua perintah berikutnya:

text
300000 hero.png
text
0050538891 * hero.png

Isi asli 300.000 byte pulih dari gudang lewat endpoint LFS, dan tanda * kembali hadir. Pada remote ber-path lokal, git-lfs menangani transfer lewat transfer agent standalone; server LFS sungguhan seperti milik GitHub dan GitLab berbicara lewat HTTPS di endpoint /info/lfs — mekanismenya sama, salurannya berbeda. Detail lengkap ada di situs resmi Git LFS dan repositori git-lfs di GitHub .

Migrasi Riwayat yang Terlanjur Penuh #

Bagaimana dengan repository yang sudah telanjur penuh blob besar? Repo latihan-migrasi meniru kondisi itu: banner 2 MB di-commit tanpa LFS, disusul satu commit README, dengan total object database 1,93 MiB. git lfs migrate import --everything menulis ulang seluruh riwayat: blob yang cocok pola diganti pointer, dan aturan .gitattributes ditambahkan otomatis.

bash
git lfs migrate import --include="*.png" --everything
git log --oneline

Output yang Dihasilkan:

text
Sorting commits: ..., done.
Rewriting commits: 100% (2/2), done.
Updating refs: ..., done.
Checkout: ..., done.

Output yang Dihasilkan:

text
ee0daa0 Tambah README
46ddeab Tambah banner tanpa LFS

Empat tahap keluaran pertama berjalan berurutan: mengurutkan commit terdampak, menulis ulang, memperbarui refs, lalu me-refresh working directory. Hash pada log kedua sudah berbeda dari commit asal — sifat penulisan ulang. Hasilnya bisa diverifikasi: git lfs ls-files menampilkan bc7b550702 * banner.png, git cat-file -p HEAD:banner.png menampilkan pointer ber-oid bc7b550702... berukuran 2.000.000 byte, dan git lfs checkout mencetak Checking out LFS objects: 100% (1/1), 2.0 MB | 0 B/s, done.

Namun git count-objects -vH langsung setelah migrasi masih menunjukkan sekitar 1,95 MiB — blob 2 MB lama belum terbuang! Objek itu kini tak terjangkau dari refs mana pun tetapi masih dilindungi reflog. Dua perintah housekeeping menghabisinya:

bash
git reflog expire --expire-unreachable=now --all
git gc --prune=now
git count-objects -vH

Output yang Dihasilkan:

text
count: 0
size: 0 bytes
in-pack: 7
packs: 1
size-pack: 1.88 KiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytes

Object database menyusut dari 1,93 MiB menjadi 1,88 KiB. Pemindaian blob terbesar ala chapter File Besar memastikan tak ada sisa: kandidat terbesar hanyalah pointer 132 banner.png.

Pointer di repository, isi di penyimpanan terpisah — repository kembali ramping walau asetnya besar. Meski begitu, clone tetap mengambil seluruh riwayat dan seluruh direktori; dua chapter berikutnya memangkas dua sisi itu, dimulai dari direktori mana yang layak hadir di working directory: Sparse Checkout .