Konvensi pesan commit paling rapi pun hanya sekuat kebiasaan pengetiknya. Format Conventional Commits
menuntut subjek berpola type(scope): deskripsi, tetapi Git sendiri tidak peduli — “update aja” tetap sah sebagai pesan. Hook commit-msg menutup celah itu: hook ini berjalan tepat setelah pesan ditulis dan sebelum commit lahir, membaca pesannya, dan menolak yang melanggar pola. Kita bangun validasinya lengkap — pola Conventional Commits plus batas panjang subjek — lalu mengujinya dengan pesan yang gagal dan pesan yang lolos.
Membaca Pesan dari Berkas $1 #
Mekanisme masukannya sudah diperkenalkan di Client-Side Hooks
: Git menyimpan draf pesan ke berkas sementara — biasanya .git/COMMIT_EDITMSG — lalu mengoper jalurnya sebagai $1. Satu jebakan layak dibuktikan lebih dulu: saat commit ditulis lewat editor, berkas itu masih memuat baris komentar berawalan # (petunjuk template dan status), dan baris-baris itu baru dibuang Git setelah hook selesai. Hook yang membaca baris pertama mentah akan salah mengira komentar sebagai subjek. Karena itu skrip kita menyaring komentar lebih dulu.
Skrip Validasi Conventional Commits #
mkdir latihan-commitmsg && cd latihan-commitmsg
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
cat > .git/hooks/commit-msg <<'HOOK'
#!/bin/sh
# commit-msg — validasi pesan Conventional Commits.
# $1 = berkas sementara (biasanya .git/COMMIT_EDITMSG) berisi
# pesan calon commit; baris komentar berawalan # masih ada.
berkas_pesan="$1"
subjek="$(grep -v '^#' "$berkas_pesan" | head -n 1)"
pola='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?: .+'
if ! printf '%s\n' "$subjek" | grep -qE "$pola"; then
{
echo "commit-msg: subjek tidak mengikuti Conventional Commits:"
echo " '$subjek'"
echo "format yang diterima: type(scope): deskripsi"
echo "type salah satu: feat fix docs style refactor perf test build ci chore revert"
} >&2
exit 1
fi
if [ "${#subjek}" -gt 72 ]; then
echo "commit-msg: subjek ${#subjek} karakter (maksimal 72): '$subjek'" >&2
exit 1
fi
HOOK
chmod +x .git/hooks/commit-msgBedah baris-per-barisnya:
berkas_pesan="$1"menyimpan jalur berkas pesan — satu-satunya argumen yang Git kirim.grep -v '^#' ... | head -n 1mengambil baris pertama yang bukan komentar; itulah subjek pesan.pola='^(feat|fix|...)...'adalah regex Conventional Commits: prefikstypedari daftar resmi,scopeopsional dalam kurung (huruf kecil, angka, tanda hubung), titik dua, spasi, lalu deskripsi. Daftar type ini persis tabel dari chapter Conventional Commits .printf '%s\n' "$subjek" | grep -qE "$pola"menguji subjek terhadap pola;-qmembuatgrepbisu karena kita hanya butuh exit code-nya.- Blok
{ ... } >&2mencetak penolakan beserta petunjuk perbaikan ke stderr — pesan kesalahan yang hanya berkata “salah” tanpa berkata “bagaimana benarnya” hanya melahirkan frustrasi. [ "${#subjek}" -gt 72 ]menghitung panjang subjek dan menjaga batas 72 karakter — angka yang sama dipakai alat commitlint di CI.
Uji: Pesan Ditolak, Pesan Lolos #
Pertama, pesan favorit semua orang yang sedang buru-buru:
printf 'pencarian v1\n' > cari.js
git add cari.js
git commit -m "update aja"Output yang Dihasilkan (perintah terakhir):
commit-msg: subjek tidak mengikuti Conventional Commits:
'update aja'
format yang diterima: type(scope): deskripsi
type salah satu: feat fix docs style refactor perf test build ci chore revertCommit batal, lengkap dengan alasan dan format yang benar — penulisnya tinggal mengetik ulang. Pesan yang benar lolos:
git commit -m "feat: tambah pencarian"Output yang Dihasilkan:
[main (root-commit) 26de84d] feat: tambah pencarian
1 file changed, 1 insertion(+)
create mode 100644 cari.jsHash 26de84d berbeda di mesin pembaca. Varian dengan scope juga sah — dan sekaligus menguji regex bagian kurungnya:
printf 'filter v1\n' > filter.js
git add filter.js
git commit -m "feat(filter): saring hasil"Output yang Dihasilkan:
[main 8f1a950] feat(filter): saring hasil
1 file changed, 1 insertion(+)
create mode 100644 filter.jsUji: Panjang Subjek #
Pemeriksaan kedua menjaga subjek tetap nyaman dibaca di git log --oneline. Subjek uji berikut 97 karakter:
git commit -m "feat(pencarian): tambah filter kategori hasil pencarian berdasarkan nama produk dan harga terkini"Output yang Dihasilkan:
commit-msg: subjek 97 karakter (maksimal 72): 'feat(pencarian): tambah filter kategori hasil pencarian berdasarkan nama produk dan harga terkini'Hook melaporkan jumlah pastinya — memotong subjek jadi pekerjaan penulis, bukan tebak-tebakan.
Berdampingan dengan Kerangka Tim #
Hook ini paling kuat saat berpasangan dengan alat bantu dari chapter Commit Conventions
: commit.template mengisi editor dengan kerangka type(scope): saat commit ditulis tanpa -m, dan hook menolak bila kerangka itu tidak diisi benar. Uji cepatnya memakai editor non-interaktif yang menyalin draf berisi komentar:
printf '# <type>(<scope>): <deskripsi>\n# type wajib: feat fix docs ...\n' > pesan-commit.txt
git config commit.template pesan-commit.txt
printf 'draf\n' > draf.md
git add draf.md
printf 'feat(pencarian): saring draf\n\n# template terisi\n' > /tmp/pesan-editor.txt
GIT_EDITOR="cp /tmp/pesan-editor.txt" git commitOutput yang Dihasilkan (perintah terakhir):
[main e9e2699] feat(pencarian): saring draf
1 file changed, 1 insertion(+)
create mode 100644 draf.mdSubjek di baris pertama lolos validasi; baris # template terisi diabaikan skrip persis seperti Git mengabaikannya saat menyimpan pesan.
Seperti semua hook klien, validasi ini hanya berdiri di mesin yang memasangnya — lapisan penegakan terakhir untuk pesan commit adalah CI, yang memeriksa pesan pada pull request di server. Sebelum sampai ke sana, satu hook pasca masih tersisa untuk dibangun: yang berjalan setelah merge, saat repository berubah tanpa kita mengetik apa pun.