Belajar Git
Version Control dan Workflow Tim
Git bukan sekadar tempat menyimpan kode, melainkan sistem koordinasi kerja tim: histori yang bisa diaudit, cabang untuk bekerja paralel, dan workflow yang menjaga main tetap sehat.
Kenapa Developer Harus Menguasai Git
Sistem koordinasi kerja, bukan sekadar simpan kode
Git bukan tempat menyimpan kode, melainkan sistem koordinasi kerja yang menjawab pertanyaan “siapa mengubah apa, kapan, dan kenapa” di sebuah tim.
Banyak orang pertama kali mengenal Git sebagai “tombol simpan ke cloud”. Anggapan itu menyesatkan karena membuat Git terasa seperti pengganti Google Drive. Kenyataannya Git adalah version control system terdistribusi: setiap perubahan dicatat sebagai titik histori yang punya penulis, waktu, dan pesan, lalu titik-titik itu dirangkai menjadi narasi proyek yang bisa diaudit, ditelusuri, dan diputar mundur kapan saja.
Pada proyek backend skincare github.com/kamu/skincare-backend, kamu tidak bekerja sendiri. Ada yang menggarap modul katalog produk, ada yang menggarap checkout, ada yang menambah migrasi database. Tanpa sistem koordinasi, dua orang yang menyentuh file internal/product/service.go akan saling menimpa. Git membuat pekerjaan paralel itu aman: tiap orang punya jalur sendiri, lalu hasilnya digabung dengan jejak yang jelas.
gitGraph commit id: "init produk" commit id: "tambah harga" branch feat/checkout checkout feat/checkout commit id: "draft keranjang" commit id: "validasi stok" checkout main commit id: "perbaiki kategori" merge feat/checkout id: "rilis v0.2"
Kerja paralel di repo bersama. Jalur feat/checkout berkembang sendiri lalu disatukan ke jalur utama tanpa menghapus jejak siapa pun.
Setidaknya ada enam peran yang Git mainkan setiap hari: version control (riwayat berversi), history (catatan kenapa kode jadi seperti sekarang), collaboration (banyak orang satu basis kode), rollback (kembali ke versi sehat saat ada bug), code review (perubahan ditinjau sebelum masuk), dan release tracking (menandai versi mana yang dirilis). Keenamnya saling menopang, dan semuanya hilang begitu kamu mengandalkan salin-tempel folder.
Git itu seperti save point game yang bisa kamu kunjungi ulang kapan saja, sekaligus jurnal yang mencatat siapa mengubah apa dan alasannya, sehingga seluruh tim membaca cerita yang sama.
Kalau dulu kamu menyimpan project-final-v2-fix.zip atau mengandalkan Undo editor yang hanya seumur sesi, Git menggantinya dengan histori penuh yang permanen, bisa dicari, dan bisa dibandingkan antar versi.
Mari rasakan langsung. Tiga perintah berikut membuat repo, menyetujui satu perubahan ke staging, lalu mengabadikannya sebagai commit pertama.
Terminalmkdir skincare-backend && cd skincare-backend git init echo "# Skincare Backend" > README.md git add README.md git commit -m "chore: inisialisasi proyek"
Setelah git commit, kamu sudah memiliki satu titik histori yang permanen. Mulai detik ini, setiap perubahan punya rumah yang aman dan setiap keputusan teknis punya jejak. Itulah fondasi cara tim software bekerja: bukan karena Git menyimpan file, melainkan karena Git mengoordinasikan manusia di sekitar file tersebut.
Mental Model Git
Git menyimpan snapshot, bukan diff; tiga area kerja
Kunci memahami Git adalah satu kalimat: tiap commit menyimpan snapshot utuh proyek, bukan daftar perubahan yang menumpuk.
Banyak sistem versi lama berpikir dalam diff: mereka menyimpan “baris 10 berubah, baris 22 dihapus” lalu menumpuknya. Git memilih cara berbeda. Saat kamu commit, Git memotret seluruh isi proyek pada momen itu dan menyimpannya sebagai snapshot. File yang tidak berubah tidak disalin ulang, melainkan ditunjuk kembali ke isi yang identik. Hasilnya histori terasa seperti rangkaian foto utuh, bukan tumpukan catatan tambal sulam.
Agar snapshot itu hemat dan konsisten, Git bersifat content-addressable: setiap potongan isi diberi nama dari hash isinya sendiri. Isi file mentah disimpan sebagai blob, struktur folder sebagai tree, dan satu commit menunjuk ke satu tree (snapshot proyek) plus pointer ke parent (commit sebelumnya). Karena nama objek berasal dari isinya, isi yang sama otomatis berbagi penyimpanan, dan perubahan sekecil apa pun menghasilkan hash berbeda yang langsung terdeteksi.
flowchart LR WT["Working tree<br/>(file di disk)"] -->|git add| ST["Staging area<br/>(index)"] ST -->|git commit| CM["Commit<br/>(snapshot + parent)"] C1["commit A"] --> C2["commit B"] --> C3["commit C (HEAD)"]
Dua sudut pandang. Atas: perjalanan satu perubahan dari disk ke commit. Bawah: commit bertaut ke parent membentuk rantai snapshot.
Soal nama objek, Git default memakai SHA-1 dan sedang dalam transisi resmi ke SHA-256. Untuk kerja harian perbedaannya tak terasa; yang penting dipahami adalah idenya: nama = hash dari isi. Itu yang membuat Git bisa memverifikasi integritas histori dan mendeteksi korupsi data.
Karena commit menyimpan snapshot lengkap plus pointer ke parent, berpindah dari satu versi ke versi lain hanya soal menggeser pointer dan menyusun ulang file, bukan memutar ulang ribuan diff satu per satu.
Git mengatur kerja lewat tiga area: working tree (file yang kamu edit di disk), staging area alias index (ruang tunggu tempat kamu memilih perubahan mana yang akan masuk commit berikutnya), dan repository (database objek di dalam .git tempat snapshot disimpan permanen). Detail mendalam tiga area ini dibahas di section 04; di sini cukup pahami bahwa staging memberimu kontrol untuk menyusun commit yang rapi, bukan asal melempar semua perubahan sekaligus.
Di frontend kamu terbiasa berpikir state sebagai snapshot tunggal yang berubah lewat aksi; histori Git memakai logika serupa di skala proyek, tiap commit adalah snapshot baru yang ditautkan ke snapshot sebelumnya.
Bayangkan kamera yang memotret seluruh proyek tiap kali kamu commit. Kamu tidak menyimpan “apa yang berubah”, kamu menyimpan “seperti apa proyek pada momen itu”, lengkap dengan tanggal di balik foto yang menunjuk foto sebelumnya.
Setup Git dan Repository Lokal
Identitas commit, config, dan isi folder .git
Sebelum commit pertama yang serius, luangkan lima menit menyetel identitas dan preferensi Git agar setiap jejak yang kamu tinggalkan benar dan konsisten.
Setiap commit mencatat siapa penulisnya. Bila identitas belum diset, commit-mu akan tertempel nama dan email asal yang membuat histori sulit ditelusuri dan riwayat kontribusi berantakan. Karena itu langkah pertama adalah menyetel user.name dan user.email secara global, lalu menentukan beberapa preferensi yang akan terasa setiap hari.
Terminalgit config --global user.name "Nama Kamu" git config --global user.email "kamu@contoh.com" git config --global init.defaultBranch main git config --global core.editor "code --wait" git config --global core.autocrlf input
Empat baris setelah identitas itu penting dipahami, bukan sekadar disalin. init.defaultBranch main menetapkan nama branch awal saat git init, mengikuti standar komunitas dan hosting modern yang memakai main. core.editor menentukan editor yang dibuka saat Git butuh pesan panjang (misalnya saat rebase interaktif); --wait memastikan Git menunggu sampai editor ditutup. core.autocrlf input mengatur line endings: ia menormalkan akhir baris ke gaya Unix saat menyimpan ke repo, penting di tim lintas sistem operasi agar diff tidak penuh perubahan palsu. Untuk autentikasi ke remote, credential helper menyimpan kredensial agar kamu tidak mengetik token berulang.
Kamu sudah biasa menaruh aturan tim di .eslintrc atau .prettierrc per proyek. Config Git serupa, hanya saja level —global berlaku untuk semua repo di mesinmu, dan config tanpa —global hanya untuk repo saat ini.
Setelah config beres, git init mengubah folder biasa menjadi repository dengan membuat subfolder .git. Folder inilah otak repo: ia menyimpan seluruh objek (blob, tree, commit), referensi branch, dan konfigurasi lokal.
- .git/
- HEAD
- config
- objects/
- refs/
- heads/
- tags/
Di dalam repo, setiap file berada di salah satu status lifecycle. File yang belum pernah Git lihat berstatus untracked; setelah git add, ia menjadi tracked dan masuk pengawasan Git. Jalankan git status kapan saja untuk membaca peta ini.
Terminalgit init git status
stateDiagram-v2 [*] --> Untracked: file baru Untracked --> Staged: git add Staged --> Committed: git commit Committed --> Modified: edit file Modified --> Staged: git add
Lifecycle file. Dari untracked sampai committed, lalu kembali berputar setiap kali file disunting.
Set init.defaultBranch main dan pakai user.email yang sama dengan akun hosting-mu di semua proyek, supaya kontribusi tercatat ke identitas yang benar dan branch awal seragam di tiap repo.
Seluruh histori lokal hidup di dalam folder .git. Menghapusnya membuat folder kembali jadi direktori biasa dan menghilangkan semua commit yang belum pernah didorong ke remote.
Tracking Perubahan
status, add, restore, diff: memilih perubahan dengan sadar
Git memisahkan perubahan menjadi tiga kondisi, dan justru di pemisahan itulah letak kendali kamu atas apa yang masuk ke sebuah commit.
Sebuah file yang kamu sentuh hidup di salah satu dari tiga kondisi: modified (sudah diubah di working tree tapi belum ditandai), staged (sudah dipilih ke staging area, siap dibungkus jadi commit), dan committed (sudah tersimpan permanen di repository). Staging area inilah yang sering diremehkan pemula: ia bukan sekadar formalitas sebelum commit, melainkan ruang untuk menyusun commit secara sadar, memilih perubahan mana yang layak masuk bersama dan mana yang ditunda.
Titik masuknya selalu git status. Perintah ini membaca ketiga area dan memberitahu kamu: file mana yang modified tapi belum staged, file mana yang sudah staged, dan file mana yang sama sekali belum dilacak (untracked). Biasakan menjalankannya sebelum dan sesudah git add, karena ia adalah kompasmu, bukan sekadar laporan.
Terminal$ git status On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: internal/product/handler.go 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: internal/product/service.go
Untuk memindahkan perubahan ke staging, git add <path>. Untuk membatalkan, ada dua arah yang sering tertukar. git restore <file> membuang perubahan di working tree (mengembalikan file ke kondisi terakhir di index, perubahanmu hilang). Sementara git restore --staged <file> hanya meng-unstage: perubahan tetap ada di working tree, hanya dikeluarkan dari staging. Perhatikan perbedaannya: yang satu membuang isi, yang satu hanya menarik dari antrean commit.
git restore service.go menimpa file dengan versi index dan tidak masuk reflog, jadi perubahan working tree yang belum di-commit benar-benar lenyap. Pastikan kamu memang ingin membuangnya.
Dua perintah diff menjawab dua pertanyaan berbeda. git diff menunjukkan selisih working tree vs index: perubahan yang sudah kamu buat tapi belum di-stage. git diff --staged menunjukkan index vs HEAD: persis apa yang akan tercatat bila kamu commit sekarang. Membaca keduanya sebelum commit adalah kebiasaan yang memisahkan commit rapi dari commit berantakan.
Sama seperti kamu memindai diff di tab Source Control sebelum stage, git diff dan git diff —staged adalah versi terminal yang portabel, bisa di-pipe, dan tidak bergantung pada editor mana pun.
Mari praktik. Ubah dua file, stage satu, lalu bandingkan kedua diff sebelum commit.
Terminal$ git add internal/product/handler.go # stage satu file saja $ git diff # sisa yang BELUM di-stage (service.go) $ git diff --staged # yang AKAN masuk commit (handler.go) $ git commit -m "Validate price is positive in product handler"
git add -p menampilkan tiap hunk dan bertanya y/n, sehingga satu file yang berisi dua perubahan tak berkaitan bisa dipecah jadi dua commit terpisah. Hindari refleks git add titik yang menyapu semuanya tanpa kamu lihat.
Commit yang Baik
Atomic, pesan jelas, why over what
Commit yang baik bukan tentang menyimpan kode, melainkan menulis catatan yang masih masuk akal saat dibaca enam bulan kemudian oleh orang yang lupa konteksnya, termasuk dirimu sendiri.
Prinsip pertama adalah atomic: satu commit memuat satu perubahan logis yang utuh. Bukan “satu file”, bukan “satu hari kerja”, tapi satu ide yang bisa dijelaskan dalam satu kalimat. Commit atomic membuat git log mudah dibaca, git revert aman (membatalkan satu commit tidak ikut menghapus hal lain), dan git bisect efektif saat memburu bug. Bila kamu menggabungkan perbaikan validasi harga dengan rename variabel di satu commit, dua-duanya jadi sulit dipisah lagi nanti.
Seperti memecah satu task besar di issue tracker jadi sub-task kecil yang jelas, satu perubahan logis sebaiknya menjadi satu commit terisolasi. Issue memberi batas; commit menjadi jejak teknis dari batas itu.
Prinsip kedua adalah pesan yang menjelaskan kenapa. Pesan commit punya dua bagian: subject (baris pertama) dan body (paragraf setelah baris kosong). Subject ditulis imperatif dan singkat, sekitar maksimal 50 karakter, seolah memberi perintah: “Add”, “Fix”, “Refactor”, bukan “Added” atau “Fixing”. Body, yang opsional, menjelaskan kenapa perubahan ini perlu, bukan mengulang apa yang sudah terbaca dari diff.
| Buruk | Baik | Kenapa |
|---|---|---|
update | Add price validation to product handler | Subjek buruk tak menjelaskan apa pun saat dibaca di log. |
fix bug | Fix negative PriceRupiah passing validation | ”Bug” yang mana? Subjek baik menyebut gejala spesifik. |
wip | Refactor product service to accept context | ”wip” tidak punya makna di history permanen. |
Pesan seperti wip, update, fix bug, atau asdf membuat history jadi kabut. Saat kamu menelusuri kenapa sebuah baris berubah, pesan kosong memaksamu membaca seluruh diff dari nol.
Berikut anatomi pesan commit yang lengkap: subject imperatif singkat, baris kosong, lalu body yang menjawab “kenapa”.
Pesan commitReject negative price at product creation PriceRupiah disimpan sebagai int64 dalam rupiah penuh, tapi handler lama menerima nilai negatif tanpa keluhan, lalu lolos sampai ke database. Validasi di tepi mencegah data harga rusak masuk ke sistem diskon yang mengandalkan nilai non-negatif.
Prinsip ketiga: pecah perubahan besar jadi beberapa commit logis. Bila satu sesi kerja menyentuh validasi, lalu rename, lalu perbaikan test, stage dan commit terpisah dengan bantuan git add -p agar tiap commit tetap atomic.
Terminal$ git add -p internal/product/service.go # pilih hunk validasi saja $ git commit -m "Reject negative price at product creation" $ git add -p internal/product/service.go # sekarang hunk rename $ git commit -m "Rename priceCents to priceRupiah for clarity"
Tulis subject seolah melengkapi kalimat “Commit ini akan …”: Add, Fix, Refactor. Simpan alasan dan trade-off di body, karena diff sudah menjawab “apa”, yang hilang justru “kenapa”.
Membaca History
log, show, diff tanpa rasa takut
History bukan museum yang hanya dipajang, melainkan alat investigasi: ia menjawab kenapa sebuah baris ada, kapan sebuah bug masuk, dan apa yang berubah di antara dua rilis.
Pintu utamanya git log. Apa adanya ia menampilkan tiap commit beserta hash, author, date, dan pesan, dari yang terbaru ke terlama. Tapi yang membuatnya berguna adalah flag-flagnya. --oneline memampatkan tiap commit jadi satu baris. --graph menggambar struktur cabang dengan garis ASCII. --all menyertakan semua branch, bukan hanya yang sedang aktif. Dikombinasikan, ketiganya memberi peta repositori yang cepat dibaca.
Terminal$ git log --oneline --graph --all * 8f3a1c2 (HEAD -> main) Reject negative price at product creation * a1b9d04 Add price validation to product handler | * 4c7e8f1 (feat/discount) Add discount engine skeleton |/ * 2d5a6b3 Initialize skincare-backend module layout
Peta cabang dari satu perintah. Garis menunjukkan feat/discount bercabang dari commit yang sama dengan main.
Secara model, history adalah rantai commit yang tiap simpulnya menunjuk ke induknya. Pada satu branch lurus, rantai itu terlihat seperti ini.
gitGraph commit id: "init module" commit id: "add handler" commit id: "validate price" commit id: "reject negative"
Rantai commit linear. Tiap commit menyimpan snapshot penuh dan menunjuk ke induknya, sehingga history bisa ditelusuri mundur.
Bila ingin melihat satu commit secara utuh, git show <hash> menampilkan metadata sekaligus diff lengkapnya. Untuk membandingkan dua titik mana pun, git diff a..b menunjukkan selisih dari commit a ke b, berguna saat menyiapkan release atau meninjau apa yang berubah sejak tag terakhir.
Terminal$ git show 8f3a1c2 # metadata + diff satu commit $ git diff a1b9d04..8f3a1c2 # selisih antara dua commit $ git log -p -- internal/product/service.go # riwayat + diff satu file $ git log --follow -- internal/product/price.go # ikut menembus rename file
Sering yang kamu butuhkan bukan seluruh history, melainkan siapa yang menyentuh satu file. git log -- path/file menyaring commit yang mengubah file tersebut, dan --follow membuat penelusuran tetap utuh meski file pernah di-rename. Tambahkan -p untuk melihat diff tiap perubahannya, sehingga evolusi sebuah file terbaca seperti cerita.
Seperti history browser yang membiarkanmu kembali ke halaman sebelumnya, git log adalah timeline proyek: tiap commit adalah titik yang bisa kamu kunjungi, bandingkan, dan pahami konteksnya.
git log —oneline —graph —all adalah perintah yang paling sering kamu butuhkan untuk orientasi. Jadikan alias (mis. git lg) agar peta cabang selalu satu ketikan jauhnya.
Yang berharga dari history bukan kodenya saja (itu ada di file sekarang), melainkan urutan keputusan: kenapa pendekatan A dipilih lalu diganti B. Itulah sebabnya pesan commit yang menjelaskan “kenapa” membuat history jauh lebih bernilai.
Branch: Ruang Kerja Paralel
Branch adalah pointer ringan ke commit
Branch bukan salinan kode, melainkan pointer ringan yang menunjuk ke satu commit, dan itulah kenapa Git memberanikan kita membuatnya sesering apa pun.
Pada section sebelumnya kita melihat histori sebagai rantai commit yang saling menunjuk ke parent-nya. Branch adalah lapisan di atas rantai itu: sebuah nama yang menyimpan satu hash commit. Tidak ada folder baru, tidak ada penggandaan file. Saat kamu menjalankan git branch feature/login, Git hanya menulis satu baris berisi hash ke dalam .git/refs/heads/. Itu sebabnya membuat branch terasa instan, bahkan pada repo dengan ratusan ribu commit.
Yang membuat branch terasa “hidup” adalah HEAD. HEAD adalah pointer ke branch yang sedang aktif, bukan langsung ke commit. Saat kamu commit, Git menambah commit baru lalu menggeser pointer branch yang ditunjuk HEAD untuk menunjuk commit baru itu. Branch lain tidak bergerak. Inilah mekanisme isolasi: pekerjaan di feature/login tidak menyentuh main sampai kamu memutuskan menggabungkannya.
flowchart LR C1["c1"] --> C2["c2"] C2 --> C3["c3 main"] C2 --> C4["c4 feature"] HEAD(["HEAD"]) -.-> C4
Branch sebagai pointer. main menunjuk c3, feature menunjuk c4, dan HEAD menandai feature sebagai branch aktif.
Di proyek skincare-backend, alur ini sangat praktis. Misalnya main memuat kode yang sudah jalan di produksi, sementara kamu sedang menggarap endpoint checkout. Kamu buat branch feature/checkout, bekerja bebas di sana, dan main tetap utuh siap dirilis kapan saja tanpa terkontaminasi kode setengah jadi.
Branch seperti cabang sungai yang memisah dari aliran utama, mengalir sendiri sebentar, lalu nanti bisa bertemu kembali ke aliran induk lewat merge.
Untuk berpindah branch, perintah modern adalah git switch. Versi lama memakai git checkout yang memikul peran ganda (pindah branch sekaligus memulihkan file), sehingga Git memisahnya menjadi git switch dan git restore. git checkout masih ada dan berfungsi, tapi untuk berpindah branch sebaiknya pakai git switch agar niat kode lebih jelas.
Terminal# membuat dan langsung pindah ke branch baru git switch -c feature/login # ... edit file, lalu: git add . git commit -m "feat: tambah handler login" # kembali ke main; commit feature/login tidak terbawa ke sini git switch main git log --oneline # commit login tidak terlihat di main
git switch -c feature/login setara dengan git branch feature/login lalu git switch feature/login dalam satu langkah, jadi kamu langsung berada di ruang kerja baru.
Kebiasaan menyalin folder project jadi project-eksperimen lalu bingung mana yang terbaru, digantikan branch resmi: satu repo, banyak garis kerja, dan Git yang menjaga mana yang aktif.
Sebuah ref branch hanya menyimpan satu hash (sekitar 41 byte di disk), jadi jangan ragu membuat branch untuk tiap fitur, percobaan, atau perbaikan kecil.
Merge: Menggabungkan Branch
Fast-forward versus three-way merge
Merge adalah cara Git menyatukan pekerjaan dari satu branch ke branch lain, dan hasilnya bergantung pada apakah base sudah bergerak sejak branch dibuat.
Setelah pekerjaan di feature/login selesai dan teruji, kamu ingin perubahannya masuk ke main. Caranya: pindah ke branch tujuan, lalu jalankan git merge. Git punya dua strategi yang dipilih otomatis tergantung bentuk histori, dan memahami keduanya membuat histori proyekmu lebih mudah dibaca.
Kasus pertama adalah fast-forward. Ini terjadi bila main tidak menerima commit baru sejak feature/login dipisah, sehingga commit main masih merupakan leluhur (ancestor) dari ujung feature. Karena tidak ada yang perlu didamaikan, Git cukup menggeser pointer main maju ke commit terakhir feature. Tidak ada commit baru yang dibuat; histori tetap satu garis lurus.
Kasus kedua adalah three-way merge. Ini terjadi bila kedua branch sama-sama maju: ada commit baru di main dan ada commit baru di feature. Git tidak bisa sekadar menggeser pointer karena keduanya menyimpang. Git mengambil tiga titik (ujung kedua branch dan commit leluhur bersama), menggabungkannya, lalu membuat merge commit yang istimewa karena punya dua parent.
gitGraph commit id: "c1" commit id: "c2" branch feature/login checkout feature/login commit id: "f1" commit id: "f2" checkout main commit id: "m1" merge feature/login id: "merge"
Three-way merge. Karena main maju dengan m1 dan feature dengan f1, f2, Git membuat merge commit berisi dua parent.
Bayangkan dua orang mengedit salinan dokumen yang sama dari titik awal yang identik. Three-way merge adalah proses menyatukan kedua revisi dengan melihat naskah asli sebagai acuan, bukan menimpa salah satunya.
Terminal# pindah ke branch tujuan dulu git switch main # fast-forward bila main belum bergerak sejak feature dibuat git merge feature/login # Updating a1b2c3d..e4f5g6h # Fast-forward # memaksa merge commit walau sebenarnya bisa fast-forward git merge --no-ff feature/login
Kapan memilih --no-ff? Flag ini memaksa Git membuat merge commit meski fast-forward sebenarnya mungkin. Hasilnya, jejak bahwa serangkaian commit itu berasal dari satu feature branch tetap terlihat sebagai satu gugus di histori. Banyak tim memakai --no-ff saat menggabungkan feature ke main agar histori bercerita “ini sekelompok perubahan untuk satu fitur”, bukan deret commit datar yang sulit dilacak asalnya.
Pakai git merge —no-ff untuk feature branch agar grup commit-nya tetap tampak utuh di histori; git log —oneline —graph akan menunjukkan struktur cabangnya dengan jelas.
Saat kedua sisi mengubah baris yang sama, three-way merge tidak bisa otomatis dan Git menandainya sebagai conflict. Cara membaca dan menyelesaikannya dibahas tuntas di section 09.
Merge Conflict
Saat Git tidak bisa menggabungkan otomatis
Merge conflict bukan tanda ada yang rusak, melainkan momen Git jujur bahwa ia tidak punya cukup informasi untuk memilih gabungan yang benar, dan menyerahkan keputusan itu kepadamu.
Konflik muncul ketika dua branch mengubah baris yang sama pada file yang sama, atau satu sisi mengubah file sementara sisi lain menghapusnya. Untuk perubahan yang menyentuh baris berbeda, Git menggabungkan otomatis tanpa kamu sadari. Hanya saat dua sisi bertabrakan di baris yang persis sama, Git berhenti, menandai file, dan meminta penyelesaian manual.
Sama seperti dua rekan yang menulis ulang paragraf yang identik di dokumen bersama lalu harus menentukan versi final, dua branch yang mengubah baris yang sama memaksamu memilih atau memadukan keduanya secara sadar.
Saat konflik terjadi, Git menyisipkan conflict marker ke dalam file. Ada tiga penanda: kepala <<<<<<< menandai awal versi branch saat ini (current), pemisah ======= membatasi kedua versi, dan ekor >>>>>>> menutup versi yang masuk (incoming). Tugasmu mengganti seluruh blok bertanda ini dengan versi final yang benar.
internal/order/service.go (saat konflik)func calcTotal(items []Item) int64 { <<<<<<< HEAD var total PriceRupiah for _, it := range items { total += it.Price * int64(it.Qty) } ======= var total int64 for _, it := range items { total += it.Subtotal } >>>>>>> feature/discount return int64(total) }
Setelah membaca kedua sisi, kamu menulis ulang blok itu menjadi satu versi yang menggabungkan maksud keduanya, lalu menghapus semua marker. Hasil resolusi bisa berupa gabungan ide dari kedua branch, bukan sekadar memilih salah satu mentah-mentah.
internal/order/service.go (setelah resolve)func calcTotal(items []Item) PriceRupiah { var total PriceRupiah for _, it := range items { total += it.Price*int64(it.Qty) - it.Discount } return total }
Begitu file beres, kamu menandainya selesai dengan git add <file>, lalu menuntaskan merge dengan git commit. Bila ternyata situasinya terlalu rumit dan kamu ingin mundur, git merge --abort mengembalikan working tree ke kondisi sebelum merge dimulai, seolah merge tak pernah terjadi.
Di main ubah satu baris fungsi, commit. Buat branch dari commit sebelumnya, ubah baris yang sama secara berbeda, commit juga.
Kembali ke main lalu jalankan git merge nama-branch; Git melaporkan CONFLICT dan menyisipkan marker ke file.
Buka file, pahami current (HEAD) dan incoming, lalu tulis ulang blok menjadi versi final tanpa menyisakan marker.
Jalankan git add file untuk menyatakan konflik teratasi, lalu git commit untuk membuat merge commit.
Jalankan test dan build (go test ./…) untuk memastikan hasil gabungan benar-benar berjalan, bukan hanya bebas marker.
Memilih satu sisi tanpa membaca konteks bisa membuang logika penting dari branch lain. Selalu baca kedua versi, dan setelah resolve jalankan test, karena file yang lolos kompilasi belum tentu benar secara logika.
Remote: Fetch, Pull, Push, Upstream
Menghubungkan repo lokal dengan workspace tim
Sampai sini Git masih sepenuhnya lokal, hidup di folder .git di laptopmu sendiri. Remote adalah pintu keluarnya ke tim.
Sebuah remote hanyalah repo Git lain yang bisa kamu ajak bertukar commit lewat jaringan, entah itu GitHub, GitLab, atau server internal kantor. Repo lokalmu menyimpan daftar remote beserta URL-nya, dan menurut konvensi remote utama bernama origin. Lihat daftarnya dengan git remote -v (flag -v menampilkan URL fetch dan push). Yang penting dipahami, Git itu terdistribusi: setiap klona membawa salinan riwayat penuh, dan origin bukan “pusat ajaib”, ia cuma satu repo yang kebetulan disepakati tim sebagai titik temu bersama.
Repo lokal dan remote ibarat dua brankas berisi catatan yang sama. Fetch menyalin halaman baru dari brankas tim ke brankasmu, push menitipkan halamanmu ke brankas tim. Tidak ada yang otomatis menimpa, kamu yang memilih kapan dan apa.
Saat kamu menghubungkan ke remote, Git membuat remote-tracking branch seperti origin/main. Ini adalah cermin lokal dari posisi main di remote pada saat terakhir kamu berkomunikasi dengannya. Penting: origin/main tidak diperbarui sendiri, ia hanya bergerak ketika kamu menjalankan fetch atau pull. Jadi main (branch kerjamu) dan origin/main (cermin remote) bisa berbeda, dan justru selisih itulah yang ingin kamu pahami sebelum menggabungkan.
flowchart LR
subgraph Lokal["Repo lokal"]
L["main"]
OM["origin/main (cermin)"]
end
subgraph Remote["origin (GitHub)"]
R["main"]
end
R -- "git fetch" --> OM
L -- "git push" --> R
OM -. "merge / rebase" .-> LFetch lalu push. Fetch memperbarui cermin origin/main, push mengirim commit lokal ke remote. Penggabungan ke main lokal adalah langkah terpisah yang kamu kendalikan.
Di sinilah git fetch dan git pull berbeda secara fundamental. git fetch mengunduh objek dan commit baru dari remote lalu menggerakkan origin/main, tetapi tidak menyentuh branch aktifmu. Working tree dan main lokal tetap apa adanya. git pull adalah fetch yang langsung disusul integrasi ke branch aktif, secara default lewat merge (atau rebase bila kamu pakai git pull --rebase). Karena pull menggabungkan tanpa memberimu jeda untuk melihat, fetch dulu lalu lihat selisihnya adalah kebiasaan yang lebih tenang.
Biasakan git fetch lalu git log --oneline main..origin/main untuk melihat commit apa yang akan masuk sebelum menggabungkannya. Kamu tidak pernah dikejutkan oleh perubahan yang tiba-tiba sudah ter-merge.
Untuk push pertama kali sebuah branch, gunakan git push -u origin main. Flag -u (singkatan --set-upstream) menetapkan upstream: relasi tetap antara branch lokalmu dan origin/main. Setelah upstream tersetel, git push, git pull, dan git status tahu lawan bicaranya tanpa kamu sebutkan lagi, dan git status bahkan akan memberitahu “your branch is ahead of origin/main by 2 commits”.
Terminal# hubungkan repo lokal ke remote dan dorong main pertama kali git remote add origin git@github.com:kamu/skincare-backend.git git remote -v git push -u origin main # lihat update tim tanpa mengubah branch aktif, lalu bandingkan git fetch origin git log --oneline main..origin/main # commit yang ada di remote, belum di lokal git status # ahead/behind terhadap upstream
Lalu ada momen yang pasti kamu temui: push ditolak dengan pesan ! [rejected] ... (non-fast-forward). Artinya remote sudah punya commit yang belum ada di repo lokalmu (rekan setim mendorong duluan), sehingga push-mu akan menghapus jejak mereka. Git menolak demi keselamatan. Penyelesaiannya bukan memaksa, melainkan menarik dulu pekerjaan mereka, menumpuk commit-mu di atasnya, baru mendorong lagi.
Hindari git push --force ke branch bersama, ia menghapus commit rekanmu. Jalankan git pull --rebase untuk menumpuk commit-mu di atas milik mereka, selesaikan konflik bila ada, lalu git push. Bila benar-benar perlu memaksa, pakai --force-with-lease yang membatalkan diri bila remote berubah di luar dugaan.
Seperti memindahkan proyek dari folder di laptop ke workspace tim yang dipakai bersama, remote mengubah riwayatmu dari catatan pribadi menjadi sumber yang bisa ditarik, ditinjau, dan dilanjutkan siapa pun di tim.
Pull Request dan Code Review
Tempat review, CI, dan keputusan merge
Mendorong branch ke remote baru setengah cerita, pull request adalah tempat branch itu ditinjau, diuji, dan diputuskan layak masuk main.
Pull request (PR di GitHub) atau merge request (MR di GitLab) adalah usulan formal untuk menggabungkan branch-mu ke branch target. Ia jauh lebih dari tombol merge: PR menyatukan judul yang jelas, deskripsi yang menjelaskan kenapa perubahan ini ada, reviewer yang ditunjuk, status check dari CI, dan keputusan approval. Inti reviewnya berbasis diff, jadi reviewer membaca selisih baris demi baris, bukan menjalankan seluruh aplikasi di kepala mereka. Karena itulah PR menjadi titik kendali mutu sekaligus arsip keputusan: enam bulan kemudian, PR-lah yang menjelaskan mengapa PriceRupiah diubah menjadi int64.
flowchart LR B["branch feature"] --> P["git push"] P --> PR["buka Pull Request"] PR --> RV["review + CI checks"] RV -->|request changes| FIX["push perbaikan"] FIX --> RV RV -->|approve + hijau| MG["merge ke main"] MG --> DP["deploy"]
Siklus pull request. Branch didorong, PR dibuka, review dan CI berputar sampai hijau dan disetujui, baru merge lalu deploy.
Alur code review yang sehat punya ritme khas. Reviewer bisa meninggalkan comment biasa untuk diskusi, suggestion yang berisi potongan kode siap-terima sekali klik, request changes yang memblokir merge sampai diperbaiki, atau approve yang memberi lampu hijau. Sebagai penulis, tanggapi setiap komentar, dorong commit perbaikan ke branch yang sama (PR memperbarui dirinya otomatis), dan tandai percakapan selesai. Yang paling menentukan kualitas review bukan ketajaman reviewer, melainkan ukuran PR: diff 60 baris diperiksa teliti dalam sepuluh menit, diff 1.500 baris hanya akan dapat “LGTM” yang sebenarnya berarti “saya menyerah membacanya”.
Pecah pekerjaan menjadi PR yang fokus pada satu hal, idealnya di bawah beberapa ratus baris diff. PR kecil mendapat review yang lebih cepat, lebih teliti, dan lebih jarang menyembunyikan bug di antara perubahan yang tidak relevan.
Deskripsi PR yang baik menjawab tiga hal sekaligus: apa yang berubah, kenapa, dan bagaimana cara memverifikasinya. Reviewer yang membaca diff tidak otomatis tahu konteks bisnisnya, jadi deskripsilah yang memberi mereka peta.
PR description## Apa Tambah endpoint POST /v1/products untuk membuat produk skincare. ## Kenapa Tim katalog perlu menambah SKU baru tanpa akses langsung ke database. Harga disimpan sebagai PriceRupiah int64 (rupiah penuh, bukan float) agar bebas galat pembulatan. ## Cara verifikasi - `go test ./internal/product/...` hijau - curl contoh ada di komentar pertama
Tujuan utama code review bukan mencari kesalahan, melainkan menyebarkan pemahaman: reviewer belajar bagian kode yang tidak ia tulis, penulis dapat sudut pandang baru, dan tim secara perlahan menyepakati gaya bersama. Kesalahan yang tertangkap hanyalah bonus.
Bila dulu kamu menyerahkan kerjaan ke QA atau lead lalu menunggu verdict di tiket, PR memindahkan percakapan itu ke konteks kode itu sendiri. Komentar menempel pada baris yang dibahas, sehingga umpan balik menjadi konkret dan langsung bisa ditindaklanjuti.
Branch Protection dan CODEOWNERS
Melindungi main dengan review, status check, dan owner
PR hanya berarti bila aturannya ditegakkan, branch protection memastikan tidak ada perubahan yang menyelinap ke main tanpa review dan CI.
Branch penting seperti main adalah sumber kebenaran yang biasanya jadi dasar deploy, jadi ia layak dipagari. GitHub menyediakan dua mekanisme: branch protection rules klasik dan rulesets modern yang lebih luwes. Rulesets bisa banyak aktif bersamaan, punya status on/off, dan dapat dilihat siapa pun yang punya akses baca; menurut dokumentasi GitHub tentang rulesets, aturan-aturan ini “layer with protection rules” sehingga ketika tumpang tindih, yang paling restriktif yang menang. Aturan yang paling sering dipakai: wajib pull request sebelum merge dengan jumlah approval tertentu, wajib status check (CI) lulus, blokir force push, dan larang penghapusan branch.
Branch protection bukan soal tidak percaya pada tim, melainkan guardrail yang membuat kebiasaan baik menjadi default dan kesalahan fatal menjadi sulit terjadi. Untuk repo baru, mulai dengan ruleset modern GitHub karena lebih ekspresif dan bisa ditumpuk per kebutuhan.
Reviewer tidak harus ditunjuk manual setiap kali. File CODEOWNERS memetakan path ke pemilik, sehingga GitHub otomatis meminta review dari tim yang tepat begitu PR menyentuh file di area mereka. File ini dicari di .github/, root, lalu docs/, dan sintaksnya mengikuti pola gitignore diikuti @username atau @org/team-name. Menurut dokumentasi GitHub tentang code owners, bila digabung dengan aturan “require review from Code Owners”, satu approval dari salah satu owner sudah cukup.
CODEOWNERS# Owner default untuk semua file * @org/maintainers # Per area: yang paling spesifik menang /frontend/ @org/team-fe /backend/ @org/team-be /infra/ @org/team-infra # Pola bisa per ekstensi atau direktori dalam **/*.sql @org/team-be
Seperti production yang hanya bisa disentuh lewat pipeline berizin, bukan SSH langsung sembarangan, branch main yang dilindungi hanya bisa berubah lewat PR yang lolos review dan CI. Pintunya sama: jalur yang teruji, bukan jalan pintas.
Menyetel proteksi pada main hanya beberapa langkah lewat antarmuka GitHub.
Di repo, masuk Settings → Rules → Rulesets, lalu New branch ruleset dan beri nama deskriptif seperti “protect-main”.
Pada Target branches, tambahkan pola yang mencakup main (atau “Default branch”), dan set Enforcement status ke Active.
Centang “Require a pull request before merging” dan tetapkan minimal jumlah approval, mis. 1, plus “Require review from Code Owners” bila pakai CODEOWNERS.
Centang “Require status checks to pass” lalu pilih job CI yang harus hijau, mis. build dan go test, agar kode rusak tidak bisa di-merge.
Aktifkan “Block force pushes” dan “Restrict deletions” supaya riwayat main tidak bisa ditimpa atau dihapus, lalu simpan ruleset.
Bila main tidak terlindungi, satu git push --force yang keliru bisa menimpa dan menghapus commit seluruh tim secara permanen di remote. Block force pushes adalah pagar paling murah dengan dampak paling besar, nyalakan sejak hari pertama.
Rebase: Merapikan Sejarah
Linear history, merge vs rebase, dan interactive rebase
Rebase memindahkan commit branch-mu ke atas base terbaru, menulis ulang sejarah agar menjadi garis lurus yang mudah dibaca.
Saat kamu bekerja di branch feature/checkout, branch main tidak diam. Rekanmu terus merge fitur lain ke sana. Kalau nanti kamu merge balik, sejarah jadi rumit: garis bercabang, merge commit di mana-mana, dan git log terlihat seperti peta kereta bawah tanah. Rebase menawarkan jalan lain: alih-alih menyatukan dua cabang dengan merge commit, ia mengangkat commit-commitmu dan menanamnya ulang satu per satu di atas ujung main yang terbaru.
Dokumen GitHub menyebut rebasing sebagai cara “menulis ulang riwayat commit”, yang membuat riwayat lebih bersih namun harus dipakai dengan hati-hati karena commit lama digantikan oleh commit baru dengan hash berbeda.
Terminalgit switch feature/checkout git fetch origin git rebase origin/main
Secara teknis, git rebase origin/main melakukan ini: Git mencari commit nenek moyang bersama antara feature/checkout dan origin/main, “melepas” tiap commit milikmu sejak titik itu, memindahkan HEAD ke ujung origin/main, lalu memutar ulang (replay) commit-commitmu satu demi satu. Karena induknya berubah, setiap commit mendapat hash baru. Isinya sama, identitasnya berbeda. Inilah inti dari “menulis ulang sejarah” yang harus kamu pahami benar sebelum melangkah lebih jauh.
Bayangkan commit-mu sebagai tanaman di pot. Merge menyambung dua pot dengan selang. Rebase mencabut tanamanmu dan menanamnya ulang di tanah main yang baru, seolah ia tumbuh di sana sejak awal.
Merge versus rebase
Keduanya menggabungkan kerja dari dua branch, tetapi menghasilkan sejarah yang berbeda secara mendasar. Merge jujur apa adanya: ia menyimpan fakta bahwa dua jalur paralel pernah ada lalu bertemu di satu merge commit. Rebase memilih kerapian: ia berpura-pura kerjamu memang dibangun di atas base terbaru, menghasilkan garis lurus tanpa merge commit.
| Aspek | Merge | Rebase |
|---|---|---|
| Sejarah | Bercabang, ada merge commit | Linear, tanpa merge commit |
| Hash commit | Tetap (tidak berubah) | Ditulis ulang (hash baru) |
| Kejujuran riwayat | Menyimpan jejak paralel | Menyembunyikan jejak paralel |
| Aman untuk branch shared? | Ya | Tidak, jangan |
flowchart LR
subgraph Merge
A1[main] --> M[merge commit]
B1[feature] --> M
end
subgraph Rebase
A2[main] --> C1[feature']
C1 --> C2[feature'']
endDua hasil akhir. Merge menyatu di satu titik; rebase memanjang lurus.
Bila commit sudah di-push dan dipakai orang lain, rebase menulis ulang hash-nya. Saat rekanmu menarik, Git melihat dua sejarah berbeda dan repo mereka kacau dengan commit ganda. Rebase hanya pada commit yang masih lokal dan belum dibagikan.
Konflik saat rebase
Karena rebase memutar ulang commit satu per satu, konflik bisa muncul di commit mana pun di tengah jalan. Saat itu terjadi, rebase berhenti dan menunggu. Pola penyelesaiannya berbeda dari merge: kamu tidak commit, melainkan melanjutkan rebase.
Buka file bertanda, sunting, lalu git add file yang sudah beres.
Jalankan git rebase —continue, Git memutar commit berikutnya.
Bila ingin batal, git rebase —abort mengembalikan branch ke kondisi sebelum rebase.
Interactive rebase: poles sebelum PR
git rebase -i membuka editor berisi daftar todo: tiap commit satu baris dengan kata kunci di depannya. Di sinilah kamu merapikan riwayat draft sebelum dibaca reviewer. Tiga commit “wip”, “fix typo”, dan “beneran fix” bisa dilebur jadi satu commit bersih yang menceritakan satu perubahan utuh.
| Kata kunci | Arti |
|---|---|
pick | Pakai commit apa adanya |
reword | Pakai commit, tapi ubah pesannya |
squash | Lebur ke commit di atasnya, gabung kedua pesan |
fixup | Lebur ke commit di atasnya, buang pesannya |
edit | Berhenti di commit ini untuk diubah |
drop | Buang commit sepenuhnya |
Terminalgit rebase -i HEAD~3 # editor terbuka: # pick a1b2c3d add product handler # fixup d4e5f6a fix typo # fixup 9z8y7x6 wip # simpan & tutup → tiga commit jadi satu
Mengubah urutan baris di editor todo akan mengubah urutan commit saat replay. Pindahkan commit dokumentasi ke bawah, atau dekatkan fixup ke commit asalnya, lalu simpan.
Saat menulis artikel, kamu tidak menerbitkan setiap coretan. Kamu menggabung paragraf, menghapus catatan, lalu publish versi rapi. Interactive rebase adalah penyuntingan akhir itu untuk riwayat commit-mu.
Inti rebase
- git rebase main memindahkan commit branch ke atas base terbaru, hasilnya linear.
- Konflik diselesaikan lalu git rebase —continue, atau batalkan dengan —abort.
- Rebase menulis ulang hash, jadi jangan pada commit publik yang dipakai orang lain.
- git rebase -i dengan squash/fixup/reword merapikan draft commit sebelum PR.
Reset, Restore, Revert, dan Stash
Membatalkan perubahan dengan aman
Git punya beberapa cara membatalkan perubahan, dan salah memilih bisa menghapus kerja keras secara permanen.
Pertanyaan “bagaimana cara undo di Git?” tidak punya satu jawaban, karena tergantung kamu ingin membatalkan apa: perubahan file yang belum di-commit, staging yang keliru, atau commit yang sudah masuk sejarah. Empat perintah menangani kasus yang berbeda, dan memahami batas masing-masing menyelamatkanmu dari kehilangan data.
git restore: buang perubahan working tree
git restore menangani file yang sudah kamu ubah tapi belum di-commit. Ada dua arah: mengembalikan isi file ke versi terakhir, atau mengeluarkannya dari staging area.
Terminalgit restore internal/product/service.go # buang edit, kembali ke HEAD git restore --staged internal/product/service.go # unstage, edit tetap ada
Ctrl+Z di editor membatalkan ketikan terakhir di satu file. git restore bekerja di level snapshot repo, mengembalikan file ke kondisi commit terakhir sekaligus, bahkan setelah editor ditutup.
git reset: memindahkan HEAD
reset menggeser pointer HEAD ke commit lain, dan tiga modenya menentukan area mana yang ikut berubah. Inilah perintah yang paling sering disalahpahami, dan --hard adalah yang paling berbahaya.
| Mode | HEAD | Index (staging) | Working tree |
|---|---|---|---|
—soft | Pindah | Tetap (perubahan tetap staged) | Tetap |
—mixed (default) | Pindah | Di-reset (unstage) | Tetap |
—hard | Pindah | Di-reset | Ditimpa (perubahan hilang) |
Terminalgit reset --soft HEAD~1 # batalkan commit terakhir, isinya tetap tersimpan & staged git reset HEAD~1 # (--mixed) batalkan commit, isi jadi unstaged di working tree git reset --hard HEAD~1 # batalkan commit DAN buang semua perubahannya
--soft berguna saat kamu ingin menyusun ulang commit terakhir tanpa kehilangan apa pun. --mixed mengembalikan perubahan ke working tree untuk dipilah ulang. --hard membuang semuanya tanpa ampun.
Mode —hard menimpa working tree dan perubahan yang belum di-commit lenyap. Untuk commit yang sempat ada, git reflog masih merekam pergerakan HEAD dan bisa dipakai memulihkan, tapi file yang belum pernah di-commit sama sekali tidak terselamatkan.
git revert: undo aman untuk sejarah publik
reset cocok untuk commit lokal, tapi berbahaya bila commit sudah di-push. Untuk membatalkan commit yang sudah dibagikan, pakai git revert. Alih-alih menghapus, ia membuat commit baru yang isinya kebalikan dari commit target. Sejarah tetap utuh, hash lama tetap ada, dan rekanmu tidak terganggu.
flowchart LR A[C1] --> B[C2 buggy] --> C[C3] --> D[C4 revert C2]
Revert menambah, bukan menghapus. C4 membalik efek C2 tanpa menyentuh sejarah yang sudah dipush.
Terminalgit revert a1b2c3d # buat commit baru yang membalik a1b2c3d
Aturan praktis: commit belum di-push? reset boleh. Commit sudah di-push dan dipakai orang lain? Selalu revert, karena ia tidak menulis ulang sejarah yang sudah dimiliki orang.
git stash: simpan sementara
Kadang kamu sedang setengah jalan mengerjakan sesuatu lalu harus pindah branch untuk perbaikan mendesak. git stash menyimpan perubahan yang belum di-commit ke tumpukan terpisah, membersihkan working tree, lalu bisa kamu kembalikan nanti.
git stash push -m “wip checkout” menyimpan dan memberi label.
Working tree bersih, bebas git switch ke branch lain.
git stash pop menerapkan lalu menghapus dari tumpukan, atau git stash apply menerapkan tanpa menghapus.
Terminalgit stash push -m "wip checkout" git stash list # lihat semua stash git switch hotfix/price-bug # ... perbaiki, commit ... git switch feature/checkout git stash pop # lanjutkan kerja sebelumnya
Empat alat undo
- restore: buang edit file (
—stageduntuk unstage), aman, lingkup file. - reset: geser HEAD;
—softsimpan staged,—mixedunstage,—hardbuang semua. - revert: commit pembalik, satu-satunya undo aman untuk sejarah publik.
- stash: parkir perubahan sementara dengan push/pop/apply/list untuk pindah branch.
Tag, Gitignore, Hooks, dan Conventional Commits
Release, kebersihan repo, dan automasi pesan
Empat alat kebersihan yang membuat repo bisa dirilis dengan rapi dan dijaga tetap bersih tanpa disiplin manual.
Sejauh ini fokus kita pada bagaimana commit mengalir. Section ini melengkapi gambar dengan empat hal di sekelilingnya, menandai titik rilis (tag), menjaga file yang tak layak masuk (.gitignore), menjalankan pengecekan otomatis di momen tepat (hooks), dan menyepakati format pesan (Conventional Commits). Keempatnya saling menguatkan, tag dan pesan terstruktur memungkinkan changelog otomatis, hook memaksa pesan itu valid sebelum commit terbentuk.
Tag: menandai versi rilis
Tag adalah penanda permanen pada sebuah commit, biasanya untuk versi rilis. Ada dua jenis. Lightweight tag hanya pointer ke commit, seperti branch yang tidak bergerak. Annotated tag adalah objek penuh dengan nama tagger, tanggal, pesan, dan bisa ditandatangani GPG. Untuk rilis publik selalu pakai annotated.
Terminalgit tag -a v1.0.0 -m "Rilis publik pertama skincare-backend" git tag # daftar tag lokal git show v1.0.0 # lihat objek tag + commit yang ditunjuk git push origin v1.0.0 # tag tidak ikut push biasa, dorong eksplisit git push origin --tags # atau dorong semua tag sekaligus
Penomoran versi mengikuti Semantic Versioning, MAJOR.MINOR.PATCH. MAJOR naik saat ada perubahan yang memecah kompatibilitas, MINOR saat ada fitur baru yang tetap kompatibel, PATCH saat hanya perbaikan bug. Pemetaan ini bukan kebetulan, ia selaras persis dengan Conventional Commits di bawah.
Angka di package.json “version” atau composer.json adalah ide yang sama, tag git menjadikannya titik yang bisa di-checkout, di-rollback, dan dibandingkan kapan saja.
.gitignore: menjaga repo tetap bersih
Repo backend tidak perlu menyimpan dependensi, rahasia, atau hasil build. File-file itu besar, berbeda per mesin, atau berbahaya bila bocor. .gitignore mendaftar pola yang Git abaikan.
.gitignore# dependensi & build node_modules/ /bin/ /tmp/ *.exe # rahasia (JANGAN pernah commit) .env .env.* !.env.example # file OS & editor .DS_Store Thumbs.db .idea/ .vscode/
Pola membaca seperti ini, / di akhir berarti hanya cocok direktori, / di awal meng-anchor relatif ke lokasi file .gitignore, dan tanpa / cocok di level mana pun. Tanda ! me-re-include file yang tadinya terabaikan, di contoh atas .env.* mengabaikan semua varian tapi .env.example tetap dilacak agar tim tahu kunci apa yang dibutuhkan. Untuk pola lintas semua proyek (mis. .DS_Store), pakai global ignore via git config --global core.excludesFile ~/.config/git/ignore.
Menambah .env ke .gitignore tidak menghapus jejaknya dari history, secret-nya tetap ada di commit lama. Rotasi (ganti) semua kredensial yang pernah bocor, lalu bersihkan history. Cara membersihkannya dibahas di section 17.
Git hooks: automasi di momen tepat
Hook adalah skrip yang Git jalankan otomatis pada event tertentu. Tiga yang paling berguna di sisi klien, pre-commit (sebelum pesan ditulis, untuk lint/format/test cepat), commit-msg (validasi format pesan), dan pre-push (cek terakhir sebelum objek dikirim ke remote). Bila hook keluar dengan kode non-nol, operasi dibatalkan.
.git/hooks/pre-commit#!/bin/sh # format & vet sebelum snapshot commit terbentuk gofmt -l . | grep . && echo "Jalankan gofmt dulu" && exit 1 go vet ./... || exit 1 exit 0
Hook hidup di .git/hooks/ dan tidak ikut saat clone, jadi disiplin tim tidak terjaga sendiri. Karena itu tim memakai manajer hook yang menyimpan konfigurasi di dalam repo dan memasangnya lewat core.hooksPath. Pilihan umum, husky (ekosistem Node/npm), lefthook (ditulis Go, cepat dan paralel, cocok untuk proyek polyglot), dan pre-commit (berbasis Python, banyak bahasa).
Format-on-save di editor hanya menjaga mesinmu. pre-commit hook adalah jaring yang sama tapi di gerbang repo, tak peduli editor atau mesin siapa, commit kotor tidak lolos.
Conventional Commits: pesan yang bisa diolah mesin
Conventional Commits 1.0.0 menstandarkan baris pertama pesan menjadi type(scope): description. Tipe yang lazim, feat (fitur baru), fix (perbaikan bug), docs, refactor, test, dan chore. Karena formatnya konsisten, alat bisa membaca riwayat untuk menghasilkan changelog dan menentukan versi otomatis.
Contoh pesanfeat(cart): tambah endpoint POST /v1/carts/items Hitung subtotal pakai PriceRupiah int64 agar bebas galat pembulatan. fix(auth): tolak token kedaluwarsa dengan 401 refactor(order): pisahkan service dari handler feat(api)!: ubah field harga jadi PriceRupiah, hapus "price_float" BREAKING CHANGE: respons /v1/products kini memakai price_rupiah.
Pemetaan ke SemVer langsung, fix menaikkan PATCH, feat menaikkan MINOR, dan perubahan yang memecah kompatibilitas (ditandai ! setelah type atau footer BREAKING CHANGE:) menaikkan MAJOR. Gabungkan dengan commit-msg hook agar pesan yang tidak sesuai format ditolak sebelum masuk history.
Pakai scope yang mencerminkan domain proyek, cart, order, auth, product. Saat membaca git log —oneline nanti, scope membuat riwayat bisa dipindai per area tanpa membuka diff.
Workflow Tim: GitHub Flow sampai Trunk-Based
GitHub Flow, GitLab Flow, Git Flow, Trunk-Based, plus hotfix
Workflow bukan aturan baku, ia cara tim menyepakati bagaimana cabang mengalir dari ide ke produksi.
Semua perintah yang sudah kamu kuasai (branch, merge, PR, rebase) adalah huruf. Workflow adalah kalimatnya, kesepakatan kapan membuat branch, ke mana ia merge, dan kapan kode mencapai pengguna. Tidak ada yang “paling benar”, yang ada adalah yang paling cocok dengan ukuran tim dan irama deployment. Mari telusuri dari yang paling sederhana.
GitHub Flow: sesederhana mungkin
GitHub Flow hanya punya satu cabang abadi, main, yang selalu siap deploy. Setiap pekerjaan lahir sebagai branch pendek dari main, dibuka sebagai Pull Request untuk review dan CI, lalu di-merge kembali dan langsung di-deploy. Branch berumur jam atau hari, bukan minggu.
gitGraph commit id: "init" commit id: "main siap" branch feat/cart checkout feat/cart commit id: "add cart" commit id: "test cart" checkout main merge feat/cart tag: "deploy" branch fix/auth checkout fix/auth commit id: "fix 401" checkout main merge fix/auth tag: "deploy"
GitHub Flow. Garis utama lurus, cabang lahir pendek lalu kembali dan langsung dirilis.
Satu task di papan kerja (mis. “tambah keranjang”) memetakan satu-satu ke satu branch pendek dan satu PR. Workflow tim sebenarnya hanya menumpuk kebiasaan harianmu menjadi konvensi bersama.
GitLab Flow: menambah environment branch
Saat deploy tidak instan dan perlu tahap (staging dulu, baru produksi), GitHub Flow kurang. GitLab Flow menambah environment branch. Fitur tetap merge ke main, lalu main dipromosikan downstream lewat merge request berurutan. Menurut dokumentasi GitLab, commit hanya mengalir ke hilir setelah teruji di tiap environment.
flowchart LR F[feature branch] -->|MR + review| M[main] M -->|promote| S[staging] S -->|MR| P[pre-production] P -->|MR| PR[production]
GitLab Flow dengan environment branch. Kode hanya bergerak ke kanan, tiap panah adalah gerbang yang sudah lolos uji.
Git Flow: lengkap tapi berat
Git Flow memakai banyak cabang berumur panjang, main (rilis), develop (integrasi), plus feature/*, release/*, dan hotfix/*. Cocok untuk produk dengan rilis berversi terjadwal, tapi sering terlalu berat untuk tim kecil yang deploy harian.
gitGraph commit id: "main" branch develop commit id: "dev base" branch feature/cart commit id: "cart" checkout develop merge feature/cart branch release/1.2 commit id: "stabilize" checkout main merge release/1.2 tag: "v1.2.0" branch hotfix/1.2.1 commit id: "patch" checkout main merge hotfix/1.2.1 tag: "v1.2.1" checkout develop merge hotfix/1.2.1
Git Flow. Dua cabang abadi (main + develop) plus release dan hotfix, perhatikan hotfix di-back-merge ke develop agar perbaikan tidak hilang.
Trunk-Based dan dua alur khusus
Trunk-Based Development mendorong integrasi sangat sering ke satu trunk dengan branch berumur sangat pendek (jam, bahkan langsung). Fitur yang belum siap disembunyikan di balik feature flag, bukan ditahan di branch panjang, sehingga konflik besar nyaris tak pernah terjadi. Di luar alur utama, dua pola sering muncul. Hotfix, cabang dari tag atau main untuk patch darurat, review cepat, tag versi PATCH baru, lalu back-merge agar perbaikan ikut ke cabang pengembangan. Release branch, cabang stabilisasi paralel tempat hanya bugfix yang diterima, sementara main jalan terus untuk fitur berikutnya, menghasilkan release candidate sebelum tag final.
Untuk kebanyakan tim, mulailah dari GitHub Flow atau Trunk-Based. Jangan mengadopsi Git Flow yang kompleks tanpa alasan nyata seperti rilis berversi terjadwal atau dukungan banyak versi sekaligus, kompleksitas itu menambah pajak harian.
Latih dua hal, simulasikan satu feature branch dari awal sampai merge ber-PR (GitHub Flow), lalu di papan rancang alur main → staging → production untuk skincare-backend dan tentukan gerbang CI apa yang dijaga di tiap promosi.
Topik Lanjutan dan Troubleshooting
Monorepo, submodule, bisect, pitfalls, security, CI/CD
Kumpulan kemampuan yang membedakan orang yang panik saat repo kacau dari orang yang tahu di mana commit “hilang” disimpan.
Setelah alur dasar lancar, sisa perjalanan adalah skala dan pemulihan, bagaimana repo besar dikelola, bagaimana melacak bug ke commit penyebabnya, dan apa yang dilakukan saat sesuatu terlihat lenyap. Kabar baiknya, Git jarang benar-benar membuang data, ia hanya menyembunyikannya sampai kamu tahu cara melihatnya.
Skala: monorepo, submodule, subtree
Monorepo menampung banyak layanan dalam satu repo. Kuncinya kepemilikan path lewat CODEOWNERS (file di .github/ berisi pola gitignore plus @tim), agar PR di area tertentu otomatis meminta review pemilik area. Untuk repo raksasa, sparse-checkout dan partial clone membuatmu mengunduh hanya bagian yang dikerjakan. Submodule dan subtree menyematkan repo lain, submodule menyimpan pointer ke commit eksternal, tapi keduanya menambah kompleksitas nyata dan sering lebih baik dihindari kecuali ada kebutuhan kuat. Apa pun pilihannya, PR kecil tetap aturan emas.
git bisect: berburu commit penyebab bug
Saat sebuah bug muncul entah kapan, jangan menebak. git bisect melakukan pencarian biner di antara commit yang masih baik dan yang sudah rusak, membelah rentang sampai menemukan commit pertama yang merusak.
Terminalgit bisect start git bisect bad # commit sekarang rusak git bisect good v1.0.0 # versi ini masih sehat # Git checkout commit di tengah, kamu uji, lalu beri tahu hasilnya: git bisect good # atau: git bisect bad # ulangi sampai Git menunjuk "first bad commit" git bisect reset # kembali ke posisi semula
Bisect. 1000 commit ditelusuri hanya dalam ~10 langkah karena tiap jawaban memotong setengah ruang pencarian.
Pitfalls dan pemulihan lewat reflog
Jebakan tersering, detached HEAD, stale branch yang tertinggal jauh, commit nyasar di branch salah, dan force push yang menimpa kerja orang. Penyelamatnya satu, git reflog, catatan setiap pergerakan HEAD di mesinmu. Bahkan setelah reset --hard, commit lama masih tercatat di reflog dan bisa dipulihkan.
Terminalgit reflog # contoh: a1b2c3d HEAD@{2}: commit: add cart git switch -c rescue a1b2c3d # tahan commit "hilang" dengan branch baru
Detached HEAD berarti commit yang tidak dipegang branch mana pun, mudah ter-garbage-collect. Selalu git switch -c <branch> sebelum commit dalam keadaan ini. Dan force push hanya boleh ke branch milikmu sendiri, gunakan —force-with-lease agar batal bila ada commit orang lain yang belum kamu lihat.
Security dan CI/CD sebagai gerbang
Hygiene minimal, pastikan .env tidak pernah masuk repo, aktifkan secret scanning untuk menangkap kunci yang bocor, pertimbangkan signed commit untuk membuktikan author, dan terapkan least privilege pada akses. Bila secret pernah ter-commit, rotasi (ganti) kuncinya, menghapus dari history saja tidak cukup karena clone lama tetap menyimpannya. CI/CD mengikat semua ini, required status check menjadikan lolosnya test sebagai syarat merge (gerbang), sementara deploy bisa branch-based (merge ke main memicu staging) atau tag-based (tag v* memicu rilis produksi).
Mendebug error runtime, kamu mempersempit ke baris kode penyebab. Mendebug repo sama persis, bisect mempersempit ke commit penyebab, dan reflog adalah stack trace pergerakan HEAD-mu.
Latih pemulihan nyata, pakai git bisect untuk melacak satu regression, lalu pindahkan sebuah commit yang nyasar di branch salah dan pulihkan hasil reset —hard yang tak sengaja lewat git reflog.
Ringkasan dan Workflow Rekomendasi
Dari kerja sendiri ke tim yang aman dan scalable
Git yang kamu kuasai sekarang bukan kumpulan perintah, melainkan satu model mental yang konsisten dari snapshot sampai kolaborasi tim.
Mari satukan benangnya. Semua dimulai dari satu ide, Git menyimpan snapshot, bukan diff. Dari sana mengalir tiga area (working tree, index, repo) yang menjelaskan kenapa add dan commit terpisah. Di atasnya berdiri commit yang baik (atomik, pesan jelas), lalu branch dan merge untuk bekerja paralel, conflict yang kamu selesaikan dengan tenang karena paham strukturnya, dan remote plus Pull Request + review untuk berbagi. Branch protection menjadikan kualitas itu wajib, sementara rebase dan operasi undo (reset, revert, reflog) memberi kendali untuk merapikan dan memulihkan. Workflow tim hanyalah cara menata semua itu untuk banyak orang.
Yang Wajib Menempel
- Git menyimpan snapshot, bukan diff, hash menjamin integritas tiap versi.
- Tiga area (working tree, index, repo) menjelaskan kenapa staging ada dan berguna.
- Commit yang baik bersifat atomik dengan pesan yang menjelaskan “kenapa”.
- Branch itu murah, merge menyatukan, fast-forward bila linear, merge commit bila bercabang.
- Conflict itu normal, selesaikan dengan memahami kedua sisi, bukan menghapus salah satunya.
- Remote, PR, dan review adalah tempat kualitas tim benar-benar terjaga.
- Branch protection dan required status check menjadikan standar tidak bisa dilewati.
- Rebase merapikan history (hanya yang belum dibagikan), revert aman untuk history publik.
- reflog adalah jaring pengaman, commit jarang benar-benar hilang.
Prinsip akhir yang menyatukan semuanya, mulai dari yang sederhana, tambah kompleksitas hanya saat tim dan deployment benar-benar membutuhkannya. Workflow yang berat sebelum waktunya adalah pajak harian tanpa imbalan.
Tim kecil / solo
GitHub Flow atau Trunk-Based. Satu main yang selalu siap deploy, branch pendek, PR ringan, CI dasar. Tidak perlu environment branch sampai deployment menuntutnya.
Tim sedang tumbuh
GitHub Flow plus branch protection, required review, dan CODEOWNERS. Tambah staging environment dan deploy branch-based saat rilis mulai perlu tahap.
Tim enterprise
GitLab Flow dengan environment branch atau Git Flow bila ada rilis berversi terjadwal. Rulesets ketat, signed commit, secret scanning, dan deploy tag-based ke produksi.
Langkah berikutnya bukan lagi membaca, melainkan berkolaborasi nyata, buka PR pertamamu di skincare-backend, minta review, dan biarkan CI menjadi gerbang. Saat alur itu terasa alami, kamu siap menyambungkannya ke pipeline CI/CD penuh, tempat setiap merge ke main mengantar kode dengan aman sampai ke pengguna.