Sistem kontrol versi (Version Control Systems atau VCS) adalah alat untuk melacak perubahan pada kode sumber (atau kumpulan file dan folder lainnya). Sesuai namanya, alat ini membantu menjaga riwayat perubahan dan memfasilitasi kolaborasi. VCS melacak perubahan pada sebuah folder beserta isinya dalam serangkaian snapshot. Setiap snapshot mencakup keseluruhan status file/folder dalam sebuah direktori tingkat atas. VCS juga menyimpan metadata seperti siapa pembuat setiap snapshot, pesan terkait setiap snapshot, dan sebagainya.
Mengapa kontrol versi bermanfaat? Bahkan saat bekerja sendiri, kontrol versi memungkinkan kita melihat snapshot lama dari proyek, menyimpan log alasan perubahan tertentu dibuat, mengerjakan cabang pengembangan paralel, dan banyak lagi. Ketika berkolaborasi dengan orang lain, kontrol versi menjadi alat yang sangat berharga untuk melihat perubahan yang telah dilakukan orang lain, serta menyelesaikan konflik dalam pengembangan bersamaan.
VCS modern juga memudahkan kita untuk menjawab pertanyaan seperti:
- Siapa yang menulis modul ini?
- Kapan baris tertentu dari file ini diedit? Oleh siapa? Mengapa diedit?
- Selama 1000 revisi terakhir, kapan/mengapa unit test tertentu berhenti bekerja?
Meskipun ada VCS lain, Git adalah standar de facto untuk kontrol versi. Komik XKCD ini menggambarkan reputasi Git:
Karena antarmuka Git merupakan abstraksi yang bocor, belajar Git dari atas ke bawah (dimulai dengan antarmuka / antarmuka baris perintah) dapat menyebabkan kebingungan. Mungkin saja menghafal serangkaian perintah dan menganggapnya sebagai mantra ajaib, lalu mengikuti pendekatan dalam komik di atas setiap kali ada masalah.
Git memang memiliki antarmuka yang kurang baik, tetapi desain dan ide yang mendasarinya indah. Antarmuka yang kurang baik harus dihafalkan, sedangkan desain yang indah dapat dipahami. Oleh karena itu, kami memberikan penjelasan Git dari bawah ke atas, dimulai dengan model datanya, kemudian membahas antarmuka baris perintah. Setelah memahami model data, perintah dapat lebih dipahami dalam konteks bagaimana mereka memanipulasi model data yang mendasarinya.
Model Data Git
Ada banyak pendekatan ad-hoc yang dapat kita ambil untuk kontrol versi. Git memiliki model yang dirancang dengan baik, memungkinkan semua fitur bagus dari kontrol versi, seperti mempertahankan riwayat, mendukung percabangan, dan memfasilitasi kolaborasi.
Snapshot
Git memodelkan riwayat koleksi file dan folder dalam beberapa direktori tingkat atas sebagai serangkaian snapshot. Dalam terminologi Git, file disebut “blob”, yang merupakan sekelompok byte. Direktori disebut “tree”, yang memetakan nama ke blob atau tree lain (sehingga direktori dapat berisi direktori lain). Snapshot adalah tree tingkat atas yang sedang dilacak. Misalnya, kita mungkin memiliki tree seperti ini:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, isi = "hello world")
|
+- baz.txt (blob, isi = "git is wonderful")
Tree tingkat atas berisi dua elemen: tree “foo” (yang sendirinya berisi satu elemen, blob “bar.txt”) dan blob “baz.txt”.
Memodelkan Riwayat: Menghubungkan Snapshot
Bagaimana seharusnya sistem kontrol versi menghubungkan snapshot? Satu model sederhana adalah memiliki riwayat linier, yaitu daftar snapshot berurutan waktu. Namun, Git tidak menggunakan model sederhana seperti ini karena berbagai alasan.
Dalam Git, riwayat adalah directed acyclic graph (DAG) dari snapshot. Istilah matematika ini hanya berarti bahwa setiap snapshot dalam Git merujuk pada satu set “orang tua”, yaitu snapshot yang mendahuluinya. Mengapa satu set orang tua, bukan satu orang tua tunggal seperti dalam riwayat linier? Karena snapshot mungkin diturunkan dari beberapa orang tua, misalnya saat menggabungkan (merge) dua cabang pengembangan paralel.
Git menyebut snapshot ini sebagai “commit”. Visualisasi riwayat commit mungkin terlihat seperti ini:
o <-- o <-- o <-- o
^
\
--- o <-- o
Dalam ilustrasi ASCII di atas, o
merepresentasikan commit individual (snapshot). Panah menunjuk ke orang tua dari setiap commit (relasi “datang sebelum”, bukan “datang setelah”). Setelah commit ketiga, riwayat bercabang menjadi dua cabang terpisah. Ini mungkin sesuai dengan, misalnya, dua fitur terpisah yang dikembangkan secara paralel dan independen. Di masa depan, cabang-cabang ini dapat digabungkan untuk membuat snapshot baru yang menggabungkan kedua fitur, menghasilkan riwayat baru seperti ini, dengan commit gabungan yang baru ditunjukkan dengan cetak tebal:
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Commit dalam Git bersifat immutable. Ini bukan berarti kesalahan tidak dapat diperbaiki. Hanya saja, “edit” pada riwayat commit sebenarnya membuat commit yang benar-benar baru, dan referensi (lihat di bawah) diperbarui untuk menunjuk ke yang baru.
Model Data Git dalam Pseudocode
Untuk lebih memahami model data Git, mari kita lihat representasinya dalam pseudocode:
// file adalah sekelompok byte
type blob = array<byte>
// direktori berisi file dan direktori yang bernama
type tree = map<string, tree | blob>
// commit memiliki orang tua, metadata, dan tree tingkat atas
type commit = struct {
parents: array<commit>
author: string
message: string
snapshot: tree
}
Model ini memberikan gambaran yang bersih dan sederhana tentang riwayat perubahan dalam Git.
Objek dan Pengalamatan Konten
Dalam Git, “objek” dapat berupa blob, tree, atau commit:
type object = blob | tree | commit
Semua objek dalam penyimpanan data Git dialamatkan berdasarkan konten mereka menggunakan hash SHA-1.
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blob, tree, dan commit terhubung dengan cara ini: semuanya adalah objek. Ketika mereka mereferensikan objek lain, mereka tidak benar-benar mengandung objek tersebut dalam representasi on-disk, melainkan hanya memiliki referensi ke objek melalui hash-nya.
Contohnya, tree untuk struktur direktori seperti yang dicontohkan sebelumnya (divisualisasikan dengan git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
), terlihat seperti ini:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
Tree itu sendiri berisi pointer ke isinya, baz.txt
(sebuah blob) dan foo
(sebuah tree). Jika kita melihat isi yang dialamatkan oleh hash untuk baz.txt dengan git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
, kita akan mendapatkan:
git is wonderful
Referensi
Sekarang, semua snapshot dapat diidentifikasi oleh hash SHA-1 mereka. Namun, ini tidak terlalu nyaman, karena manusia tidak pandai mengingat string heksadesimal sepanjang 40 karakter.
Solusi Git untuk masalah ini adalah dengan memberikan nama yang dapat dibaca manusia untuk hash SHA-1, yang disebut “referensi”. Referensi adalah pointer ke commit. Tidak seperti objek yang immutable, referensi bersifat mutable (dapat diperbarui untuk menunjuk ke commit baru). Misalnya, referensi master
biasanya menunjuk ke commit terbaru di cabang utama pengembangan.
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
Dengan ini, Git dapat menggunakan nama yang dapat dibaca manusia seperti “master” untuk merujuk ke snapshot tertentu dalam riwayat, alih-alih string heksadesimal yang panjang.
Satu detail lagi adalah bahwa kita sering memerlukan gagasan tentang “di mana kita saat ini berada” dalam riwayat, sehingga ketika kita mengambil snapshot baru, kita tahu posisinya relatif terhadap snapshot sebelumnya (untuk mengisi field parents
dari commit). Dalam Git, “di mana kita saat ini berada” direpresentasikan oleh referensi khusus yang disebut “HEAD”.
Repositori
Akhirnya, kita dapat mendefinisikan apa itu (kurang lebih) repositori Git: yaitu data objects
dan references
.
Di disk, semua penyimpanan Git terdiri dari objek dan referensi: hanya itulah model data Git. Semua perintah git
berkaitan dengan beberapa manipulasi pada DAG commit dengan menambahkan objek dan menambahkan/memperbarui referensi.
Setiap kali Anda mengetikkan perintah apa pun, pikirkan manipulasi apa yang dilakukan perintah tersebut pada struktur data grafik yang mendasarinya. Sebaliknya, jika Anda mencoba membuat jenis perubahan tertentu pada DAG commit, misalnya “buang perubahan yang belum di-commit dan buat ref ‘master’ menunjuk ke commit 5d83f9e
”, mungkin ada perintah untuk melakukannya (misalnya dalam hal ini, git checkout master; git reset --hard 5d83f9e
).
Staging Area
Ini adalah konsep lain yang ortogonal terhadap model data, tetapi merupakan bagian dari antarmuka untuk membuat commit.
Salah satu cara yang mungkin Anda bayangkan untuk mengimplementasikan snapshot seperti yang dijelaskan di atas adalah dengan memiliki perintah “buat snapshot” yang membuat snapshot baru berdasarkan status saat ini dari direktori kerja. Beberapa alat kontrol versi bekerja seperti ini, tetapi bukan Git. Kita ingin snapshot yang bersih, dan mungkin tidak selalu ideal untuk membuat snapshot dari status saat ini. Misalnya, bayangkan skenario di mana Anda telah mengimplementasikan dua fitur terpisah, dan Anda ingin membuat dua commit terpisah, di mana yang pertama memperkenalkan fitur pertama, dan yang berikutnya memperkenalkan fitur kedua. Atau bayangkan skenario di mana Anda memiliki pernyataan debug print yang ditambahkan di seluruh kode Anda, bersama dengan perbaikan bug; Anda ingin melakukan commit perbaikan bug sambil membuang semua pernyataan print.
Git mengakomodasi skenario seperti itu dengan memungkinkan Anda menentukan modifikasi mana yang harus disertakan dalam snapshot berikutnya melalui mekanisme yang disebut “staging area”.
Antarmuka Baris Perintah Git
Untuk menghindari duplikasi informasi, kami tidak akan menjelaskan perintah di bawah ini secara rinci. Lihat Pro Git yang sangat direkomendasikan untuk informasi lebih lanjut, atau tonton video kuliah.
Dasar-dasar
{% comment %}
Perintah git init
menginisialisasi repositori Git baru, dengan metadata repositori disimpan dalam direktori .git
:
$ mkdir proyeksaya
$ cd proyeksaya
$ git init
Initialized empty Git repository in /home/missing-semester/proyeksaya/.git/
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
Bagaimana kita menafsirkan output ini? “No commits yet” pada dasarnya berarti riwayat versi kita masih kosong. Mari kita perbaiki itu.
$ echo "halo, git" > halo.txt
$ git add halo.txt
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: halo.txt
$ git commit -m 'Commit awal'
[master (root-commit) 4515d17] Commit awal
1 file changed, 1 insertion(+)
create mode 100644 halo.txt
Dengan ini, kita telah git add
file ke staging area, dan kemudian git commit
perubahan tersebut, menambahkan pesan commit sederhana “Initial commit”. Jika kita tidak menentukan opsi -m
, Git akan membuka editor teks kita untuk memungkinkan kita mengetik pesan commit.
Setelah memiliki riwayat versi yang tidak kosong, kita dapat memvisualisasikan riwayat tersebut. Memvisualisasikan riwayat perubahan sebagai graf (DAG - Directed Acyclic Graph) sangat membantu dalam memahami status terkini repositori kita. Ini ibarat melihat peta perjalanan pengembangan proyek kita.
Perintah git log
memvisualisasikan riwayat tersebut. Secara default, perintah ini menampilkan versi yang diratakan, yang menyembunyikan struktur graf. Jika Kita menggunakan perintah seperti git log --all --graph --decorate
, itu akan menampilkan versi lengkap dari riwayat repositori, divisualisasikan dalam bentuk graf.
$ git log --all --graph --decorate
* commit 4515d17a167bdef0a91ee7d50d75b12c9c2652aa (HEAD -> master)
Author: Missing Semester <[email protected]>
Date: Tue Jan 21 22:18:36 2020 -0500
Commit awal
Ini belum terlihat seperti graf, karena hanya berisi satu node saja. Mari kita buat beberapa perubahan lagi, buat sebuah commit baru, dan visualisasikan riwayatnya sekali lagi.
$ echo "baris lain" >> halo.txt
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: halo.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ git add halo.txt
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: halo.txt
$ git commit -m 'Tambahkan baris'
[master 35f60a8] Tambahkan baris
1 file changed, 1 insertion(+)
Sekarang, jika kita memvisualisasikan riwayatnya lagi, kita akan melihat beberapa struktur graf:
* commit 35f60a825be0106036dd2fbc7657598eb7b04c67 (HEAD -> master)
| Author: Missing Semester <[email protected]>
| Date: Tue Jan 21 22:26:20 2020 -0500
|
| Tambahkan baris
|
* commit 4515d17a167bdef0a91ee7d50d75b12c9c2652aa
Author: Missing Semester <[email protected]>
Date: Tue Jan 21 22:18:36 2020 -0500
Commit awal
Perhatikan juga bahwa itu menunjukkan HEAD saat ini, bersama dengan cabang saat ini (master).
Kita dapat melihat versi lama menggunakan perintah git checkout
.
$ git checkout 4515d17 # hash commit sebelumnya; milik Anda akan berbeda
Note: checking out '4515d17'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 4515d17 Commit awal
$ cat halo.txt
halo, git
$ git checkout master
Previous HEAD position was 4515d17 Commit awal
Switched to branch 'master'
$ cat halo.txt
halo, git
baris lain
Git dapat menunjukkan kepada Kita bagaimana file telah berevolusi (perbedaan, atau diff) menggunakan perintah git diff
:
$ git diff 4515d17 halo.txt
diff --git c/halo.txt w/halo.txt
index 94bab17..f0013b2 100644
--- c/halo.txt
+++ w/halo.txt
@@ -1 +1,2 @@
halo, git
+baris lain
{% endcomment %}
Berikut adalah beberapa perintah Git dasar yang sering digunakan:
git help <perintah>
: Mendapatkan bantuan untuk perintah Git tertentu.git init
: Membuat repositori Git baru, dengan data yang disimpan dalam direktori.git
.git status
: Memberitahu kita tentang status terkini repositori.git add <nama_file>
: Menambahkan file ke staging area, persiapan sebelum commit.git commit
: Membuat commit baru, menyimpan perubahan secara permanen dalam riwayat versi.- Tulislah pesan commit yang baik! Ini akan sangat membantu dalam memahami riwayat perubahan.
- Lebih banyak alasan untuk menulis pesan commit yang informatif!
git log
: Menampilkan riwayat commit dalam bentuk yang diratakan.git log --all --graph --decorate
: Memvisualisasikan riwayat commit sebagai graf (DAG - Directed Acyclic Graph).git diff <nama_file>
: Menunjukkan perubahan yang kita buat pada file, relatif terhadap versi di staging area.git diff <revisi> <nama_file>
: Menunjukkan perbedaan dalam file antara dua snapshot (commit) berbeda.git checkout <revisi>
: Memperbarui HEAD dan cabang saat ini ke revisi (commit) tertentu.
Dengan memahami perintah-perintah dasar ini, Anda sudah dapat mulai menggunakan Git secara efektif untuk mengelola riwayat perubahan dalam proyek Anda.
Branching dan Merging
Branching memungkinkan kita untuk “membelokkan” riwayat versi, sehingga kita dapat mengerjakan fitur atau perbaikan bug secara terpisah dan paralel. Perintah git branch
digunakan untuk membuat cabang baru, sedangkan git checkout -b <nama cabang>
membuat sekaligus beralih ke cabang tersebut.
Di sisi lain, merging adalah proses menggabungkan riwayat versi yang telah bercabang, misalnya saat ingin menggabungkan cabang fitur kembali ke cabang utama. Perintah git merge
digunakan untuk keperluan ini.
Berikut adalah beberapa perintah terkait branching dan merging:
git branch
: Menampilkan daftar cabang yang ada.git branch <nama>
: Membuat cabang baru dengan nama tertentu.git checkout -b <nama>
: Membuat cabang baru dan langsung beralih ke cabang tersebut. Ini setara dengan menjalankangit branch <nama>
dilanjutkan dengangit checkout <nama>
.git merge <revisi>
: Menggabungkan cabang atau revisi tertentu ke cabang saat ini.git mergetool
: Menggunakan alat canggih untuk membantu menyelesaikan konflik saat melakukan penggabungan.git rebase
: Memindahkan sekumpulan perubahan (patch) ke basis baru.
Remote
Remote merujuk pada repositori Git yang berada di tempat lain, seperti di server atau di layanan hosting Git. Berikut adalah beberapa perintah terkait remote:
git remote
: Menampilkan daftar remote yang terhubung.git remote add <nama> <url>
: Menambahkan remote baru dengan nama dan URL tertentu.git push <remote> <cabang lokal>:<cabang remote>
: Mengirim objek ke remote dan memperbarui referensi remote.git branch --set-upstream-to=<remote>/<cabang remote>
: Mengatur hubungan antara cabang lokal dan cabang remote.git fetch
: Mengambil objek dan referensi terbaru dari remote.git pull
: Melakukangit fetch
dangit merge
sekaligus untuk mendapatkan perubahan terbaru dari remote.git clone
: Mengunduh repositori dari remote.
Undo
Kadang kita perlu membatalkan perubahan yang telah dilakukan. Berikut adalah beberapa perintah untuk melakukan “undo” di Git:
git commit --amend
: Mengedit isi atau pesan commit terakhir.git reset HEAD <file>
: Menghapus file dari staging area (unstage).git checkout -- <file>
: Membuang perubahan yang belum di-commit pada file tertentu.
Git Lanjutan
Git menyediakan banyak fitur dan kustomisasi lanjutan. Beberapa di antaranya:
git config
: Git sangat dapat disesuaikan.git clone --depth=1
: Melakukan clone dangkal, tanpa mengambil seluruh riwayat versi.git add -p
: Melakukan staging secara interaktif.git rebase -i
: Melakukan rebasing secara interaktif.git blame
: Menunjukkan siapa yang terakhir mengedit setiap baris file.git stash
: Menyimpan sementara perubahan yang belum di-commit.git bisect
: Melakukan pencarian biner pada riwayat commit (berguna untuk menemukan penyebab regresi)..gitignore
: Mendefinisikan file yang sengaja diabaikan dan tidak terlacak oleh Git.
Lain-lain
- GUI: Ada banyak klien GUI untuk Git. Kami secara pribadi tidak menggunakannya dan lebih memilih antarmuka baris perintah.
- Integrasi shell: Sangat berguna untuk memiliki status Git sebagai bagian dari prompt shell kita (zsh, bash). Fitur ini sering disertakan dalam framework seperti Oh My Zsh.
- Integrasi editor: Serupa dengan di atas, integrasi yang berguna dengan banyak fitur. fugitive.vim adalah standar untuk Vim.
- Workflows: Kami mengajarkan model data Git, ditambah beberapa perintah dasar. Namun, kami tidak memberi tahu praktik apa yang harus diikuti saat mengerjakan proyek besar (dan ada banyak berbagai pendekatan).
- GitHub: Git berbeda dengan GitHub. GitHub memiliki cara spesifik untuk berkontribusi kode ke proyek lain, yang disebut pull request.
- Penyedia Git lainnya: GitHub bukan satu-satunya penyedia layanan Git. Ada banyak host repositori Git lainnya, seperti GitLab dan BitBucket.
Sumber Daya
- Pro Git sangat direkomendasikan untuk dibaca. Membaca Bab 1—5 akan mengajarkan sebagian besar hal yang Anda butuhkan untuk menggunakan Git secara mahir, setelah memahami model datanya. Bab-bab selanjutnya memiliki materi menarik dan lanjutan.
- Oh Shit, Git!?! adalah panduan singkat tentang cara memulihkan dari beberapa kesalahan Git yang umum.
- Git untuk Ilmuwan Komputer adalah penjelasan singkat tentang model data Git, dengan lebih sedikit pseudocode dan lebih banyak diagram mewah dibandingkan catatan kuliah ini.
- Git from the Bottom Up adalah penjelasan rinci tentang detail implementasi Git di luar model data saja, untuk yang penasaran.
- Cara menjelaskan git dalam kata-kata sederhana
- Learn Git Branching adalah game berbasis browser yang mengajarkan Git.
Latihan
- Jika Anda belum memiliki pengalaman dengan Git, cobalah membaca beberapa bab pertama dari Pro Git atau ikuti tutorial seperti Learn Git Branching. Saat mengerjakan latihan, kaitkan perintah Git dengan model data.
- Clone repositori untuk situs web kelas.
- Jelajahi riwayat versi dengan memvisualisasikannya sebagai graf.
- Siapa orang terakhir yang memodifikasi
README.md
? (Petunjuk: gunakangit log
dengan argumen). - Apa pesan commit yang terkait dengan modifikasi terakhir pada baris
collections:
dari_config.yml
? (Petunjuk: gunakangit blame
dangit show
).
- Salah satu kesalahan umum saat belajar Git adalah melakukan commit file besar yang seharusnya tidak dikelola oleh Git atau menambahkan informasi sensitif. Cobalah menambahkan file ke repositori, buat beberapa commit, lalu hapus file tersebut dari riwayat (Anda mungkin ingin melihat ini).
- Clone beberapa repositori dari GitHub, dan modifikasi salah satu file yang ada. Apa yang terjadi ketika Anda menjalankan
git stash
? Apa yang Anda lihat saat menjalankangit log --all --oneline
? Jalankangit stash pop
untuk membatalkan apa yang Anda lakukan dengangit stash
. Dalam skenario apa ini mungkin berguna? - Seperti banyak alat baris perintah, Git menyediakan file konfigurasi (atau dotfile) yang disebut
~/.gitconfig
. Buat alias di~/.gitconfig
sehingga ketika Anda menjalankangit graph
, Anda mendapatkan output darigit log --all --graph --decorate --oneline
. Anda dapat melakukan ini dengan langsung mengedit file~/.gitconfig
, atau menggunakan perintahgit config
untuk menambahkan alias. Informasi tentang alias git dapat ditemukan di sini. - Anda dapat mendefinisikan pola global untuk diabaikan di
~/.gitignore_global
setelah menjalankangit config --global core.excludesfile ~/.gitignore_global
. Lakukan ini, dan atur file gitignore global Anda untuk mengabaikan file sementara spesifik OS atau spesifik editor, seperti.DS_Store
. - Fork repositori untuk situs web kelas, temukan kesalahan ketik atau peningkatan lain yang dapat Anda buat, dan ajukan pull request di GitHub (Anda mungkin ingin melihat ini). Harap hanya kirimkan PR yang berguna (jangan spam, tolong!). Jika Anda tidak dapat menemukan peningkatan untuk dilakukan, Anda dapat melewati latihan ini.