Skip to content

Version Control (Git) - seri Missing Semester

Updated: at 06.30

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:

Meskipun ada VCS lain, Git adalah standar de facto untuk kontrol versi. Komik XKCD ini menggambarkan reputasi Git:

xkcd 1597

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:

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:

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:

Undo

Kadang kita perlu membatalkan perubahan yang telah dilakukan. Berikut adalah beberapa perintah untuk melakukan “undo” di Git:

Git Lanjutan

Git menyediakan banyak fitur dan kustomisasi lanjutan. Beberapa di antaranya:

Lain-lain

Sumber Daya

Latihan

  1. 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.
  2. Clone repositori untuk situs web kelas.
    1. Jelajahi riwayat versi dengan memvisualisasikannya sebagai graf.
    2. Siapa orang terakhir yang memodifikasi README.md? (Petunjuk: gunakan git log dengan argumen).
    3. Apa pesan commit yang terkait dengan modifikasi terakhir pada baris collections: dari _config.yml? (Petunjuk: gunakan git blame dan git show).
  3. 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).
  4. 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 menjalankan git log --all --oneline? Jalankan git stash pop untuk membatalkan apa yang Anda lakukan dengan git stash. Dalam skenario apa ini mungkin berguna?
  5. Seperti banyak alat baris perintah, Git menyediakan file konfigurasi (atau dotfile) yang disebut ~/.gitconfig. Buat alias di ~/.gitconfig sehingga ketika Anda menjalankan git graph, Anda mendapatkan output dari git log --all --graph --decorate --oneline. Anda dapat melakukan ini dengan langsung mengedit file ~/.gitconfig, atau menggunakan perintah git config untuk menambahkan alias. Informasi tentang alias git dapat ditemukan di sini.
  6. Anda dapat mendefinisikan pola global untuk diabaikan di ~/.gitignore_global setelah menjalankan git 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.
  7. 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.