Hook sisi klien berhenti berkuasa begitu penulisnya menghapusnya — dan tidak ada yang menghapusnya lebih sering daripada orang yang sedang diburu deadline. Untuk aturan yang tidak bisa ditawar, Git menyediakan hooks yang berjalan di server: skrip di dalam repository yang menerima push, dieksekusi sebelum atau sesudah refs diperbarui, dan tidak bisa dihindari kontributor mana pun. Inilah lapisan penegakan yang dipakai hosting platform di balik protected branches — dibahas konsepnya di Protected Branches
. Chapter ini membangun server mini milik sendiri, lalu membuktikan ketiga hook utamanya: pre-receive, update, dan post-receive.
Menyiapkan Server Mini #
Server Git paling sederhana adalah bare repository — repository tanpa working tree yang tugasnya hanya menerima push, persis pola yang kita pakai sepanjang pembahasan remote. Bare repo tetap punya folder hooks/ di dalam .git-nya; bedanya, berkas di situ dieksekusi untuk setiap push yang datang dari siapa pun:
mkdir latihan-server && cd latihan-server
git init --bare pusat.git
mkdir budi && cd budi
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
git remote add origin ../pusat.git
printf 'beranda\n' > situs.md
git add situs.md
GIT_AUTHOR_DATE="2026-11-06T10:00:00+07:00" GIT_COMMITTER_DATE="2026-11-06T10:00:00+07:00" \
git commit -m "Buat halaman beranda"pre-receive: Gerbang Seluruh Push #
Hook pre-receive berjalan sekali per operasi push, sebelum satu pun ref diperbarui. Daftar refs yang akan berubah datang lewat stdin — satu baris per ref dengan format old new ref — dan satu exit 1 menolak seluruh push. Versi paling sederhananya menolak apa saja:
cd ../pusat.git
cat > hooks/pre-receive <<'HOOK'
#!/bin/sh
echo "pre-receive: server sedang maintenance, push ditolak" >&2
exit 1
HOOK
chmod +x hooks/pre-receive
cd ../budi
git push -u origin mainOutput yang Dihasilkan (perintah terakhir):
remote: pre-receive: server sedang maintenance, push ditolak
To ../pusat.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '../pusat.git'Tiga baris terakhir layak dibedah satu per satu karena ketiganya adalah tanda tangan penolakan server:
remote: pre-receive: ...— keluaran hook di server, diteruskan ke terminal klien dengan prefiksremote:. Spasi-spasi di ujung baris bawaan Git untuk merapikan tampilan; inilah satu-satunya cara pesan dari server sampai ke pengepush.! [remote rejected] main -> main (pre-receive hook declined)— penolakan terjadi di server, bukan di klien. Tanda seru menandai kegagalan, dan(pre-receive hook declined)menyebut penolaknya.error: failed to push some refs to ...— ringkasan akhir bahwa tidak ada refs yang berhasil.
Bandingkan dengan blokir pre-push dari chapter sebelumnya: keluarannya mirip, tapi penolakan pre-push berasal dari mesin sendiri — bisa dihapus. Penolakan ini berasal dari server — satu-satunya cara lolos adalah meyakinkan server, bukan menyembunyikan sesuatu dari laptop sendiri.
Menolak Push Langsung ke main #
Kekuatan pre-receive jadi berguna saat ia membaca stdin. Skrip berikut menolak setiap dorongan langsung ke branch main — fondasi paling primitif dari protected branch:
cd ../pusat.git
cat > hooks/pre-receive <<'HOOK'
#!/bin/sh
# Tolak setiap dorongan langsung ke branch main.
while read old new ref; do
if [ "$ref" = "refs/heads/main" ]; then
echo "pre-receive: push langsung ke main ditolak — kirim lewat pull request" >&2
exit 1
fi
done
HOOK
chmod +x hooks/pre-receive
cd ../budi
git push -u origin mainOutput yang Dihasilkan (perintah terakhir):
remote: pre-receive: push langsung ke main ditolak — kirim lewat pull request
To ../pusat.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '../pusat.git'Skripnya bekerja baris per baris begini:
while read old new refmembaca stdin baris demi baris; tiap baris berisi hash lama, hash baru, dan nama ref yang akan diperbarui.if [ "$ref" = "refs/heads/main" ]mencocokkan nama ref — branch bernamamaindi server selalu ditulis penuh sebagairefs/heads/main.exit 1menolak seluruh push begitu satu ref bermasalah ditemukan — pre-receive bersifat semua-atau-tidak-sama-sekali.
Branch lain tetap bebas. Budi mengirim pekerjaannya lewat branch fitur, dan server meloloskannya:
git switch -c fitur-kontak
printf 'kontak\n' > kontak.md
git add kontak.md
GIT_AUTHOR_DATE="2026-11-06T10:10:00+07:00" GIT_COMMITTER_DATE="2026-11-06T10:10:00+07:00" \
git commit -m "Tambah halaman kontak"
git push -u origin fitur-kontakOutput yang Dihasilkan (perintah terakhir):
To ../pusat.git
* [new branch] fitur-kontak -> fitur-kontak
branch 'fitur-kontak' set up to track 'origin/fitur-kontak'.Varian pre-receive lain — misalnya yang menolak penulisan ulang riwayat lewat git merge-base --is-ancestor — sudah kita bangun dan uji di chapter Protected Branches
; kombinasi larangan push langsung dan larangan force push adalah pasangan standar proteksi branch.
update: Keputusan Per Ref #
Hook update berjalan sekali per ref dengan tiga argumen: nama ref ($1), hash lama ($2), hash baru ($3). Perbedaan pentingnya dari pre-receive: penolakan update hanya menjatuhkan ref yang bermasalah — ref lain dalam push yang sama tetap diperbarui. Kita buktikan dengan mendorong dua branch sekaligus:
cd ../pusat.git
rm hooks/pre-receive
cat > hooks/update <<'HOOK'
#!/bin/sh
ref="$1"
old="$2"
new="$3"
if [ "$ref" = "refs/heads/main" ]; then
echo "update: $ref ditolak" >&2
exit 1
fi
echo "update: $ref lolos ($old -> $new)" >&2
HOOK
chmod +x hooks/update
cd ../budi
git switch main
git switch -c fitur-lain
printf 'faq\n' > faq.md
git add faq.md
GIT_AUTHOR_DATE="2026-11-06T10:20:00+07:00" GIT_COMMITTER_DATE="2026-11-06T10:20:00+07:00" \
git commit -m "Tambah FAQ"
git push origin main fitur-lainOutput yang Dihasilkan (perintah terakhir):
remote: update: refs/heads/main ditolak
remote: error: hook declined to update refs/heads/main
remote: update: refs/heads/fitur-lain lolos (0000000000000000000000000000000000000000 -> f57b18a13eafc5b9944d8a2e648a5324743dc1fa)
To ../pusat.git
* [new branch] fitur-lain -> fitur-lain
! [remote rejected] main -> main (hook declined)Deretan nol pada baris ketiga adalah hash lama branch baru — belum ada isinya di server. Baris keempat menunjukkan hasilnya: fitur-lain berhasil dibuat, main ditolak — dalam satu operasi push. Hook update adalah pilihan tepat bila aturan berlaku per-branch, sementara pre-receive untuk aturan yang membatalkan semuanya.
post-receive: Setelah Refs Diperbarui #
Hook post-receive berjalan setelah refs diperbarui — penolakan sudah tidak mungkin, karena pekerjaannya justru tindak lanjut: mengirim notifikasi, memicu CI, memicu deploy, mencatat audit. Masukannya format stdin yang sama dengan pre-receive:
cd ../pusat.git
rm hooks/update
cat > hooks/post-receive <<'HOOK'
#!/bin/sh
while read old new ref; do
echo "post-receive: $ref kini di $new" >&2
done
HOOK
chmod +x hooks/post-receive
cd ../budi
git push origin mainOutput yang Dihasilkan (perintah terakhir):
remote: post-receive: refs/heads/main kini di 5518219c4e78032d8198dc1eaa0c9dc8c01fe869
To ../pusat.git
* [new branch] main -> mainHash 5518219c berbeda di mesin pembaca. Di dunia nyata, skrip pada posisi ini yang men-schedule job build, membuka tiket deploy, atau memanggil webhook platform — tiga pola yang kita sambungkan kembali di chapter Git dalam CI/CD
.
| Hook server | Momen | Masukan | Efek exit non-zero |
|---|---|---|---|
pre-receive | sebelum refs diperbarui, sekali per push | refs di stdin | seluruh push ditolak |
update | sebelum tiap ref diperbarui | $1 ref, $2 old, $3 new | hanya ref itu yang ditolak |
post-receive | setelah refs diperbarui | refs di stdin | tidak memblokir — hanya kode keluar skrip |
Server kini punya gerbang sungguhan. Tapi perhatikan beban yang baru saja kita pikul: setiap push menjalankan skrip shell di server, dan skrip di sana menentukan nasib seluruh tim — server yang di-hosting sendiri berarti tim juga memelihara hook-nya sendiri. Sebelum membahas beban itu, kita kembali dulu ke sisi klien untuk membangun hook yang paling sering dipakai sehari-hari — pre-commit — dalam bentuk produksi yang lengkap.