Git dari dalam: objek, ref, reflog, dan mengapa riwayat sulit benar-benar dihapus
Cara Git menyimpan data sebagai objek beralamat hash (blob, tree, commit, tag), apa itu ref dan reflog, cara memulihkan commit yang 'hilang', mengapa commit yang dihapus masih bisa ditemukan, risiko SHA-1, dan cara menandatangani commit dengan kunci SSH.
- git
- keamanan
- forensik-digital
Kebanyakan developer memakai Git setiap hari lewat segelintir perintah: add, commit, push, pull. Itu cukup sampai ada yang salah: commit hilang setelah reset --hard, rahasia terlanjur di-push, atau rekan tim bertanya apakah commit atas nama Anda benar-benar dari Anda. Pada saat seperti itu, memahami cara Git menyimpan data jauh lebih berguna daripada menghafal perintah.
Tulisan ini membuka folder .git/: bagaimana data disimpan, apa yang sebenarnya terjadi saat kita "menghapus" sesuatu, dan apa akibatnya bagi keamanan.
Sekilas sejarah
Git ditulis Linus Torvalds pada April 2005, setelah lisensi gratis BitKeeper yang dipakai untuk mengembangkan kernel Linux dicabut. Targetnya jelas: cepat, terdistribusi penuh, dan sulit dirusak diam-diam. Dalam beberapa minggu Git sudah dipakai untuk kernel Linux, lalu pemeliharaannya diserahkan ke Junio Hamano, yang masih memimpin proyek ini sampai sekarang.
Tujuan "sulit dirusak diam-diam" itulah yang membentuk seluruh desain Git: setiap potong data diberi nama berdasarkan hash isinya.
Empat jenis objek
Di dalam .git/objects/ hanya ada empat jenis objek:
| Objek | Isi |
|---|---|
| blob | isi satu file, tanpa nama file dan tanpa izin akses |
| tree | daftar isi satu folder: nama, mode, dan hash blob atau tree di dalamnya |
| commit | hash satu tree (snapshot proyek), hash commit induk, author, committer, waktu, dan pesan |
| tag | tag beranotasi: menunjuk ke objek lain, beserta nama pembuat, pesan, dan tanda tangan opsional |
Nama setiap objek adalah hash SHA-1 dari header <jenis> <ukuran>\0 diikuti isinya. Kita bisa membuktikannya sendiri:
$ printf 'halo\n' | git hash-object --stdin
1185b465e5d77cbaf348effaf07b801e63998d17
$ printf 'blob 5\0halo\n' | sha1sum
1185b465e5d77cbaf348effaf07b801e63998d17 -Siapa pun, di komputer mana pun, yang menyimpan file berisi halo akan mendapat hash yang sama. Inilah content-addressable storage: alamat ditentukan oleh isi.
Sekarang buat satu commit dan lihat isinya dengan git cat-file:
$ git cat-file -p HEAD
tree 17825fadd92192535884f4f84a3b8aee40b3bb50
author Delfi <delfi@example.com> 1793674800 +0700
committer Delfi <delfi@example.com> 1793674800 +0700
Tambah salam
$ git cat-file -p HEAD^{tree}
100644 blob 1185b465e5d77cbaf348effaf07b801e63998d17 salam.txt
$ find .git/objects -type f
.git/objects/3f/1712761f2d9d3162b67061ff21c1b1146f876c
.git/objects/17/825fadd92192535884f4f84a3b8aee40b3bb50
.git/objects/11/85b465e5d77cbaf348effaf07b801e63998d17Tiga objek: commit, tree, dan blob. Masing-masing disimpan terkompresi zlib dalam folder bernama dua karakter pertama hash-nya. Commit pertama tidak punya baris parent; commit berikutnya punya satu, dan commit hasil merge punya dua atau lebih.
Rantai ini membuat riwayat Git berbentuk Merkle DAG. Hash commit bergantung pada hash tree, yang bergantung pada hash setiap blob, dan juga pada hash commit induknya. Mengubah satu byte di file lama berarti mengubah hash blob, tree, commit tempatnya berada, dan semua commit sesudahnya. Karena itu satu hash commit cukup untuk menjamin integritas seluruh riwayat di belakangnya, dan karena itu pula "mengubah riwayat" di Git selalu berarti membuat commit-commit baru.
Satu hal yang sering terlewat: author dan committer hanyalah teks yang ditulis sendiri oleh pembuat commit. Siapa pun bisa menjalankan git -c user.name="Linus Torvalds" -c user.email=... commit. Hash menjamin isi tidak berubah, bukan siapa penulisnya. Kita kembali ke soal ini di bagian tanda tangan.
Ref, HEAD, dan cabang yang ternyata cuma file
Hash 40 karakter sulit diingat, jadi Git memberinya nama lewat ref. Sebuah cabang hanyalah file teks berisi satu hash:
$ cat .git/HEAD
ref: refs/heads/main
$ cat .git/refs/heads/main
3f1712761f2d9d3162b67061ff21c1b1146f876cHEAD menunjuk ke cabang aktif, dan cabang menunjuk ke commit. Membuat cabang baru berarti menulis file 41 byte, itulah sebabnya cabang di Git sangat murah. Saat Anda commit, Git membuat objek commit baru lalu menggeser penunjuk cabang ke sana. Ref yang jarang berubah bisa dipindahkan ke .git/packed-refs, dan Git versi baru juga mendukung format penyimpanan ref reftable, tetapi konsepnya tetap sama.
Dari sini muncul konsep penting: objek yang tidak bisa dicapai dari ref mana pun disebut unreachable. Objek itu tidak langsung hilang; ia hanya tidak lagi terlihat di git log.
Reflog: jaring pengaman lokal
Setiap kali HEAD atau sebuah cabang bergeser, karena commit, checkout, reset, rebase, atau amend, Git mencatatnya di reflog:
$ git commit --amend -m "Tambah salam!"
$ git reflog
db49bbc HEAD@{0}: commit (amend): Tambah salam!
3f17127 HEAD@{1}: commit (initial): Tambah salamCommit 3f17127 sudah "diganti" oleh amend, tetapi objeknya masih ada dan reflog masih mengingatnya. Hal yang sama berlaku setelah reset --hard yang keliru atau rebase yang berantakan:
git reflog # cari posisi sebelum kesalahan
git branch penyelamat HEAD@{1} # pasang cabang baru di sana
# atau langsung kembalikan:
git reset --hard HEAD@{1}Bila reflog pun sudah tidak membantu, misalnya stash yang di-drop atau cabang yang dihapus lama, cari objek yatim dengan git fsck:
git fsck --unreachable --no-reflogs | grep commit
git fsck --lost-found # salin ke .git/lost-found/
git show <hash> # periksa satu per satuReflog bersifat lokal: tidak ikut di-push dan tidak ada di clone baru. Secara bawaan, entri yang masih bisa dicapai disimpan 90 hari dan yang tidak bisa dicapai 30 hari (gc.reflogExpire dan gc.reflogExpireUnreachable). Setelah reflog kedaluwarsa, git gc akan membuang objek yatim yang lebih tua dari dua minggu (gc.pruneExpire). Jadi waktu untuk menyelamatkan pekerjaan cukup panjang, tetapi tidak selamanya.
Packfile: tempat objek tinggal lama
Menyimpan setiap versi file secara utuh terdengar boros. Git mengatasinya dengan packfile: saat git gc berjalan atau saat data dikirim lewat jaringan, objek-objek longgar digabung ke satu berkas .pack dan objek yang mirip disimpan sebagai delta dari objek lain.
git count-objects -vH
git verify-pack -v .git/objects/pack/pack-*.idx | sort -k3 -n | tail -5 # 5 objek terbesarPerintah kedua berguna untuk mencari file besar yang pernah ter-commit lalu dihapus: file itu sudah tidak ada di working tree, tetapi masih membuat clone repo terasa berat karena tersimpan di riwayat.
Kenapa "menghapus" di Git hampir tidak pernah benar-benar menghapus
Dari semua bagian di atas, kesimpulannya sama: Git dirancang untuk tidak kehilangan data. Itu bagus untuk kerja sehari-hari, tetapi buruk ketika yang perlu dihapus adalah kunci API atau file pribadi.
- Commit baru yang menghapus file tidak menyentuh blob lama. File itu masih bisa diambil dengan
git show <commit-lama>:path/ke/file. git resetataurebaselalupush --forcemenghilangkan commit dari cabang, tetapi objeknya masih ada di reflog lokal Anda, di clone setiap orang yang sempatfetch, di fork, di cache CI, dan di cermin seperti Software Heritage.- Di GitHub, commit yang sudah tidak dirujuk cabang mana pun sering masih bisa dibuka langsung lewat hash-nya. Pada 2024 Truffle Security menunjukkan bahwa karena fork berbagi satu jaringan penyimpanan objek, commit dari fork yang sudah dihapus, bahkan dari repo privat yang kemudian dipublikasikan, masih bisa diakses lewat repo induknya. GitHub menyatakan perilaku ini memang bagian dari desain.
Karena itu urutan penanganannya selalu sama, seperti yang dibahas di OPSEC untuk developer: cabut dan ganti rahasianya dulu, baru bersihkan riwayat dengan git filter-repo. Membersihkan riwayat merapikan repo; mencabut rahasia yang membuat kebocoran itu tidak berbahaya lagi.
SHA-1, SHAttered, dan SHA-256
Seluruh jaminan integritas di atas bergantung pada kekuatan fungsi hash. Pada Februari 2017 peneliti Google dan CWI Amsterdam mengumumkan SHAttered, tabrakan SHA-1 praktis pertama: dua PDF berbeda dengan hash yang sama. Secara teori, tabrakan seperti itu memungkinkan dua objek Git berbeda dengan nama yang sama.
Respons Git ada dua lapis:
- SHA-1DC. Sejak Git 2.13 (2017), Git memakai implementasi SHA-1 yang mendeteksi pola serangan tabrakan yang dikenal dan menolak objek yang mencurigakan. Hash yang dihasilkan tetap SHA-1, jadi semua repo lama tetap kompatibel.
- Format objek SHA-256. Sejak Git 2.29 repo bisa dibuat dengan
git init --object-format=sha256, dan proyek Git sudah merencanakan SHA-256 sebagai bawaan di Git 3.0. Kendalanya ada di ekosistem: belum semua layanan hosting dan alat mendukung repo SHA-256, dan repo SHA-1 tidak bisa diubah formatnya di tempat.
Untuk sebagian besar proyek, SHA-1DC sudah cukup. Tetapi ini juga pengingat bahwa tanda tangan kriptografis, bukan hash saja, yang membuktikan siapa yang membuat sebuah commit.
Menandatangani commit dengan kunci SSH
Sejak Git 2.34, commit dan tag bisa ditandatangani memakai kunci SSH yang kemungkinan besar sudah Anda punya, tanpa perlu menyiapkan GPG:
[user]
signingkey = ~/.ssh/id_ed25519.pub
[gpg]
format = ssh
[gpg "ssh"]
allowedSignersFile = ~/.config/git/allowed_signers
[commit]
gpgsign = true
[tag]
gpgsign = trueAgar Git bisa memverifikasi tanda tangan secara lokal, daftarkan kunci publik yang dipercaya di berkas allowed signers:
echo "delfi@example.com $(cat ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signers
git log --show-signature -1
git verify-commit HEADTanda tangan disimpan di header gpgsig objek commit, sehingga ikut menentukan hash commit. Di GitHub atau GitLab, unggah kunci yang sama sebagai signing key agar commit diberi label Verified. Untuk tim, aktifkan aturan cabang yang mewajibkan commit bertanda tangan, sehingga commit yang mengaku-ngaku sebagai orang lain langsung terlihat.
Perlu diingat batasnya: tanda tangan membuktikan bahwa pemegang kunci menyetujui commit itu, bukan bahwa kodenya aman. Bila kunci privat di laptop dicuri, penyerang juga bisa menandatangani. Lindungi kunci dengan passphrase, atau simpan di kunci keamanan perangkat keras (ssh-keygen -t ed25519-sk).
Ringkasan
- Git adalah basis data objek beralamat hash: blob (isi file), tree (folder), commit (snapshot + induk + metadata), dan tag.
- Cabang dan
HEADhanyalah penunjuk ke hash. Mengubah riwayat berarti membuat objek baru dan menggeser penunjuk. - Reflog dan
git fsckbisa memulihkan hampir semua kesalahan lokal, selama belum lewat masa kedaluwarsa dangit gc. - Apa pun yang pernah di-push harus dianggap sudah tersalin. Untuk rahasia: cabut dulu, bersihkan riwayat kemudian.
- Hash menjamin isi, bukan identitas. Untuk identitas, tandatangani commit dan tag.